diff --git a/models/compat/shields/v1/badge.go b/models/compat/shields/v1/badge.go index a6cd7ed..515a2c8 100644 --- a/models/compat/shields/v1/badge.go +++ b/models/compat/shields/v1/badge.go @@ -22,8 +22,8 @@ type BadgeData struct { func NewBadgeDataFrom(summary *models.Summary, filters *models.Filters) *BadgeData { var total time.Duration - if hasFilter, filterType, filterKey := filters.First(); hasFilter { - total = summary.TotalTimeByKey(filterType, filterKey) + if hasFilter, _, _ := filters.First(); hasFilter { + total = summary.TotalTimeByFilters(filters) } else { total = summary.TotalTime() } diff --git a/models/compat/wakatime/v1/all_time.go b/models/compat/wakatime/v1/all_time.go index 44aa537..3d87a32 100644 --- a/models/compat/wakatime/v1/all_time.go +++ b/models/compat/wakatime/v1/all_time.go @@ -21,7 +21,7 @@ type allTimeData struct { func NewAllTimeFrom(summary *models.Summary, filters *models.Filters) *AllTimeViewModel { var total time.Duration if key := filters.Project; key != "" { - total = summary.TotalTimeByKey(models.SummaryProject, key) + total = summary.TotalTimeByFilters(filters) } else { total = summary.TotalTime() } diff --git a/models/filters.go b/models/filters.go new file mode 100644 index 0000000..70d847e --- /dev/null +++ b/models/filters.go @@ -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 +} diff --git a/models/shared.go b/models/shared.go index c591a53..a5f7b4c 100644 --- a/models/shared.go +++ b/models/shared.go @@ -24,45 +24,6 @@ type KeyStringValue struct { 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 func (j *CustomTime) UnmarshalJSON(b []byte) error { diff --git a/models/summary.go b/models/summary.go index 1141ef0..b0f97c2 100644 --- a/models/summary.go +++ b/models/summary.go @@ -147,31 +147,32 @@ func (s *Summary) TotalTime() time.Duration { return timeSum * time.Second } -func (s *Summary) TotalTimeBy(entityType uint8) time.Duration { - var timeSum time.Duration - +func (s *Summary) TotalTimeBy(entityType uint8) (timeSum time.Duration) { mappedItems := s.MappedItems() if items := mappedItems[entityType]; len(*items) > 0 { for _, item := range *items { - timeSum += item.Total + timeSum = timeSum + item.Total*time.Second } } - - return timeSum * time.Second + return timeSum } -func (s *Summary) TotalTimeByKey(entityType uint8, key string) time.Duration { - var timeSum time.Duration - +func (s *Summary) TotalTimeByKey(entityType uint8, key string) (timeSum time.Duration) { mappedItems := s.MappedItems() if items := mappedItems[entityType]; len(*items) > 0 { for _, item := range *items { if item.Key != key { continue } - timeSum += item.Total + timeSum = timeSum + item.Total*time.Second } } - - return timeSum * time.Second + return timeSum +} + +func (s *Summary) TotalTimeByFilters(filter *Filters) (timeSum time.Duration) { + for _, f := range filter.All() { + timeSum += s.TotalTimeByKey(f.Type, f.Key) + } + return timeSum } diff --git a/routes/compat/shields/v1/badge.go b/routes/compat/shields/v1/badge.go index 6d003ae..4c865de 100644 --- a/routes/compat/shields/v1/badge.go +++ b/routes/compat/shields/v1/badge.go @@ -20,6 +20,7 @@ const ( type BadgeHandler struct { userSrvc *services.UserService summarySrvc *services.SummaryService + aliasSrvc *services.AliasService config *config2.Config } @@ -57,18 +58,18 @@ func (h *BadgeHandler) ApiGet(w http.ResponseWriter, r *http.Request) { interval = groups[1] } - filters := &models.Filters{} + var filters *models.Filters switch filterEntity { case "project": - filters.Project = filterKey + filters = models.NewFiltersWith(models.SummaryProject, filterKey) case "os": - filters.OS = filterKey + filters = models.NewFiltersWith(models.SummaryOS, filterKey) case "editor": - filters.Editor = filterKey + filters = models.NewFiltersWith(models.SummaryEditor, filterKey) case "language": - filters.Language = filterKey + filters = models.NewFiltersWith(models.SummaryLanguage, filterKey) case "machine": - filters.Machine = filterKey + filters = models.NewFiltersWith(models.SummaryMachine, filterKey) } summary, err, status := h.loadUserSummary(user, interval) @@ -94,7 +95,9 @@ func (h *BadgeHandler) loadUserSummary(user *models.User, interval string) (*mod 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 { return nil, err, http.StatusInternalServerError } diff --git a/routes/compat/wakatime/v1/all_time.go b/routes/compat/wakatime/v1/all_time.go index 51577ce..28d4475 100644 --- a/routes/compat/wakatime/v1/all_time.go +++ b/routes/compat/wakatime/v1/all_time.go @@ -55,7 +55,9 @@ func (h *AllTimeHandler) loadUserSummary(user *models.User) (*models.Summary, er 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 { return nil, err, http.StatusInternalServerError } diff --git a/routes/compat/wakatime/v1/summaries.go b/routes/compat/wakatime/v1/summaries.go index fa3e40b..4565377 100644 --- a/routes/compat/wakatime/v1/summaries.go +++ b/routes/compat/wakatime/v1/summaries.go @@ -86,7 +86,9 @@ func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary summaries := make([]*models.Summary, len(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 { return nil, err, http.StatusInternalServerError } diff --git a/routes/summary.go b/routes/summary.go index 92fe2d8..a81ba26 100644 --- a/routes/summary.go +++ b/routes/summary.go @@ -69,7 +69,9 @@ func (h *SummaryHandler) loadUserSummary(r *http.Request) (*models.Summary, erro 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 { return nil, err, http.StatusInternalServerError } diff --git a/services/summary.go b/services/summary.go index 1d9ea2a..dfc70d4 100644 --- a/services/summary.go +++ b/services/summary.go @@ -150,6 +150,62 @@ func (srv *SummaryService) Construct(from, to time.Time, user *models.User, reco 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 { if err := srv.Db.Create(summary).Error; err != nil { return err @@ -210,10 +266,6 @@ func (srv *SummaryService) aggregateBy(heartbeats []*models.Heartbeat, summaryTy key = models.UnknownSummaryKey } - if aliasedKey, err := srv.AliasService.GetAliasOrDefault(user.ID, summaryType, key); err == nil { - key = aliasedKey - } - if _, ok := durations[key]; !ok { durations[key] = time.Duration(0) } diff --git a/version.txt b/version.txt index feaae22..b50dd27 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.13.0 +1.13.1