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

fix: critical fixes related to alias resolution

This commit is contained in:
Ferdinand Mütsch 2020-11-01 12:50:59 +01:00
parent 861c81e414
commit c2d30826f6
11 changed files with 159 additions and 69 deletions

View File

@ -22,8 +22,8 @@ type BadgeData struct {
func NewBadgeDataFrom(summary *models.Summary, filters *models.Filters) *BadgeData { func NewBadgeDataFrom(summary *models.Summary, filters *models.Filters) *BadgeData {
var total time.Duration var total time.Duration
if hasFilter, filterType, filterKey := filters.First(); hasFilter { if hasFilter, _, _ := filters.First(); hasFilter {
total = summary.TotalTimeByKey(filterType, filterKey) total = summary.TotalTimeByFilters(filters)
} else { } else {
total = summary.TotalTime() total = summary.TotalTime()
} }

View File

@ -21,7 +21,7 @@ type allTimeData struct {
func NewAllTimeFrom(summary *models.Summary, filters *models.Filters) *AllTimeViewModel { func NewAllTimeFrom(summary *models.Summary, filters *models.Filters) *AllTimeViewModel {
var total time.Duration var total time.Duration
if key := filters.Project; key != "" { if key := filters.Project; key != "" {
total = summary.TotalTimeByKey(models.SummaryProject, key) total = summary.TotalTimeByFilters(filters)
} else { } else {
total = summary.TotalTime() total = summary.TotalTime()
} }

67
models/filters.go Normal file
View File

@ -0,0 +1,67 @@
package models
type Filters struct {
Project string
OS string
Language string
Editor string
Machine string
}
type FilterElement struct {
Type uint8
Key string
}
func NewFiltersWith(entity uint8, key string) *Filters {
switch entity {
case SummaryProject:
return &Filters{Project: key}
case SummaryOS:
return &Filters{Project: key}
case SummaryLanguage:
return &Filters{Project: key}
case SummaryEditor:
return &Filters{Project: key}
case SummaryMachine:
return &Filters{Project: key}
}
return &Filters{}
}
func (f *Filters) First() (bool, uint8, string) {
if f.Project != "" {
return true, SummaryProject, f.Project
} else if f.OS != "" {
return true, SummaryOS, f.OS
} else if f.Language != "" {
return true, SummaryLanguage, f.Language
} else if f.Editor != "" {
return true, SummaryEditor, f.Editor
} else if f.Machine != "" {
return true, SummaryMachine, f.Machine
}
return false, 0, ""
}
func (f *Filters) All() []*FilterElement {
all := make([]*FilterElement, 0)
if f.Project != "" {
all = append(all, &FilterElement{Type: SummaryProject, Key: f.Project})
}
if f.Editor != "" {
all = append(all, &FilterElement{Type: SummaryEditor, Key: f.Editor})
}
if f.Language != "" {
all = append(all, &FilterElement{Type: SummaryLanguage, Key: f.Language})
}
if f.Machine != "" {
all = append(all, &FilterElement{Type: SummaryMachine, Key: f.Machine})
}
if f.OS != "" {
all = append(all, &FilterElement{Type: SummaryOS, Key: f.OS})
}
return all
}

View File

@ -24,45 +24,6 @@ type KeyStringValue struct {
Value string `gorm:"type:text"` Value string `gorm:"type:text"`
} }
type Filters struct {
Project string
OS string
Language string
Editor string
Machine string
}
func NewFiltersWith(entity uint8, key string) *Filters {
switch entity {
case SummaryProject:
return &Filters{Project: key}
case SummaryOS:
return &Filters{Project: key}
case SummaryLanguage:
return &Filters{Project: key}
case SummaryEditor:
return &Filters{Project: key}
case SummaryMachine:
return &Filters{Project: key}
}
return &Filters{}
}
func (f *Filters) First() (bool, uint8, string) {
if f.Project != "" {
return true, SummaryProject, f.Project
} else if f.OS != "" {
return true, SummaryOS, f.OS
} else if f.Language != "" {
return true, SummaryLanguage, f.Language
} else if f.Editor != "" {
return true, SummaryEditor, f.Editor
} else if f.Machine != "" {
return true, SummaryMachine, f.Machine
}
return false, 0, ""
}
type CustomTime time.Time type CustomTime time.Time
func (j *CustomTime) UnmarshalJSON(b []byte) error { func (j *CustomTime) UnmarshalJSON(b []byte) error {

View File

@ -147,31 +147,32 @@ func (s *Summary) TotalTime() time.Duration {
return timeSum * time.Second return timeSum * time.Second
} }
func (s *Summary) TotalTimeBy(entityType uint8) time.Duration { func (s *Summary) TotalTimeBy(entityType uint8) (timeSum time.Duration) {
var timeSum time.Duration
mappedItems := s.MappedItems() mappedItems := s.MappedItems()
if items := mappedItems[entityType]; len(*items) > 0 { if items := mappedItems[entityType]; len(*items) > 0 {
for _, item := range *items { for _, item := range *items {
timeSum += item.Total timeSum = timeSum + item.Total*time.Second
} }
} }
return timeSum
return timeSum * time.Second
} }
func (s *Summary) TotalTimeByKey(entityType uint8, key string) time.Duration { func (s *Summary) TotalTimeByKey(entityType uint8, key string) (timeSum time.Duration) {
var timeSum time.Duration
mappedItems := s.MappedItems() mappedItems := s.MappedItems()
if items := mappedItems[entityType]; len(*items) > 0 { if items := mappedItems[entityType]; len(*items) > 0 {
for _, item := range *items { for _, item := range *items {
if item.Key != key { if item.Key != key {
continue continue
} }
timeSum += item.Total timeSum = timeSum + item.Total*time.Second
} }
} }
return timeSum
return timeSum * time.Second }
func (s *Summary) TotalTimeByFilters(filter *Filters) (timeSum time.Duration) {
for _, f := range filter.All() {
timeSum += s.TotalTimeByKey(f.Type, f.Key)
}
return timeSum
} }

View File

@ -20,6 +20,7 @@ const (
type BadgeHandler struct { type BadgeHandler struct {
userSrvc *services.UserService userSrvc *services.UserService
summarySrvc *services.SummaryService summarySrvc *services.SummaryService
aliasSrvc *services.AliasService
config *config2.Config config *config2.Config
} }
@ -57,18 +58,18 @@ func (h *BadgeHandler) ApiGet(w http.ResponseWriter, r *http.Request) {
interval = groups[1] interval = groups[1]
} }
filters := &models.Filters{} var filters *models.Filters
switch filterEntity { switch filterEntity {
case "project": case "project":
filters.Project = filterKey filters = models.NewFiltersWith(models.SummaryProject, filterKey)
case "os": case "os":
filters.OS = filterKey filters = models.NewFiltersWith(models.SummaryOS, filterKey)
case "editor": case "editor":
filters.Editor = filterKey filters = models.NewFiltersWith(models.SummaryEditor, filterKey)
case "language": case "language":
filters.Language = filterKey filters = models.NewFiltersWith(models.SummaryLanguage, filterKey)
case "machine": case "machine":
filters.Machine = filterKey filters = models.NewFiltersWith(models.SummaryMachine, filterKey)
} }
summary, err, status := h.loadUserSummary(user, interval) summary, err, status := h.loadUserSummary(user, interval)
@ -94,7 +95,9 @@ func (h *BadgeHandler) loadUserSummary(user *models.User, interval string) (*mod
User: user, User: user,
} }
summary, err := h.summarySrvc.Construct(summaryParams.From, summaryParams.To, summaryParams.User, summaryParams.Recompute) summary, err := h.summarySrvc.PostProcessWrapped(
h.summarySrvc.Construct(summaryParams.From, summaryParams.To, summaryParams.User, summaryParams.Recompute),
)
if err != nil { if err != nil {
return nil, err, http.StatusInternalServerError return nil, err, http.StatusInternalServerError
} }

View File

@ -55,7 +55,9 @@ func (h *AllTimeHandler) loadUserSummary(user *models.User) (*models.Summary, er
Recompute: false, Recompute: false,
} }
summary, err := h.summarySrvc.Construct(summaryParams.From, summaryParams.To, summaryParams.User, summaryParams.Recompute) // 'to' is always constant summary, err := h.summarySrvc.PostProcessWrapped(
h.summarySrvc.Construct(summaryParams.From, summaryParams.To, summaryParams.User, summaryParams.Recompute), // 'to' is always constant
)
if err != nil { if err != nil {
return nil, err, http.StatusInternalServerError return nil, err, http.StatusInternalServerError
} }

View File

@ -86,7 +86,9 @@ func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary
summaries := make([]*models.Summary, len(intervals)) summaries := make([]*models.Summary, len(intervals))
for i, interval := range intervals { for i, interval := range intervals {
summary, err := h.summarySrvc.Construct(interval[0], interval[1], user, false) // 'to' is always constant summary, err := h.summarySrvc.PostProcessWrapped(
h.summarySrvc.Construct(interval[0], interval[1], user, false), // 'to' is always constant
)
if err != nil { if err != nil {
return nil, err, http.StatusInternalServerError return nil, err, http.StatusInternalServerError
} }

View File

@ -69,7 +69,9 @@ func (h *SummaryHandler) loadUserSummary(r *http.Request) (*models.Summary, erro
return nil, err, http.StatusBadRequest return nil, err, http.StatusBadRequest
} }
summary, err := h.summarySrvc.Construct(summaryParams.From, summaryParams.To, summaryParams.User, summaryParams.Recompute) // 'to' is always constant summary, err := h.summarySrvc.PostProcessWrapped(
h.summarySrvc.Construct(summaryParams.From, summaryParams.To, summaryParams.User, summaryParams.Recompute), // 'to' is always constant
)
if err != nil { if err != nil {
return nil, err, http.StatusInternalServerError return nil, err, http.StatusInternalServerError
} }

View File

@ -150,6 +150,62 @@ func (srv *SummaryService) Construct(from, to time.Time, user *models.User, reco
return summary, nil return summary, nil
} }
func (srv *SummaryService) PostProcessWrapped(summary *models.Summary, err error) (*models.Summary, error) {
if err != nil {
return nil, err
}
return srv.PostProcess(summary), nil
}
func (srv *SummaryService) PostProcess(summary *models.Summary) *models.Summary {
updatedSummary := &models.Summary{
ID: summary.ID,
UserID: summary.UserID,
FromTime: summary.FromTime,
ToTime: summary.ToTime,
}
processAliases := func(origin []*models.SummaryItem) []*models.SummaryItem {
target := make([]*models.SummaryItem, 0)
findItem := func(key string) *models.SummaryItem {
for _, item := range target {
if item.Key == key {
return item
}
}
return nil
}
for _, item := range origin {
// Add all "top-level" items, i.e. such without aliases
if key, _ := srv.AliasService.GetAliasOrDefault(summary.UserID, item.Type, item.Key); key == item.Key {
target = append(target, item)
}
}
for _, item := range origin {
// Add all remaining projects and merge with their alias
if key, _ := srv.AliasService.GetAliasOrDefault(summary.UserID, item.Type, item.Key); key != item.Key {
if targetItem := findItem(key); targetItem != nil {
targetItem.Total += item.Total
}
}
}
return target
}
// Resolve aliases
updatedSummary.Projects = processAliases(summary.Projects)
updatedSummary.Editors = processAliases(summary.Editors)
updatedSummary.Languages = processAliases(summary.Languages)
updatedSummary.OperatingSystems = processAliases(summary.OperatingSystems)
updatedSummary.Machines = processAliases(summary.Machines)
return updatedSummary
}
func (srv *SummaryService) Insert(summary *models.Summary) error { func (srv *SummaryService) Insert(summary *models.Summary) error {
if err := srv.Db.Create(summary).Error; err != nil { if err := srv.Db.Create(summary).Error; err != nil {
return err return err
@ -210,10 +266,6 @@ func (srv *SummaryService) aggregateBy(heartbeats []*models.Heartbeat, summaryTy
key = models.UnknownSummaryKey key = models.UnknownSummaryKey
} }
if aliasedKey, err := srv.AliasService.GetAliasOrDefault(user.ID, summaryType, key); err == nil {
key = aliasedKey
}
if _, ok := durations[key]; !ok { if _, ok := durations[key]; !ok {
durations[key] = time.Duration(0) durations[key] = time.Duration(0)
} }

View File

@ -1 +1 @@
1.13.0 1.13.1