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:
parent
8a3e6f0179
commit
a279548c89
@ -10,7 +10,7 @@ type DurationServiceMock struct {
|
|||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DurationServiceMock) Get(time time.Time, time2 time.Time, user *models.User) (models.Durations, error) {
|
func (m *DurationServiceMock) Get(time time.Time, time2 time.Time, user *models.User, f *models.Filters) (models.Durations, error) {
|
||||||
args := m.Called(time, time2, user)
|
args := m.Called(time, time2, user, f)
|
||||||
return args.Get(0).(models.Durations), args.Error(1)
|
return args.Get(0).(models.Durations), args.Error(1)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
package models
|
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 {
|
type Alias struct {
|
||||||
ID uint `gorm:"primary_key"`
|
ID uint `gorm:"primary_key"`
|
||||||
Type uint8 `gorm:"not null; index:idx_alias_type_key"`
|
Type uint8 `gorm:"not null; index:idx_alias_type_key"`
|
||||||
|
@ -3,7 +3,6 @@ package v1
|
|||||||
import (
|
import (
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// https://shields.io/endpoint
|
// https://shields.io/endpoint
|
||||||
@ -20,18 +19,11 @@ type BadgeData struct {
|
|||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBadgeDataFrom(summary *models.Summary, filters *models.Filters) *BadgeData {
|
func NewBadgeDataFrom(summary *models.Summary) *BadgeData {
|
||||||
var total time.Duration
|
|
||||||
if hasFilter, _, _ := filters.One(); hasFilter {
|
|
||||||
total = summary.TotalTimeByFilters(filters)
|
|
||||||
} else {
|
|
||||||
total = summary.TotalTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
return &BadgeData{
|
return &BadgeData{
|
||||||
SchemaVersion: 1,
|
SchemaVersion: 1,
|
||||||
Label: defaultLabel,
|
Label: defaultLabel,
|
||||||
Message: utils.FmtWakatimeDuration(total),
|
Message: utils.FmtWakatimeDuration(summary.TotalTime()),
|
||||||
Color: defaultColor,
|
Color: defaultColor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package v1
|
|||||||
import (
|
import (
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// https://wakatime.com/developers#all_time_since_today
|
// https://wakatime.com/developers#all_time_since_today
|
||||||
@ -27,14 +26,8 @@ type AllTimeRange struct {
|
|||||||
Timezone string `json:"timezone"`
|
Timezone string `json:"timezone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAllTimeFrom(summary *models.Summary, filters *models.Filters) *AllTimeViewModel {
|
func NewAllTimeFrom(summary *models.Summary) *AllTimeViewModel {
|
||||||
var total time.Duration
|
total := summary.TotalTime()
|
||||||
if key := filters.Project; key != "" {
|
|
||||||
total = summary.TotalTimeByFilters(filters)
|
|
||||||
} else {
|
|
||||||
total = summary.TotalTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
return &AllTimeViewModel{
|
return &AllTimeViewModel{
|
||||||
Data: &AllTimeData{
|
Data: &AllTimeData{
|
||||||
TotalSeconds: float32(total.Seconds()),
|
TotalSeconds: float32(total.Seconds()),
|
||||||
|
@ -57,8 +57,7 @@ type SummariesRange struct {
|
|||||||
Timezone string `json:"timezone"`
|
Timezone string `json:"timezone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSummariesFrom(summaries []*models.Summary, filters *models.Filters) *SummariesViewModel {
|
func NewSummariesFrom(summaries []*models.Summary) *SummariesViewModel {
|
||||||
// TODO: implement filtering (https://github.com/muety/wakapi/issues/58)
|
|
||||||
data := make([]*SummariesData, len(summaries))
|
data := make([]*SummariesData, len(summaries))
|
||||||
minDate, maxDate := time.Now().Add(1*time.Second), time.Time{}
|
minDate, maxDate := time.Now().Add(1*time.Second), time.Time{}
|
||||||
|
|
||||||
|
@ -1,50 +1,158 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/emvi/logbuch"
|
||||||
|
"github.com/mitchellh/hashstructure/v2"
|
||||||
|
)
|
||||||
|
|
||||||
type Filters struct {
|
type Filters struct {
|
||||||
Project string
|
Project OrFilter
|
||||||
OS string
|
OS OrFilter
|
||||||
Language string
|
Language OrFilter
|
||||||
Editor string
|
Editor OrFilter
|
||||||
Machine string
|
Machine OrFilter
|
||||||
Label string
|
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 FilterElement struct {
|
||||||
Type uint8
|
entity uint8
|
||||||
Key string
|
filter OrFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFiltersWith(entity uint8, key string) *Filters {
|
func NewFiltersWith(entity uint8, key string) *Filters {
|
||||||
switch entity {
|
return NewFilterWithMultiple(entity, []string{key})
|
||||||
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{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filters) One() (bool, uint8, string) {
|
func NewFilterWithMultiple(entity uint8, keys []string) *Filters {
|
||||||
if f.Project != "" {
|
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
|
return true, SummaryProject, f.Project
|
||||||
} else if f.OS != "" {
|
} else if f.OS != nil && f.OS.Exists() {
|
||||||
return true, SummaryOS, f.OS
|
return true, SummaryOS, f.OS
|
||||||
} else if f.Language != "" {
|
} else if f.Language != nil && f.Language.Exists() {
|
||||||
return true, SummaryLanguage, f.Language
|
return true, SummaryLanguage, f.Language
|
||||||
} else if f.Editor != "" {
|
} else if f.Editor != nil && f.Editor.Exists() {
|
||||||
return true, SummaryEditor, f.Editor
|
return true, SummaryEditor, f.Editor
|
||||||
} else if f.Machine != "" {
|
} else if f.Machine != nil && f.Machine.Exists() {
|
||||||
return true, SummaryMachine, f.Machine
|
return true, SummaryMachine, f.Machine
|
||||||
} else if f.Label != "" {
|
} else if f.Label != nil && f.Label.Exists() {
|
||||||
return true, SummaryLabel, f.Label
|
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
|
||||||
}
|
}
|
||||||
|
@ -57,8 +57,6 @@ type SummaryParams struct {
|
|||||||
Recompute bool
|
Recompute bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type AliasResolver func(t uint8, k string) string
|
|
||||||
|
|
||||||
func SummaryTypes() []uint8 {
|
func SummaryTypes() []uint8 {
|
||||||
return []uint8{SummaryProject, SummaryLanguage, SummaryEditor, SummaryOS, SummaryMachine, SummaryLabel}
|
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
|
return timeSum
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Summary) TotalTimeByFilters(filters *Filters) time.Duration {
|
func (s *Summary) TotalTimeByFilter(filter FilterElement) time.Duration {
|
||||||
do, typeId, key := filters.One()
|
var total time.Duration
|
||||||
if do {
|
for _, f := range filter.filter {
|
||||||
return s.TotalTimeByKey(typeId, key)
|
total += s.TotalTimeByKey(filter.entity, f)
|
||||||
}
|
}
|
||||||
return 0
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Summary) MaxBy(entityType uint8) *SummaryItem {
|
func (s *Summary) MaxBy(entityType uint8) *SummaryItem {
|
||||||
|
@ -98,20 +98,13 @@ func TestSummary_TotalTimeByFilters(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specifying filters about multiple entites is not supported at the moment
|
filters1 := NewFiltersWith(SummaryProject, "wakapi").OneOrEmpty()
|
||||||
// as the current, very rudimentary, time calculation logic wouldn't make sense then.
|
filters2 := NewFiltersWith(SummaryLanguage, "Go").OneOrEmpty()
|
||||||
// Evaluating a filter like (project="wakapi", language="go") can only be realized
|
filters3 := FilterElement{}
|
||||||
// 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 := &Filters{Project: "wakapi"}
|
assert.Equal(t, testDuration1, sut.TotalTimeByFilter(filters1))
|
||||||
filters2 := &Filters{Language: "Go"}
|
assert.Equal(t, testDuration3, sut.TotalTimeByFilter(filters2))
|
||||||
filters3 := &Filters{}
|
assert.Zero(t, sut.TotalTimeByFilter(filters3))
|
||||||
|
|
||||||
assert.Equal(t, testDuration1, sut.TotalTimeByFilters(filters1))
|
|
||||||
assert.Equal(t, testDuration3, sut.TotalTimeByFilters(filters2))
|
|
||||||
assert.Zero(t, sut.TotalTimeByFilters(filters3))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSummary_WithResolvedAliases(t *testing.T) {
|
func TestSummary_WithResolvedAliases(t *testing.T) {
|
||||||
|
@ -116,7 +116,7 @@ func (h *MetricsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error) {
|
func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error) {
|
||||||
var metrics mm.Metrics
|
var metrics mm.Metrics
|
||||||
|
|
||||||
summaryAllTime, err := h.summarySrvc.Aliased(time.Time{}, time.Now(), user, h.summarySrvc.Retrieve, false)
|
summaryAllTime, err := h.summarySrvc.Aliased(time.Time{}, time.Now(), user, h.summarySrvc.Retrieve, nil, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logbuch.Error("failed to retrieve all time summary for user '%s' for metric", user.ID)
|
logbuch.Error("failed to retrieve all time summary for user '%s' for metric", user.ID)
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -124,7 +124,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
|
|
||||||
from, to := utils.MustResolveIntervalRawTZ("today", user.TZ())
|
from, to := utils.MustResolveIntervalRawTZ("today", user.TZ())
|
||||||
|
|
||||||
summaryToday, err := h.summarySrvc.Aliased(from, to, user, h.summarySrvc.Retrieve, false)
|
summaryToday, err := h.summarySrvc.Aliased(from, to, user, h.summarySrvc.Retrieve, nil, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logbuch.Error("failed to retrieve today's summary for user '%s' for metric", user.ID)
|
logbuch.Error("failed to retrieve today's summary for user '%s' for metric", user.ID)
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -141,7 +141,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_cumulative_seconds_total",
|
Name: MetricsPrefix + "_cumulative_seconds_total",
|
||||||
Desc: DescAllTime,
|
Desc: DescAllTime,
|
||||||
Value: int(v1.NewAllTimeFrom(summaryAllTime, &models.Filters{}).Data.TotalSeconds),
|
Value: int(v1.NewAllTimeFrom(summaryAllTime).Data.TotalSeconds),
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -122,19 +122,19 @@ func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err, status := h.loadUserSummary(user, interval)
|
summary, err, status := h.loadUserSummary(user, interval, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
vm := v1.NewBadgeDataFrom(summary, filters)
|
vm := v1.NewBadgeDataFrom(summary)
|
||||||
h.cache.SetDefault(cacheKey, vm)
|
h.cache.SetDefault(cacheKey, vm)
|
||||||
utils.RespondJSON(w, r, http.StatusOK, vm)
|
utils.RespondJSON(w, r, http.StatusOK, vm)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *BadgeHandler) loadUserSummary(user *models.User, interval *models.IntervalKey) (*models.Summary, error, int) {
|
func (h *BadgeHandler) loadUserSummary(user *models.User, interval *models.IntervalKey, filters *models.Filters) (*models.Summary, error, int) {
|
||||||
err, from, to := utils.ResolveIntervalTZ(interval, user.TZ())
|
err, from, to := utils.ResolveIntervalTZ(interval, user.TZ())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err, http.StatusBadRequest
|
return nil, err, http.StatusBadRequest
|
||||||
@ -151,7 +151,14 @@ func (h *BadgeHandler) loadUserSummary(user *models.User, interval *models.Inter
|
|||||||
retrieveSummary = h.summarySrvc.Summarize
|
retrieveSummary = h.summarySrvc.Summarize
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err := h.summarySrvc.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, summaryParams.Recompute)
|
summary, err := h.summarySrvc.Aliased(
|
||||||
|
summaryParams.From,
|
||||||
|
summaryParams.To,
|
||||||
|
summaryParams.User,
|
||||||
|
retrieveSummary,
|
||||||
|
filters,
|
||||||
|
summaryParams.Recompute,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err, http.StatusInternalServerError
|
return nil, err, http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
conf "github.com/muety/wakapi/config"
|
conf "github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/middlewares"
|
"github.com/muety/wakapi/middlewares"
|
||||||
@ -13,6 +9,8 @@ import (
|
|||||||
routeutils "github.com/muety/wakapi/routes/utils"
|
routeutils "github.com/muety/wakapi/routes/utils"
|
||||||
"github.com/muety/wakapi/services"
|
"github.com/muety/wakapi/services"
|
||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AllTimeHandler struct {
|
type AllTimeHandler struct {
|
||||||
@ -47,25 +45,23 @@ func (h *AllTimeHandler) RegisterRoutes(router *mux.Router) {
|
|||||||
// @Success 200 {object} v1.AllTimeViewModel
|
// @Success 200 {object} v1.AllTimeViewModel
|
||||||
// @Router /compat/wakatime/v1/users/{user}/all_time_since_today [get]
|
// @Router /compat/wakatime/v1/users/{user}/all_time_since_today [get]
|
||||||
func (h *AllTimeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
func (h *AllTimeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||||
values, _ := url.ParseQuery(r.URL.RawQuery)
|
|
||||||
|
|
||||||
user, err := routeutils.CheckEffectiveUser(w, r, h.userSrvc, "current")
|
user, err := routeutils.CheckEffectiveUser(w, r, h.userSrvc, "current")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return // response was already sent by util function
|
return // response was already sent by util function
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err, status := h.loadUserSummary(user)
|
summary, err, status := h.loadUserSummary(user, routeutils.ParseFilters(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
vm := v1.NewAllTimeFrom(summary, models.NewFiltersWith(models.SummaryProject, values.Get("project")))
|
vm := v1.NewAllTimeFrom(summary)
|
||||||
utils.RespondJSON(w, r, http.StatusOK, vm)
|
utils.RespondJSON(w, r, http.StatusOK, vm)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AllTimeHandler) loadUserSummary(user *models.User) (*models.Summary, error, int) {
|
func (h *AllTimeHandler) loadUserSummary(user *models.User, filters *models.Filters) (*models.Summary, error, int) {
|
||||||
summaryParams := &models.SummaryParams{
|
summaryParams := &models.SummaryParams{
|
||||||
From: time.Time{},
|
From: time.Time{},
|
||||||
To: time.Now(),
|
To: time.Now(),
|
||||||
@ -78,7 +74,14 @@ func (h *AllTimeHandler) loadUserSummary(user *models.User) (*models.Summary, er
|
|||||||
retrieveSummary = h.summarySrvc.Summarize
|
retrieveSummary = h.summarySrvc.Summarize
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err := h.summarySrvc.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, summaryParams.Recompute)
|
summary, err := h.summarySrvc.Aliased(
|
||||||
|
summaryParams.From,
|
||||||
|
summaryParams.To,
|
||||||
|
summaryParams.User,
|
||||||
|
retrieveSummary,
|
||||||
|
filters,
|
||||||
|
summaryParams.Recompute,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err, http.StatusInternalServerError
|
return nil, err, http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/muety/wakapi/middlewares"
|
"github.com/muety/wakapi/middlewares"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
||||||
|
routeutils "github.com/muety/wakapi/routes/utils"
|
||||||
"github.com/muety/wakapi/services"
|
"github.com/muety/wakapi/services"
|
||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
)
|
)
|
||||||
@ -88,7 +89,7 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err, status := h.loadUserSummary(requestedUser, rangeFrom, rangeTo)
|
summary, err, status := h.loadUserSummary(requestedUser, rangeFrom, rangeTo, routeutils.ParseFilters(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
@ -117,7 +118,7 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.RespondJSON(w, r, http.StatusOK, stats)
|
utils.RespondJSON(w, r, http.StatusOK, stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *StatsHandler) loadUserSummary(user *models.User, start, end time.Time) (*models.Summary, error, int) {
|
func (h *StatsHandler) loadUserSummary(user *models.User, start, end time.Time, filters *models.Filters) (*models.Summary, error, int) {
|
||||||
overallParams := &models.SummaryParams{
|
overallParams := &models.SummaryParams{
|
||||||
From: start,
|
From: start,
|
||||||
To: end,
|
To: end,
|
||||||
@ -125,7 +126,7 @@ func (h *StatsHandler) loadUserSummary(user *models.User, start, end time.Time)
|
|||||||
Recompute: false,
|
Recompute: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err := h.summarySrvc.Aliased(overallParams.From, overallParams.To, user, h.summarySrvc.Retrieve, false)
|
summary, err := h.summarySrvc.Aliased(overallParams.From, overallParams.To, user, h.summarySrvc.Retrieve, filters, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err, http.StatusInternalServerError
|
return nil, err, http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ func (h *StatusBarHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
summariesView := v1.NewSummariesFrom([]*models.Summary{summary}, &models.Filters{})
|
summariesView := v1.NewSummariesFrom([]*models.Summary{summary})
|
||||||
utils.RespondJSON(w, r, http.StatusOK, StatusBarViewModel{
|
utils.RespondJSON(w, r, http.StatusOK, StatusBarViewModel{
|
||||||
CachedAt: time.Now(),
|
CachedAt: time.Now(),
|
||||||
Data: *summariesView.Data[0],
|
Data: *summariesView.Data[0],
|
||||||
@ -98,7 +98,7 @@ func (h *StatusBarHandler) loadUserSummary(user *models.User, start, end time.Ti
|
|||||||
retrieveSummary = h.summarySrvc.Summarize
|
retrieveSummary = h.summarySrvc.Summarize
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err := h.summarySrvc.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, summaryParams.Recompute)
|
summary, err := h.summarySrvc.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, nil, summaryParams.Recompute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, http.StatusInternalServerError, err
|
return nil, http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
@ -68,12 +68,7 @@ func (h *SummariesHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filters := &models.Filters{}
|
vm := v1.NewSummariesFrom(summaries)
|
||||||
if projectQuery := r.URL.Query().Get("project"); projectQuery != "" {
|
|
||||||
filters.Project = projectQuery
|
|
||||||
}
|
|
||||||
|
|
||||||
vm := v1.NewSummariesFrom(summaries, filters)
|
|
||||||
utils.RespondJSON(w, r, http.StatusOK, vm)
|
utils.RespondJSON(w, r, http.StatusOK, vm)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,8 +125,11 @@ func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary
|
|||||||
intervals := utils.SplitRangeByDays(overallParams.From, overallParams.To)
|
intervals := utils.SplitRangeByDays(overallParams.From, overallParams.To)
|
||||||
summaries := make([]*models.Summary, len(intervals))
|
summaries := make([]*models.Summary, len(intervals))
|
||||||
|
|
||||||
|
// filtering
|
||||||
|
filters := routeutils.ParseFilters(r)
|
||||||
|
|
||||||
for i, interval := range intervals {
|
for i, interval := range intervals {
|
||||||
summary, err := h.summarySrvc.Aliased(interval[0], interval[1], user, h.summarySrvc.Retrieve, end.After(time.Now()))
|
summary, err := h.summarySrvc.Aliased(interval[0], interval[1], user, h.summarySrvc.Retrieve, filters, end.After(time.Now()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err, http.StatusInternalServerError
|
return nil, err, http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ func LoadUserSummary(ss services.ISummaryService, r *http.Request) (*models.Summ
|
|||||||
retrieveSummary = ss.Summarize
|
retrieveSummary = ss.Summarize
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err := ss.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, summaryParams.Recompute)
|
summary, err := ss.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, ParseFilters(r), summaryParams.Recompute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err, http.StatusInternalServerError
|
return nil, err, http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
@ -30,3 +30,26 @@ func LoadUserSummary(ss services.ISummaryService, r *http.Request) (*models.Summ
|
|||||||
|
|
||||||
return summary, nil, http.StatusOK
|
return summary, nil, http.StatusOK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseFilters(r *http.Request) *models.Filters {
|
||||||
|
filters := &models.Filters{}
|
||||||
|
if q := r.URL.Query().Get("project"); q != "" {
|
||||||
|
filters.With(models.SummaryProject, q)
|
||||||
|
}
|
||||||
|
if q := r.URL.Query().Get("language"); q != "" {
|
||||||
|
filters.With(models.SummaryLanguage, q)
|
||||||
|
}
|
||||||
|
if q := r.URL.Query().Get("editor"); q != "" {
|
||||||
|
filters.With(models.SummaryEditor, q)
|
||||||
|
}
|
||||||
|
if q := r.URL.Query().Get("machine"); q != "" {
|
||||||
|
filters.With(models.SummaryMachine, q)
|
||||||
|
}
|
||||||
|
if q := r.URL.Query().Get("operating_system"); q != "" {
|
||||||
|
filters.With(models.SummaryOS, q)
|
||||||
|
}
|
||||||
|
if q := r.URL.Query().Get("label"); q != "" {
|
||||||
|
filters.With(models.SummaryLabel, q)
|
||||||
|
}
|
||||||
|
return filters
|
||||||
|
}
|
||||||
|
@ -83,7 +83,7 @@ func (srv *AggregationService) Run(userIds map[string]bool) error {
|
|||||||
|
|
||||||
func (srv *AggregationService) summaryWorker(jobs <-chan *AggregationJob, summaries chan<- *models.Summary) {
|
func (srv *AggregationService) summaryWorker(jobs <-chan *AggregationJob, summaries chan<- *models.Summary) {
|
||||||
for job := range jobs {
|
for job := range jobs {
|
||||||
if summary, err := srv.summaryService.Summarize(job.From, job.To, &models.User{ID: job.UserID}); err != nil {
|
if summary, err := srv.summaryService.Summarize(job.From, job.To, &models.User{ID: job.UserID}, nil); err != nil {
|
||||||
config.Log().Error("failed to generate summary (%v, %v, %s) – %v", job.From, job.To, job.UserID, err)
|
config.Log().Error("failed to generate summary (%v, %v, %s) – %v", job.From, job.To, job.UserID, err)
|
||||||
} else {
|
} else {
|
||||||
logbuch.Info("successfully generated summary (%v, %v, %s)", job.From, job.To, job.UserID)
|
logbuch.Info("successfully generated summary (%v, %v, %s)", job.From, job.To, job.UserID)
|
||||||
|
@ -2,6 +2,7 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/emvi/logbuch"
|
"github.com/emvi/logbuch"
|
||||||
"github.com/muety/wakapi/config"
|
"github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
@ -38,35 +39,53 @@ func (srv *AliasService) InitializeUser(userId string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *AliasService) GetByUser(userId string) ([]*models.Alias, error) {
|
func (srv *AliasService) MayInitializeUser(userId string) {
|
||||||
aliases, err := srv.repository.GetByUser(userId)
|
if err := srv.InitializeUser(userId); err != nil {
|
||||||
if err != nil {
|
logbuch.Error("failed to initialize user alias map for user %s", userId)
|
||||||
return nil, err
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *AliasService) GetByUser(userId string) ([]*models.Alias, error) {
|
||||||
|
if !srv.IsInitialized(userId) {
|
||||||
|
srv.MayInitializeUser(userId)
|
||||||
|
}
|
||||||
|
if aliases, ok := userAliases.Load(userId); ok {
|
||||||
|
return aliases.([]*models.Alias), nil
|
||||||
|
} else {
|
||||||
|
return nil, errors.New(fmt.Sprintf("no user aliases loaded for user %s", userId))
|
||||||
}
|
}
|
||||||
return aliases, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *AliasService) GetByUserAndKeyAndType(userId, key string, summaryType uint8) ([]*models.Alias, error) {
|
func (srv *AliasService) GetByUserAndKeyAndType(userId, key string, summaryType uint8) ([]*models.Alias, error) {
|
||||||
aliases, err := srv.repository.GetByUserAndKeyAndType(userId, key, summaryType)
|
if !srv.IsInitialized(userId) {
|
||||||
if err != nil {
|
srv.MayInitializeUser(userId)
|
||||||
return nil, err
|
}
|
||||||
|
if aliases, ok := userAliases.Load(userId); ok {
|
||||||
|
filteredAliases := make([]*models.Alias, 0, len(aliases.([]*models.Alias)))
|
||||||
|
for _, a := range aliases.([]*models.Alias) {
|
||||||
|
if a.Key == key && a.Type == summaryType {
|
||||||
|
filteredAliases = append(filteredAliases, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredAliases, nil
|
||||||
|
} else {
|
||||||
|
return nil, errors.New(fmt.Sprintf("no user aliases loaded for user %s", userId))
|
||||||
}
|
}
|
||||||
return aliases, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *AliasService) GetAliasOrDefault(userId string, summaryType uint8, value string) (string, error) {
|
func (srv *AliasService) GetAliasOrDefault(userId string, summaryType uint8, value string) (string, error) {
|
||||||
if !srv.IsInitialized(userId) {
|
if !srv.IsInitialized(userId) {
|
||||||
if err := srv.InitializeUser(userId); err != nil {
|
srv.MayInitializeUser(userId)
|
||||||
return "", err
|
}
|
||||||
|
|
||||||
|
if aliases, ok := userAliases.Load(userId); ok {
|
||||||
|
for _, a := range aliases.([]*models.Alias) {
|
||||||
|
if a.Type == summaryType && a.Value == value {
|
||||||
|
return a.Key, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
aliases, _ := userAliases.Load(userId)
|
|
||||||
for _, a := range aliases.([]*models.Alias) {
|
|
||||||
if a.Type == summaryType && a.Value == value {
|
|
||||||
return a.Key, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +94,7 @@ func (srv *AliasService) Create(alias *models.Alias) (*models.Alias, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
go srv.reinitUser(alias.UserID)
|
go srv.MayInitializeUser(alias.UserID)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +103,7 @@ func (srv *AliasService) Delete(alias *models.Alias) error {
|
|||||||
return errors.New("no user id specified")
|
return errors.New("no user id specified")
|
||||||
}
|
}
|
||||||
err := srv.repository.Delete(alias.ID)
|
err := srv.repository.Delete(alias.ID)
|
||||||
go srv.reinitUser(alias.UserID)
|
go srv.MayInitializeUser(alias.UserID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,14 +121,8 @@ func (srv *AliasService) DeleteMulti(aliases []*models.Alias) error {
|
|||||||
err := srv.repository.DeleteBatch(ids)
|
err := srv.repository.DeleteBatch(ids)
|
||||||
|
|
||||||
for k := range affectedUsers {
|
for k := range affectedUsers {
|
||||||
go srv.reinitUser(k)
|
go srv.MayInitializeUser(k)
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *AliasService) reinitUser(userId string) {
|
|
||||||
if err := srv.InitializeUser(userId); err != nil {
|
|
||||||
logbuch.Error("error initializing user aliases – %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -52,12 +52,3 @@ func (suite *AliasServiceTestSuite) TestAliasService_GetAliasOrDefault() {
|
|||||||
assert.Equal(suite.T(), "anchr", result3)
|
assert.Equal(suite.T(), "anchr", result3)
|
||||||
assert.Nil(suite.T(), err3)
|
assert.Nil(suite.T(), err3)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *AliasServiceTestSuite) TestAliasService_GetAliasOrDefault_ErrorOnNonExistingUser() {
|
|
||||||
sut := NewAliasService(suite.AliasRepository)
|
|
||||||
|
|
||||||
result, err := sut.GetAliasOrDefault("nonexisting", models.SummaryProject, "wakapi-mobile")
|
|
||||||
|
|
||||||
assert.Empty(suite.T(), result)
|
|
||||||
assert.Error(suite.T(), err)
|
|
||||||
}
|
|
||||||
|
@ -21,7 +21,7 @@ func NewDurationService(heartbeatService IHeartbeatService) *DurationService {
|
|||||||
return srv
|
return srv
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *DurationService) Get(from, to time.Time, user *models.User) (models.Durations, error) {
|
func (srv *DurationService) Get(from, to time.Time, user *models.User, filters *models.Filters) (models.Durations, error) {
|
||||||
heartbeats, err := srv.heartbeatService.GetAllWithin(from, to, user)
|
heartbeats, err := srv.heartbeatService.GetAllWithin(from, to, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -34,6 +34,10 @@ func (srv *DurationService) Get(from, to time.Time, user *models.User) (models.D
|
|||||||
mapping := make(map[string][]*models.Duration)
|
mapping := make(map[string][]*models.Duration)
|
||||||
|
|
||||||
for _, h := range heartbeats {
|
for _, h := range heartbeats {
|
||||||
|
if filters != nil && !filters.Match(h) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
d1 := models.NewDurationFromHeartbeat(h)
|
d1 := models.NewDurationFromHeartbeat(h)
|
||||||
|
|
||||||
if list, ok := mapping[d1.GroupHash]; !ok || len(list) < 1 {
|
if list, ok := mapping[d1.GroupHash]; !ok || len(list) < 1 {
|
||||||
|
@ -126,7 +126,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get() {
|
|||||||
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(-1*time.Minute)
|
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(-1*time.Minute)
|
||||||
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(filterHeartbeats(from, to, suite.TestHeartbeats), nil)
|
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(filterHeartbeats(from, to, suite.TestHeartbeats), nil)
|
||||||
|
|
||||||
durations, err = sut.Get(from, to, suite.TestUser)
|
durations, err = sut.Get(from, to, suite.TestUser, nil)
|
||||||
|
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
assert.Empty(suite.T(), durations)
|
assert.Empty(suite.T(), durations)
|
||||||
@ -135,7 +135,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get() {
|
|||||||
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(1*time.Second)
|
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(1*time.Second)
|
||||||
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(filterHeartbeats(from, to, suite.TestHeartbeats), nil)
|
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(filterHeartbeats(from, to, suite.TestHeartbeats), nil)
|
||||||
|
|
||||||
durations, err = sut.Get(from, to, suite.TestUser)
|
durations, err = sut.Get(from, to, suite.TestUser, nil)
|
||||||
|
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
assert.Len(suite.T(), durations, 1)
|
assert.Len(suite.T(), durations, 1)
|
||||||
@ -146,7 +146,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get() {
|
|||||||
from, to = suite.TestStartTime, suite.TestStartTime.Add(1*time.Hour)
|
from, to = suite.TestStartTime, suite.TestStartTime.Add(1*time.Hour)
|
||||||
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(filterHeartbeats(from, to, suite.TestHeartbeats), nil)
|
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(filterHeartbeats(from, to, suite.TestHeartbeats), nil)
|
||||||
|
|
||||||
durations, err = sut.Get(from, to, suite.TestUser)
|
durations, err = sut.Get(from, to, suite.TestUser, nil)
|
||||||
|
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
assert.Len(suite.T(), durations, 3)
|
assert.Len(suite.T(), durations, 3)
|
||||||
|
@ -97,7 +97,7 @@ func (srv *MiscService) runCountTotalTime() error {
|
|||||||
|
|
||||||
func (srv *MiscService) countTotalTimeWorker(jobs <-chan *CountTotalTimeJob, results chan<- *CountTotalTimeResult) {
|
func (srv *MiscService) countTotalTimeWorker(jobs <-chan *CountTotalTimeJob, results chan<- *CountTotalTimeResult) {
|
||||||
for job := range jobs {
|
for job := range jobs {
|
||||||
if result, err := srv.summaryService.Aliased(time.Time{}, time.Now(), &models.User{ID: job.UserID}, srv.summaryService.Retrieve, false); err != nil {
|
if result, err := srv.summaryService.Aliased(time.Time{}, time.Now(), &models.User{ID: job.UserID}, srv.summaryService.Retrieve, nil, false); err != nil {
|
||||||
config.Log().Error("failed to count total for user %s: %v", job.UserID, err)
|
config.Log().Error("failed to count total for user %s: %v", job.UserID, err)
|
||||||
} else {
|
} else {
|
||||||
results <- &CountTotalTimeResult{
|
results <- &CountTotalTimeResult{
|
||||||
|
@ -112,7 +112,7 @@ func (srv *ReportService) Run(user *models.User, duration time.Duration) error {
|
|||||||
end := time.Now().In(user.TZ())
|
end := time.Now().In(user.TZ())
|
||||||
start := time.Now().Add(-1 * duration)
|
start := time.Now().Add(-1 * duration)
|
||||||
|
|
||||||
summary, err := srv.summaryService.Aliased(start, end, user, srv.summaryService.Retrieve, false)
|
summary, err := srv.summaryService.Aliased(start, end, user, srv.summaryService.Retrieve, nil, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
config.Log().Error("failed to generate report for '%s' – %v", user.ID, err)
|
config.Log().Error("failed to generate report for '%s' – %v", user.ID, err)
|
||||||
return err
|
return err
|
||||||
|
@ -75,13 +75,13 @@ type IMailService interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IDurationService interface {
|
type IDurationService interface {
|
||||||
Get(time.Time, time.Time, *models.User) (models.Durations, error)
|
Get(time.Time, time.Time, *models.User, *models.Filters) (models.Durations, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ISummaryService interface {
|
type ISummaryService interface {
|
||||||
Aliased(time.Time, time.Time, *models.User, SummaryRetriever, bool) (*models.Summary, error)
|
Aliased(time.Time, time.Time, *models.User, SummaryRetriever, *models.Filters, bool) (*models.Summary, error)
|
||||||
Retrieve(time.Time, time.Time, *models.User) (*models.Summary, error)
|
Retrieve(time.Time, time.Time, *models.User, *models.Filters) (*models.Summary, error)
|
||||||
Summarize(time.Time, time.Time, *models.User) (*models.Summary, error)
|
Summarize(time.Time, time.Time, *models.User, *models.Filters) (*models.Summary, error)
|
||||||
GetLatestByUser() ([]*models.TimeByUser, error)
|
GetLatestByUser() ([]*models.TimeByUser, error)
|
||||||
DeleteByUser(string) error
|
DeleteByUser(string) error
|
||||||
Insert(*models.Summary) error
|
Insert(*models.Summary) error
|
||||||
|
@ -24,7 +24,7 @@ type SummaryService struct {
|
|||||||
projectLabelService IProjectLabelService
|
projectLabelService IProjectLabelService
|
||||||
}
|
}
|
||||||
|
|
||||||
type SummaryRetriever func(f, t time.Time, u *models.User) (*models.Summary, error)
|
type SummaryRetriever func(f, t time.Time, u *models.User, filters *models.Filters) (*models.Summary, error)
|
||||||
|
|
||||||
func NewSummaryService(summaryRepo repositories.ISummaryRepository, durationService IDurationService, aliasService IAliasService, projectLabelService IProjectLabelService) *SummaryService {
|
func NewSummaryService(summaryRepo repositories.ISummaryRepository, durationService IDurationService, aliasService IAliasService, projectLabelService IProjectLabelService) *SummaryService {
|
||||||
srv := &SummaryService{
|
srv := &SummaryService{
|
||||||
@ -55,10 +55,10 @@ func NewSummaryService(summaryRepo repositories.ISummaryRepository, durationServ
|
|||||||
// Public summary generation methods
|
// Public summary generation methods
|
||||||
|
|
||||||
// Aliased retrieves or computes a new summary based on the given SummaryRetriever and augments it with entity aliases and project labels
|
// Aliased retrieves or computes a new summary based on the given SummaryRetriever and augments it with entity aliases and project labels
|
||||||
func (srv *SummaryService) Aliased(from, to time.Time, user *models.User, f SummaryRetriever, skipCache bool) (*models.Summary, error) {
|
func (srv *SummaryService) Aliased(from, to time.Time, user *models.User, f SummaryRetriever, filters *models.Filters, skipCache bool) (*models.Summary, error) {
|
||||||
// Check cache
|
// Check cache
|
||||||
cacheKey := srv.getHash(from.String(), to.String(), user.ID, "--aliased")
|
cacheKey := srv.getHash(from.String(), to.String(), user.ID, filters.Hash(), "--aliased")
|
||||||
if cacheResult, ok := srv.cache.Get(cacheKey); ok && !skipCache && false {
|
if cacheResult, ok := srv.cache.Get(cacheKey); ok && !skipCache {
|
||||||
return cacheResult.(*models.Summary), nil
|
return cacheResult.(*models.Summary), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +67,19 @@ func (srv *SummaryService) Aliased(from, to time.Time, user *models.User, f Summ
|
|||||||
s, _ := srv.aliasService.GetAliasOrDefault(user.ID, t, k)
|
s, _ := srv.aliasService.GetAliasOrDefault(user.ID, t, k)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
resolveReverse := func(t uint8, k string) []string {
|
||||||
|
aliases, _ := srv.aliasService.GetByUserAndKeyAndType(user.ID, k, t)
|
||||||
|
aliasStrings := make([]string, 0, len(aliases))
|
||||||
|
for _, a := range aliases {
|
||||||
|
aliasStrings = append(aliasStrings, a.Value)
|
||||||
|
}
|
||||||
|
return aliasStrings
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-process filters
|
||||||
|
if filters != nil {
|
||||||
|
filters = filters.WithAliases(resolveReverse)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize alias resolver service
|
// Initialize alias resolver service
|
||||||
if err := srv.aliasService.InitializeUser(user.ID); err != nil {
|
if err := srv.aliasService.InitializeUser(user.ID); err != nil {
|
||||||
@ -74,7 +87,7 @@ func (srv *SummaryService) Aliased(from, to time.Time, user *models.User, f Summ
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get actual summary
|
// Get actual summary
|
||||||
s, err := f(from, to, user)
|
s, err := f(from, to, user, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -89,17 +102,24 @@ func (srv *SummaryService) Aliased(from, to time.Time, user *models.User, f Summ
|
|||||||
return summary.Sorted(), nil
|
return summary.Sorted(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User) (*models.Summary, error) {
|
func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User, filters *models.Filters) (*models.Summary, error) {
|
||||||
// Get all already existing, pre-generated summaries that fall into the requested interval
|
summaries := make([]*models.Summary, 0)
|
||||||
summaries, err := srv.repository.GetByUserWithin(user, from, to)
|
|
||||||
if err != nil {
|
// Filtered summaries are not persisted currently
|
||||||
return nil, err
|
if filters == nil || filters.IsEmpty() {
|
||||||
|
// Get all already existing, pre-generated summaries that fall into the requested interval
|
||||||
|
result, err := srv.repository.GetByUserWithin(user, from, to)
|
||||||
|
if err == nil {
|
||||||
|
summaries = result
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate missing slots (especially before and after existing summaries) from durations (formerly raw heartbeats)
|
// Generate missing slots (especially before and after existing summaries) from durations (formerly raw heartbeats)
|
||||||
missingIntervals := srv.getMissingIntervals(from, to, summaries)
|
missingIntervals := srv.getMissingIntervals(from, to, summaries)
|
||||||
for _, interval := range missingIntervals {
|
for _, interval := range missingIntervals {
|
||||||
if s, err := srv.Summarize(interval.Start, interval.End, user); err == nil {
|
if s, err := srv.Summarize(interval.Start, interval.End, user, filters); err == nil {
|
||||||
summaries = append(summaries, s)
|
summaries = append(summaries, s)
|
||||||
} else {
|
} else {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -115,9 +135,9 @@ func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User) (*mod
|
|||||||
return summary.Sorted(), nil
|
return summary.Sorted(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *SummaryService) Summarize(from, to time.Time, user *models.User) (*models.Summary, error) {
|
func (srv *SummaryService) Summarize(from, to time.Time, user *models.User, filters *models.Filters) (*models.Summary, error) {
|
||||||
// Initialize and fetch data
|
// Initialize and fetch data
|
||||||
durations, err := srv.durationService.Get(from, to, user)
|
durations, err := srv.durationService.Get(from, to, user, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -108,9 +108,9 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Summarize() {
|
|||||||
|
|
||||||
/* TEST 1 */
|
/* TEST 1 */
|
||||||
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(-1*time.Minute)
|
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(-1*time.Minute)
|
||||||
suite.DurationService.On("Get", from, to, suite.TestUser).Return(filterDurations(from, to, suite.TestDurations), nil)
|
suite.DurationService.On("Get", from, to, suite.TestUser, mock.Anything).Return(filterDurations(from, to, suite.TestDurations), nil)
|
||||||
|
|
||||||
result, err = sut.Summarize(from, to, suite.TestUser)
|
result, err = sut.Summarize(from, to, suite.TestUser, nil)
|
||||||
|
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
assert.NotNil(suite.T(), result)
|
assert.NotNil(suite.T(), result)
|
||||||
@ -122,9 +122,9 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Summarize() {
|
|||||||
|
|
||||||
/* TEST 2 */
|
/* TEST 2 */
|
||||||
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(1*time.Second)
|
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(1*time.Second)
|
||||||
suite.DurationService.On("Get", from, to, suite.TestUser).Return(filterDurations(from, to, suite.TestDurations), nil)
|
suite.DurationService.On("Get", from, to, suite.TestUser, mock.Anything).Return(filterDurations(from, to, suite.TestDurations), nil)
|
||||||
|
|
||||||
result, err = sut.Summarize(from, to, suite.TestUser)
|
result, err = sut.Summarize(from, to, suite.TestUser, nil)
|
||||||
|
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
assert.NotNil(suite.T(), result)
|
assert.NotNil(suite.T(), result)
|
||||||
@ -136,9 +136,9 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Summarize() {
|
|||||||
|
|
||||||
/* TEST 3 */
|
/* TEST 3 */
|
||||||
from, to = suite.TestStartTime, suite.TestStartTime.Add(1*time.Hour)
|
from, to = suite.TestStartTime, suite.TestStartTime.Add(1*time.Hour)
|
||||||
suite.DurationService.On("Get", from, to, suite.TestUser).Return(filterDurations(from, to, suite.TestDurations), nil)
|
suite.DurationService.On("Get", from, to, suite.TestUser, mock.Anything).Return(filterDurations(from, to, suite.TestDurations), nil)
|
||||||
|
|
||||||
result, err = sut.Summarize(from, to, suite.TestUser)
|
result, err = sut.Summarize(from, to, suite.TestUser, nil)
|
||||||
|
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
assert.NotNil(suite.T(), result)
|
assert.NotNil(suite.T(), result)
|
||||||
@ -187,10 +187,10 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return(summaries, nil)
|
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return(summaries, nil)
|
||||||
suite.DurationService.On("Get", from, summaries[0].FromTime.T(), suite.TestUser).Return(models.Durations{}, nil)
|
suite.DurationService.On("Get", from, summaries[0].FromTime.T(), suite.TestUser, mock.Anything).Return(models.Durations{}, nil)
|
||||||
suite.DurationService.On("Get", summaries[0].ToTime.T(), to, suite.TestUser).Return(models.Durations{}, nil)
|
suite.DurationService.On("Get", summaries[0].ToTime.T(), to, suite.TestUser, mock.Anything).Return(models.Durations{}, nil)
|
||||||
|
|
||||||
result, err = sut.Retrieve(from, to, suite.TestUser)
|
result, err = sut.Retrieve(from, to, suite.TestUser, nil)
|
||||||
|
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
assert.NotNil(suite.T(), result)
|
assert.NotNil(suite.T(), result)
|
||||||
@ -241,9 +241,9 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return(summaries, nil)
|
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return(summaries, nil)
|
||||||
suite.DurationService.On("Get", from, summaries[0].FromTime.T(), suite.TestUser).Return(filterDurations(from, summaries[0].FromTime.T(), suite.TestDurations), nil)
|
suite.DurationService.On("Get", from, summaries[0].FromTime.T(), suite.TestUser, mock.Anything).Return(filterDurations(from, summaries[0].FromTime.T(), suite.TestDurations), nil)
|
||||||
|
|
||||||
result, err = sut.Retrieve(from, to, suite.TestUser)
|
result, err = sut.Retrieve(from, to, suite.TestUser, nil)
|
||||||
|
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
assert.NotNil(suite.T(), result)
|
assert.NotNil(suite.T(), result)
|
||||||
@ -297,9 +297,9 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return(summaries, nil)
|
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return(summaries, nil)
|
||||||
suite.DurationService.On("Get", summaries[0].ToTime.T(), summaries[1].FromTime.T(), suite.TestUser).Return(filterDurations(summaries[0].ToTime.T(), summaries[1].FromTime.T(), suite.TestDurations), nil)
|
suite.DurationService.On("Get", summaries[0].ToTime.T(), summaries[1].FromTime.T(), suite.TestUser, mock.Anything).Return(filterDurations(summaries[0].ToTime.T(), summaries[1].FromTime.T(), suite.TestDurations), nil)
|
||||||
|
|
||||||
result, err = sut.Retrieve(from, to, suite.TestUser)
|
result, err = sut.Retrieve(from, to, suite.TestUser, nil)
|
||||||
|
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
assert.NotNil(suite.T(), result)
|
assert.NotNil(suite.T(), result)
|
||||||
@ -347,10 +347,10 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve_DuplicateSumma
|
|||||||
summaries = append(summaries, &(*summaries[0])) // add same summary again -> mustn't be counted twice!
|
summaries = append(summaries, &(*summaries[0])) // add same summary again -> mustn't be counted twice!
|
||||||
|
|
||||||
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return(summaries, nil)
|
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return(summaries, nil)
|
||||||
suite.DurationService.On("Get", from, summaries[0].FromTime.T(), suite.TestUser).Return(models.Durations{}, nil)
|
suite.DurationService.On("Get", from, summaries[0].FromTime.T(), suite.TestUser, mock.Anything).Return(models.Durations{}, nil)
|
||||||
suite.DurationService.On("Get", summaries[0].ToTime.T(), to, suite.TestUser).Return(models.Durations{}, nil)
|
suite.DurationService.On("Get", summaries[0].ToTime.T(), to, suite.TestUser, mock.Anything).Return(models.Durations{}, nil)
|
||||||
|
|
||||||
result, err = sut.Retrieve(from, to, suite.TestUser)
|
result, err = sut.Retrieve(from, to, suite.TestUser, nil)
|
||||||
|
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
assert.NotNil(suite.T(), result)
|
assert.NotNil(suite.T(), result)
|
||||||
@ -362,6 +362,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve_DuplicateSumma
|
|||||||
func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased() {
|
func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased() {
|
||||||
sut := NewSummaryService(suite.SummaryRepository, suite.DurationService, suite.AliasService, suite.ProjectLabelService)
|
sut := NewSummaryService(suite.SummaryRepository, suite.DurationService, suite.AliasService, suite.ProjectLabelService)
|
||||||
|
|
||||||
|
suite.AliasService.On("InitializeUser", suite.TestUser.ID).Return(nil)
|
||||||
suite.ProjectLabelService.On("GetByUser", suite.TestUser.ID).Return([]*models.ProjectLabel{}, nil)
|
suite.ProjectLabelService.On("GetByUser", suite.TestUser.ID).Return([]*models.ProjectLabel{}, nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -385,14 +386,14 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased() {
|
|||||||
Duration: 0, // not relevant here
|
Duration: 0, // not relevant here
|
||||||
})
|
})
|
||||||
|
|
||||||
suite.DurationService.On("Get", from, to, suite.TestUser).Return(models.Durations(durations), nil)
|
suite.DurationService.On("Get", from, to, suite.TestUser, mock.Anything).Return(models.Durations(durations), nil)
|
||||||
suite.AliasService.On("InitializeUser", TestUserId).Return(nil)
|
suite.AliasService.On("InitializeUser", TestUserId).Return(nil)
|
||||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject1).Return(TestProject2, nil)
|
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject1).Return(TestProject2, nil)
|
||||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject2).Return(TestProject2, nil)
|
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject2).Return(TestProject2, nil)
|
||||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, mock.Anything).Return("", nil)
|
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, mock.Anything).Return("", nil)
|
||||||
suite.ProjectLabelService.On("GetByUser", suite.TestUser.ID).Return(suite.TestLabels, nil).Once()
|
suite.ProjectLabelService.On("GetByUser", suite.TestUser.ID).Return(suite.TestLabels, nil).Once()
|
||||||
|
|
||||||
result, err = sut.Aliased(from, to, suite.TestUser, sut.Summarize, false)
|
result, err = sut.Aliased(from, to, suite.TestUser, sut.Summarize, nil, false)
|
||||||
|
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
assert.NotNil(suite.T(), result)
|
assert.NotNil(suite.T(), result)
|
||||||
@ -426,13 +427,13 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased_ProjectLabels()
|
|||||||
})
|
})
|
||||||
|
|
||||||
suite.ProjectLabelService.On("GetByUser", suite.TestUser.ID).Return(suite.TestLabels, nil).Once()
|
suite.ProjectLabelService.On("GetByUser", suite.TestUser.ID).Return(suite.TestLabels, nil).Once()
|
||||||
suite.DurationService.On("Get", from, to, suite.TestUser).Return(models.Durations(durations), nil)
|
suite.DurationService.On("Get", from, to, suite.TestUser, mock.Anything).Return(models.Durations(durations), nil)
|
||||||
suite.AliasService.On("InitializeUser", TestUserId).Return(nil)
|
suite.AliasService.On("InitializeUser", TestUserId).Return(nil)
|
||||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject1).Return(TestProject1, nil)
|
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject1).Return(TestProject1, nil)
|
||||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject2).Return(TestProject1, nil)
|
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject2).Return(TestProject1, nil)
|
||||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, mock.Anything).Return("", nil)
|
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, mock.Anything).Return("", nil)
|
||||||
|
|
||||||
result, err = sut.Aliased(from, to, suite.TestUser, sut.Summarize, false)
|
result, err = sut.Aliased(from, to, suite.TestUser, sut.Summarize, nil, false)
|
||||||
|
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
assert.NotNil(suite.T(), result)
|
assert.NotNil(suite.T(), result)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user