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

@@ -83,7 +83,7 @@ func (srv *AggregationService) Run(userIds map[string]bool) error {
func (srv *AggregationService) summaryWorker(jobs <-chan *AggregationJob, summaries chan<- *models.Summary) {
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)
} else {
logbuch.Info("successfully generated summary (%v, %v, %s)", job.From, job.To, job.UserID)

View File

@@ -2,6 +2,7 @@ package services
import (
"errors"
"fmt"
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
@@ -38,35 +39,53 @@ func (srv *AliasService) InitializeUser(userId string) error {
return err
}
func (srv *AliasService) GetByUser(userId string) ([]*models.Alias, error) {
aliases, err := srv.repository.GetByUser(userId)
if err != nil {
return nil, err
func (srv *AliasService) MayInitializeUser(userId string) {
if err := srv.InitializeUser(userId); err != nil {
logbuch.Error("failed to initialize user alias map for user %s", userId)
}
}
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) {
aliases, err := srv.repository.GetByUserAndKeyAndType(userId, key, summaryType)
if err != nil {
return nil, err
if !srv.IsInitialized(userId) {
srv.MayInitializeUser(userId)
}
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) {
if !srv.IsInitialized(userId) {
if err := srv.InitializeUser(userId); err != nil {
return "", err
srv.MayInitializeUser(userId)
}
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
}
@@ -75,7 +94,7 @@ func (srv *AliasService) Create(alias *models.Alias) (*models.Alias, error) {
if err != nil {
return nil, err
}
go srv.reinitUser(alias.UserID)
go srv.MayInitializeUser(alias.UserID)
return result, nil
}
@@ -84,7 +103,7 @@ func (srv *AliasService) Delete(alias *models.Alias) error {
return errors.New("no user id specified")
}
err := srv.repository.Delete(alias.ID)
go srv.reinitUser(alias.UserID)
go srv.MayInitializeUser(alias.UserID)
return err
}
@@ -102,14 +121,8 @@ func (srv *AliasService) DeleteMulti(aliases []*models.Alias) error {
err := srv.repository.DeleteBatch(ids)
for k := range affectedUsers {
go srv.reinitUser(k)
go srv.MayInitializeUser(k)
}
return err
}
func (srv *AliasService) reinitUser(userId string) {
if err := srv.InitializeUser(userId); err != nil {
logbuch.Error("error initializing user aliases %v", err)
}
}

View File

@@ -52,12 +52,3 @@ func (suite *AliasServiceTestSuite) TestAliasService_GetAliasOrDefault() {
assert.Equal(suite.T(), "anchr", result3)
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)
}

View File

@@ -21,7 +21,7 @@ func NewDurationService(heartbeatService IHeartbeatService) *DurationService {
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)
if err != nil {
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)
for _, h := range heartbeats {
if filters != nil && !filters.Match(h) {
continue
}
d1 := models.NewDurationFromHeartbeat(h)
if list, ok := mapping[d1.GroupHash]; !ok || len(list) < 1 {

View File

@@ -126,7 +126,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get() {
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)
durations, err = sut.Get(from, to, suite.TestUser)
durations, err = sut.Get(from, to, suite.TestUser, nil)
assert.Nil(suite.T(), err)
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)
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.Len(suite.T(), durations, 1)
@@ -146,7 +146,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get() {
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)
durations, err = sut.Get(from, to, suite.TestUser)
durations, err = sut.Get(from, to, suite.TestUser, nil)
assert.Nil(suite.T(), err)
assert.Len(suite.T(), durations, 3)

View File

@@ -97,7 +97,7 @@ func (srv *MiscService) runCountTotalTime() error {
func (srv *MiscService) countTotalTimeWorker(jobs <-chan *CountTotalTimeJob, results chan<- *CountTotalTimeResult) {
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)
} else {
results <- &CountTotalTimeResult{

View File

@@ -112,7 +112,7 @@ func (srv *ReportService) Run(user *models.User, duration time.Duration) error {
end := time.Now().In(user.TZ())
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 {
config.Log().Error("failed to generate report for '%s' %v", user.ID, err)
return err

View File

@@ -75,13 +75,13 @@ type IMailService 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 {
Aliased(time.Time, time.Time, *models.User, SummaryRetriever, bool) (*models.Summary, error)
Retrieve(time.Time, time.Time, *models.User) (*models.Summary, error)
Summarize(time.Time, time.Time, *models.User) (*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.Filters) (*models.Summary, error)
Summarize(time.Time, time.Time, *models.User, *models.Filters) (*models.Summary, error)
GetLatestByUser() ([]*models.TimeByUser, error)
DeleteByUser(string) error
Insert(*models.Summary) error

View File

@@ -24,7 +24,7 @@ type SummaryService struct {
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 {
srv := &SummaryService{
@@ -55,10 +55,10 @@ func NewSummaryService(summaryRepo repositories.ISummaryRepository, durationServ
// 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
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
cacheKey := srv.getHash(from.String(), to.String(), user.ID, "--aliased")
if cacheResult, ok := srv.cache.Get(cacheKey); ok && !skipCache && false {
cacheKey := srv.getHash(from.String(), to.String(), user.ID, filters.Hash(), "--aliased")
if cacheResult, ok := srv.cache.Get(cacheKey); ok && !skipCache {
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)
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
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
s, err := f(from, to, user)
s, err := f(from, to, user, filters)
if err != nil {
return nil, err
}
@@ -89,17 +102,24 @@ func (srv *SummaryService) Aliased(from, to time.Time, user *models.User, f Summ
return summary.Sorted(), nil
}
func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User) (*models.Summary, error) {
// Get all already existing, pre-generated summaries that fall into the requested interval
summaries, err := srv.repository.GetByUserWithin(user, from, to)
if err != nil {
return nil, err
func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User, filters *models.Filters) (*models.Summary, error) {
summaries := make([]*models.Summary, 0)
// Filtered summaries are not persisted currently
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)
missingIntervals := srv.getMissingIntervals(from, to, summaries)
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)
} else {
return nil, err
@@ -115,9 +135,9 @@ func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User) (*mod
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
durations, err := srv.durationService.Get(from, to, user)
durations, err := srv.durationService.Get(from, to, user, filters)
if err != nil {
return nil, err
}

View File

@@ -108,9 +108,9 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Summarize() {
/* TEST 1 */
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.NotNil(suite.T(), result)
@@ -122,9 +122,9 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Summarize() {
/* TEST 2 */
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.NotNil(suite.T(), result)
@@ -136,9 +136,9 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Summarize() {
/* TEST 3 */
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.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.DurationService.On("Get", from, summaries[0].FromTime.T(), suite.TestUser).Return(models.Durations{}, nil)
suite.DurationService.On("Get", summaries[0].ToTime.T(), to, 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, 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.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.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.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.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.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!
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", summaries[0].ToTime.T(), to, 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, 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.NotNil(suite.T(), result)
@@ -362,6 +362,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve_DuplicateSumma
func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased() {
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)
var (
@@ -385,14 +386,14 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased() {
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("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, mock.Anything).Return("", nil)
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.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.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("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, 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.NotNil(suite.T(), result)