fix: summary from date display (resolve #843)

This commit is contained in:
Ferdinand Mütsch
2025-09-16 00:02:33 +02:00
parent b12f76ec0f
commit 849d7a8996
5 changed files with 1935 additions and 1790 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/becheran/wildmatch-go"
"github.com/duke-git/lancet/v2/condition"
"github.com/duke-git/lancet/v2/datetime"
"github.com/duke-git/lancet/v2/slice"
"github.com/leandro-lugaresi/hub"
@@ -62,7 +63,7 @@ func (srv *SummaryService) Aliased(from, to time.Time, user *models.User, f type
cacheKey := srv.getHash(from.String(), to.String(), user.ID, filters.Hash(), strconv.Itoa(int(requestedTimeout)), "--aliased")
if to.Truncate(time.Second).Equal(to) && from.Truncate(time.Second).Equal(from) {
if cacheResult, ok := srv.cache.Get(cacheKey); ok && !skipCache {
return cacheResult.(*models.Summary), nil
return cacheResult.(*models.Summary).Sorted().InTZ(user.TZ()), nil
}
}
@@ -117,7 +118,7 @@ func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User, filte
// 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
summaries = srv.fixZeroDuration(result)
} else {
return nil, err
}
@@ -146,6 +147,9 @@ func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User, filte
return nil, err
}
// prevent 0001-01-01T00:00:00 caused by empty "pre" missing interval, see https://github.com/muety/wakapi/issues/843
summary.FromTime = models.CustomTime(condition.Ternary(summary.FromTime.T().Before(from), from, summary.FromTime.T()))
if filters != nil && filters.CountDistinctTypes() == 1 && filters.SelectFilteredOnly {
filter := filters.OneOrEmpty()
summary.KeepOnly(map[uint8]bool{filter.Entity: true}).ApplyFilter(filter)
@@ -470,6 +474,27 @@ func (srv *SummaryService) getMissingIntervals(from, to time.Time, summaries []*
return intervals
}
// Since summary timestamps are only second-level precision, we rarely observe examples where from- and to-time are allegedly equal.
// We artificially modify those to give them a one second duration and potentially fix the subsequent summary as well to prevent overlaps.
// Assumes summaries slice to be sorted by from time.
func (s *SummaryService) fixZeroDuration(summaries []*models.Summary) []*models.Summary {
for i, summary := range summaries {
if summary.FromTime.T().Equal(summary.ToTime.T()) {
summary.ToTime = models.CustomTime(summary.ToTime.T().Add(1 * time.Second))
if i < len(summaries)-1 {
summaryNext := summaries[i+1]
if summaryNext.FromTime.T().Before(summary.ToTime.T()) {
// intentionally not trying to resolve larger overlaps that were there before (even though they shouldn't happen in theory)
summaryNext.FromTime = models.CustomTime(summaryNext.FromTime.T().Add(1 * time.Second))
}
}
}
}
return summaries
}
func (srv *SummaryService) getHash(args ...string) string {
return strings.Join(args, "__")
}

View File

@@ -381,6 +381,118 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve_DuplicateSumma
suite.DurationService.AssertNumberOfCalls(suite.T(), "Get", 2)
}
func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve_ZeroLengthSummaries() {
sut := NewSummaryService(suite.SummaryRepository, suite.HeartbeatService, suite.DurationService, suite.AliasService, suite.ProjectLabelService)
suite.ProjectLabelService.On("GetByUser", suite.TestUser.ID).Return([]*models.ProjectLabel{}, nil)
var (
summaries []*models.Summary
from time.Time
to time.Time
result *models.Summary
err error
)
from, to = suite.TestStartTime.Add(-12*time.Hour), suite.TestStartTime.Add(12*time.Hour)
summaries = []*models.Summary{
{
ID: uint(rand.Uint32()),
UserID: TestUserId,
FromTime: models.CustomTime(from.Add(10 * time.Minute)),
ToTime: models.CustomTime(from.Add(10 * time.Minute)),
Projects: []*models.SummaryItem{},
Languages: []*models.SummaryItem{},
Editors: []*models.SummaryItem{},
OperatingSystems: []*models.SummaryItem{},
Machines: []*models.SummaryItem{},
},
}
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return(summaries, nil)
suite.DurationService.On("Get", from, summaries[0].FromTime.T(), suite.TestUser, mock.Anything, mock.Anything, false).Return(models.Durations{}, nil)
suite.DurationService.On("Get", summaries[0].ToTime.T().Add(1*time.Second), to, suite.TestUser, mock.Anything, mock.Anything, false).Return(models.Durations{}, nil)
result, err = sut.Retrieve(from, to, suite.TestUser, nil, nil)
assert.Nil(suite.T(), err)
assert.NotNil(suite.T(), result)
suite.DurationService.AssertNumberOfCalls(suite.T(), "Get", 2)
}
func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve_DateRange() {
sut := NewSummaryService(suite.SummaryRepository, suite.HeartbeatService, suite.DurationService, suite.AliasService, suite.ProjectLabelService)
suite.ProjectLabelService.On("GetByUser", suite.TestUser.ID).Return([]*models.ProjectLabel{}, nil)
var (
summaries []*models.Summary
from time.Time
to time.Time
result *models.Summary
err error
)
from, to = suite.TestStartTime.Add(-12*time.Hour), suite.TestStartTime.Add(12*time.Hour)
summaries = []*models.Summary{
{
ID: uint(rand.Uint32()),
UserID: TestUserId,
FromTime: models.CustomTime(from.Add(10 * time.Minute)),
ToTime: models.CustomTime(to.Add(-10 * time.Minute)),
Projects: []*models.SummaryItem{
{
Type: models.SummaryProject,
Key: TestProject1,
Total: 45 * time.Minute / time.Second, // hack
},
},
Languages: []*models.SummaryItem{},
Editors: []*models.SummaryItem{},
OperatingSystems: []*models.SummaryItem{},
Machines: []*models.SummaryItem{},
},
}
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return(summaries, nil)
suite.DurationService.On("Get", from, summaries[0].FromTime.T(), suite.TestUser, mock.Anything, mock.Anything, false).Return(models.Durations{}, nil)
suite.DurationService.On("Get", summaries[0].ToTime.T(), to, suite.TestUser, mock.Anything, mock.Anything, false).Return(models.Durations{}, nil)
result, err = sut.Retrieve(from, to, suite.TestUser, nil, nil)
assert.Nil(suite.T(), err)
assert.NotNil(suite.T(), result)
assert.Equal(suite.T(), from, result.FromTime.T()) // requested from date
assert.Equal(suite.T(), to, result.ToTime.T()) // requested to date
suite.DurationService.AssertNumberOfCalls(suite.T(), "Get", 2)
}
func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve_DateRange_NoData() {
sut := NewSummaryService(suite.SummaryRepository, suite.HeartbeatService, suite.DurationService, suite.AliasService, suite.ProjectLabelService)
suite.ProjectLabelService.On("GetByUser", suite.TestUser.ID).Return([]*models.ProjectLabel{}, nil)
var (
from time.Time
to time.Time
result *models.Summary
err error
)
from, to = suite.TestStartTime.Add(-12*time.Hour), suite.TestStartTime.Add(12*time.Hour)
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return([]*models.Summary{}, nil)
suite.DurationService.On("Get", from, to, suite.TestUser, mock.Anything, mock.Anything, false).Return(models.Durations{}, nil)
result, err = sut.Retrieve(from, to, suite.TestUser, nil, nil)
assert.Nil(suite.T(), err)
assert.NotNil(suite.T(), result)
assert.Equal(suite.T(), from, result.FromTime.T())
assert.Equal(suite.T(), to, result.ToTime.T())
suite.DurationService.AssertNumberOfCalls(suite.T(), "Get", 1)
}
func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased() {
sut := NewSummaryService(suite.SummaryRepository, suite.HeartbeatService, suite.DurationService, suite.AliasService, suite.ProjectLabelService)

View File

@@ -29,6 +29,7 @@ tests {
test("Correct time zone", function () {
const targetDateTz = moment(`2021-05-28T00:00:00${bru.getCollectionVar('TZ_OFFSET')}`)
expect(moment(res.body.from).isSame(targetDateTz)).to.eql(true)
expect(moment(res.body.to).isSame(targetDateTz)).to.eql(true)
});
}

View File

@@ -30,6 +30,7 @@ tests {
// when it was midnight in UTC+3, it was still 11 pm in Germany
const targetDateTz = moment(`2021-05-28T00:00:00${bru.getCollectionVar('TZ_OFFSET')}`).add(-1, 'h')
expect(moment(res.body.from).isSame(targetDateTz)).to.eql(true)
expect(moment(res.body.to).isSame(targetDateTz)).to.eql(true)
});
}