2019-05-19 20:49:27 +03:00
|
|
|
package models
|
|
|
|
|
|
|
|
import (
|
2020-11-22 00:30:56 +03:00
|
|
|
"sort"
|
2019-05-19 20:49:27 +03:00
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2020-08-29 23:03:01 +03:00
|
|
|
NSummaryTypes uint8 = 99
|
2019-05-19 20:49:27 +03:00
|
|
|
SummaryProject uint8 = 0
|
|
|
|
SummaryLanguage uint8 = 1
|
|
|
|
SummaryEditor uint8 = 2
|
|
|
|
SummaryOS uint8 = 3
|
2020-08-29 23:03:01 +03:00
|
|
|
SummaryMachine uint8 = 4
|
2019-05-19 20:49:27 +03:00
|
|
|
)
|
|
|
|
|
2020-09-06 13:15:46 +03:00
|
|
|
const (
|
2020-09-12 13:40:38 +03:00
|
|
|
IntervalToday string = "today"
|
|
|
|
IntervalYesterday string = "day"
|
|
|
|
IntervalThisWeek string = "week"
|
|
|
|
IntervalThisMonth string = "month"
|
|
|
|
IntervalThisYear string = "year"
|
|
|
|
IntervalPast7Days string = "7_days"
|
|
|
|
IntervalPast30Days string = "30_days"
|
|
|
|
IntervalPast12Months string = "12_months"
|
|
|
|
IntervalAny string = "any"
|
2021-01-31 18:23:47 +03:00
|
|
|
|
|
|
|
// https://wakatime.com/developers/#summaries
|
|
|
|
IntervalWakatimeToday string = "Today"
|
|
|
|
IntervalWakatimeYesterday string = "Yesterday"
|
|
|
|
IntervalWakatimeLast7Days string = "Last 7 Days"
|
|
|
|
IntervalWakatimeLast7DaysYesterday string = "Last 7 Days from Yesterday"
|
|
|
|
IntervalWakatimeLast14Days string = "Last 14 Days"
|
|
|
|
IntervalWakatimeLast30Days string = "Last 30 Days"
|
|
|
|
IntervalWakatimeThisWeek string = "This Week"
|
|
|
|
IntervalWakatimeLastWeek string = "Last Week"
|
|
|
|
IntervalWakatimeThisMonth string = "This Month"
|
|
|
|
IntervalWakatimeLastMonth string = "Last Month"
|
2020-09-06 13:15:46 +03:00
|
|
|
)
|
|
|
|
|
2020-09-12 17:09:23 +03:00
|
|
|
func Intervals() []string {
|
|
|
|
return []string{
|
|
|
|
IntervalToday, IntervalYesterday, IntervalThisWeek, IntervalThisMonth, IntervalThisYear, IntervalPast7Days, IntervalPast30Days, IntervalPast12Months, IntervalAny,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-30 00:16:21 +03:00
|
|
|
const UnknownSummaryKey = "unknown"
|
|
|
|
|
2019-05-19 20:49:27 +03:00
|
|
|
type Summary struct {
|
2020-11-22 00:30:56 +03:00
|
|
|
ID uint `json:"-" gorm:"primary_key"`
|
|
|
|
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
|
|
|
UserID string `json:"user_id" gorm:"not null; index:idx_time_summary_user"`
|
|
|
|
FromTime CustomTime `json:"from" gorm:"not null; type:timestamp; default:CURRENT_TIMESTAMP; index:idx_time_summary_user"`
|
|
|
|
ToTime CustomTime `json:"to" gorm:"not null; type:timestamp; default:CURRENT_TIMESTAMP; index:idx_time_summary_user"`
|
|
|
|
Projects SummaryItems `json:"projects" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
|
|
|
Languages SummaryItems `json:"languages" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
|
|
|
Editors SummaryItems `json:"editors" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
|
|
|
OperatingSystems SummaryItems `json:"operating_systems" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
|
|
|
Machines SummaryItems `json:"machines" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
2019-05-19 20:49:27 +03:00
|
|
|
}
|
|
|
|
|
2020-11-22 00:30:56 +03:00
|
|
|
type SummaryItems []*SummaryItem
|
|
|
|
|
2019-05-19 20:49:27 +03:00
|
|
|
type SummaryItem struct {
|
2019-10-10 00:26:28 +03:00
|
|
|
ID uint `json:"-" gorm:"primary_key"`
|
2020-11-06 19:09:41 +03:00
|
|
|
Summary *Summary `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
2019-10-10 00:26:28 +03:00
|
|
|
SummaryID uint `json:"-"`
|
2019-10-10 19:32:17 +03:00
|
|
|
Type uint8 `json:"-"`
|
2019-10-10 00:26:28 +03:00
|
|
|
Key string `json:"key"`
|
|
|
|
Total time.Duration `json:"total"`
|
2019-05-19 20:49:27 +03:00
|
|
|
}
|
2019-05-19 22:00:19 +03:00
|
|
|
|
|
|
|
type SummaryItemContainer struct {
|
|
|
|
Type uint8
|
2019-10-11 09:00:02 +03:00
|
|
|
Items []*SummaryItem
|
2019-05-19 22:00:19 +03:00
|
|
|
}
|
2020-03-31 12:24:44 +03:00
|
|
|
|
|
|
|
type SummaryViewModel struct {
|
|
|
|
*Summary
|
|
|
|
LanguageColors map[string]string
|
2021-01-30 11:51:36 +03:00
|
|
|
EditorColors map[string]string
|
|
|
|
OSColors map[string]string
|
2020-05-24 14:41:19 +03:00
|
|
|
Error string
|
2020-05-24 17:34:32 +03:00
|
|
|
Success string
|
2020-05-24 22:19:05 +03:00
|
|
|
ApiKey string
|
2020-03-31 12:24:44 +03:00
|
|
|
}
|
2020-08-30 00:16:21 +03:00
|
|
|
|
2020-09-06 13:15:46 +03:00
|
|
|
type SummaryParams struct {
|
|
|
|
From time.Time
|
|
|
|
To time.Time
|
|
|
|
User *User
|
|
|
|
Recompute bool
|
|
|
|
}
|
|
|
|
|
2020-11-07 14:01:35 +03:00
|
|
|
type AliasResolver func(t uint8, k string) string
|
|
|
|
|
2020-09-06 13:15:46 +03:00
|
|
|
func SummaryTypes() []uint8 {
|
|
|
|
return []uint8{SummaryProject, SummaryLanguage, SummaryEditor, SummaryOS, SummaryMachine}
|
|
|
|
}
|
|
|
|
|
2020-11-22 00:30:56 +03:00
|
|
|
func (s *Summary) Sorted() *Summary {
|
|
|
|
sort.Sort(sort.Reverse(s.Projects))
|
|
|
|
sort.Sort(sort.Reverse(s.Machines))
|
|
|
|
sort.Sort(sort.Reverse(s.OperatingSystems))
|
|
|
|
sort.Sort(sort.Reverse(s.Languages))
|
|
|
|
sort.Sort(sort.Reverse(s.Editors))
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
2020-09-06 13:15:46 +03:00
|
|
|
func (s *Summary) Types() []uint8 {
|
|
|
|
return SummaryTypes()
|
|
|
|
}
|
|
|
|
|
2020-11-22 00:30:56 +03:00
|
|
|
func (s *Summary) MappedItems() map[uint8]*SummaryItems {
|
|
|
|
return map[uint8]*SummaryItems{
|
2020-09-06 13:15:46 +03:00
|
|
|
SummaryProject: &s.Projects,
|
|
|
|
SummaryLanguage: &s.Languages,
|
|
|
|
SummaryEditor: &s.Editors,
|
|
|
|
SummaryOS: &s.OperatingSystems,
|
|
|
|
SummaryMachine: &s.Machines,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-30 00:16:21 +03:00
|
|
|
/* Augments the summary in a way that at least one item is present for every type.
|
|
|
|
If a summary has zero items for a given type, but one or more for any of the other types,
|
|
|
|
the total summary duration can be derived from those and inserted as a dummy-item with key "unknown"
|
|
|
|
for the missing type.
|
|
|
|
For instance, the machine type was introduced post hoc. Accordingly, no "machine"-information is present in
|
|
|
|
the data for old heartbeats and summaries. If a user has two years of data without machine information and
|
|
|
|
one day with such, a "machine"-chart plotted from that data will reference a way smaller absolute total amount
|
|
|
|
of time than the other ones.
|
|
|
|
To avoid having to modify persisted data retrospectively, i.e. inserting a dummy SummaryItem for the new type,
|
|
|
|
such is generated dynamically here, considering the "machine" for all old heartbeats "unknown".
|
|
|
|
*/
|
|
|
|
func (s *Summary) FillUnknown() {
|
2020-09-06 13:15:46 +03:00
|
|
|
types := s.Types()
|
|
|
|
typeItems := s.MappedItems()
|
2020-08-30 00:16:21 +03:00
|
|
|
missingTypes := make([]uint8, 0)
|
|
|
|
|
|
|
|
for _, t := range types {
|
|
|
|
if len(*typeItems[t]) == 0 {
|
|
|
|
missingTypes = append(missingTypes, t)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// can't proceed if entire summary is empty
|
|
|
|
if len(missingTypes) == len(types) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-09-06 13:15:46 +03:00
|
|
|
timeSum := s.TotalTime()
|
2020-08-30 00:16:21 +03:00
|
|
|
|
|
|
|
// construct dummy item for all missing types
|
|
|
|
for _, t := range missingTypes {
|
|
|
|
*typeItems[t] = append(*typeItems[t], &SummaryItem{
|
|
|
|
Type: t,
|
|
|
|
Key: UnknownSummaryKey,
|
|
|
|
Total: timeSum,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2020-09-06 13:15:46 +03:00
|
|
|
|
|
|
|
func (s *Summary) TotalTime() time.Duration {
|
|
|
|
var timeSum time.Duration
|
|
|
|
|
|
|
|
mappedItems := s.MappedItems()
|
|
|
|
// calculate total duration from any of the present sets of items
|
|
|
|
for _, t := range s.Types() {
|
|
|
|
if items := mappedItems[t]; len(*items) > 0 {
|
|
|
|
for _, item := range *items {
|
|
|
|
timeSum += item.Total
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-12 00:24:51 +03:00
|
|
|
return timeSum * time.Second
|
2020-09-06 13:15:46 +03:00
|
|
|
}
|
2020-09-11 21:22:33 +03:00
|
|
|
|
2020-11-01 14:50:59 +03:00
|
|
|
func (s *Summary) TotalTimeBy(entityType uint8) (timeSum time.Duration) {
|
2020-09-11 21:22:33 +03:00
|
|
|
mappedItems := s.MappedItems()
|
2020-09-12 00:24:51 +03:00
|
|
|
if items := mappedItems[entityType]; len(*items) > 0 {
|
|
|
|
for _, item := range *items {
|
2020-11-01 14:50:59 +03:00
|
|
|
timeSum = timeSum + item.Total*time.Second
|
2020-09-12 00:24:51 +03:00
|
|
|
}
|
|
|
|
}
|
2020-11-01 14:50:59 +03:00
|
|
|
return timeSum
|
2020-09-12 00:24:51 +03:00
|
|
|
}
|
|
|
|
|
2020-11-01 14:50:59 +03:00
|
|
|
func (s *Summary) TotalTimeByKey(entityType uint8, key string) (timeSum time.Duration) {
|
2020-09-12 00:24:51 +03:00
|
|
|
mappedItems := s.MappedItems()
|
2020-09-11 21:22:33 +03:00
|
|
|
if items := mappedItems[entityType]; len(*items) > 0 {
|
|
|
|
for _, item := range *items {
|
|
|
|
if item.Key != key {
|
|
|
|
continue
|
|
|
|
}
|
2020-11-01 14:50:59 +03:00
|
|
|
timeSum = timeSum + item.Total*time.Second
|
2020-09-11 21:22:33 +03:00
|
|
|
}
|
|
|
|
}
|
2020-11-01 14:50:59 +03:00
|
|
|
return timeSum
|
|
|
|
}
|
2020-09-11 21:22:33 +03:00
|
|
|
|
2021-01-31 18:58:59 +03:00
|
|
|
func (s *Summary) TotalTimeByFilters(filters *Filters) time.Duration {
|
2021-01-31 18:23:47 +03:00
|
|
|
do, typeId, key := filters.One()
|
|
|
|
if do {
|
|
|
|
return s.TotalTimeByKey(typeId, key)
|
2020-11-01 14:50:59 +03:00
|
|
|
}
|
2021-01-31 18:58:59 +03:00
|
|
|
return 0
|
2020-09-11 21:22:33 +03:00
|
|
|
}
|
2020-11-07 14:01:35 +03:00
|
|
|
|
|
|
|
func (s *Summary) WithResolvedAliases(resolve AliasResolver) *Summary {
|
|
|
|
processAliases := func(origin []*SummaryItem) []*SummaryItem {
|
|
|
|
target := make([]*SummaryItem, 0)
|
|
|
|
|
|
|
|
findItem := func(key string) *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 := resolve(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 := resolve(item.Type, item.Key); key != item.Key {
|
|
|
|
if targetItem := findItem(key); targetItem != nil {
|
|
|
|
targetItem.Total += item.Total
|
|
|
|
} else {
|
|
|
|
target = append(target, &SummaryItem{
|
|
|
|
ID: item.ID,
|
|
|
|
SummaryID: item.SummaryID,
|
|
|
|
Type: item.Type,
|
|
|
|
Key: key,
|
|
|
|
Total: item.Total,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return target
|
|
|
|
}
|
|
|
|
|
|
|
|
// Resolve aliases
|
|
|
|
s.Projects = processAliases(s.Projects)
|
|
|
|
s.Editors = processAliases(s.Editors)
|
|
|
|
s.Languages = processAliases(s.Languages)
|
|
|
|
s.OperatingSystems = processAliases(s.OperatingSystems)
|
|
|
|
s.Machines = processAliases(s.Machines)
|
|
|
|
|
|
|
|
return s
|
|
|
|
}
|
2020-11-22 00:30:56 +03:00
|
|
|
|
|
|
|
func (s SummaryItems) Len() int {
|
|
|
|
return len(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s SummaryItems) Less(i, j int) bool {
|
|
|
|
return s[i].Total < s[j].Total
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s SummaryItems) Swap(i, j int) {
|
|
|
|
s[i], s[j] = s[j], s[i]
|
|
|
|
}
|