2019-05-19 20:49:27 +03:00
package services
import (
2019-10-10 19:10:14 +03:00
"errors"
2019-10-10 17:47:19 +03:00
"fmt"
2019-05-19 20:49:27 +03:00
"math"
2019-07-07 11:37:17 +03:00
"sort"
2019-05-19 20:49:27 +03:00
"time"
"github.com/jinzhu/gorm"
"github.com/n1try/wakapi/models"
)
type SummaryService struct {
Config * models . Config
Db * gorm . DB
HeartbeatService * HeartbeatService
2019-07-06 18:53:20 +03:00
AliasService * AliasService
2019-05-19 20:49:27 +03:00
}
2019-10-10 19:10:14 +03:00
type Interval struct {
Start time . Time
End time . Time
}
2019-10-10 19:32:17 +03:00
// TODO: Rename methods to clarify difference between generating a new summary from old summaries and heartbeats, like this method, and only retrieving a persisted summary from database, like GetByUserWithin
func ( srv * SummaryService ) CreateSummary ( from , to time . Time , user * models . User ) ( * models . Summary , error ) {
2019-10-10 19:10:14 +03:00
existingSummaries , err := srv . GetByUserWithin ( user , from , to )
2019-05-19 20:49:27 +03:00
if err != nil {
return nil , err
}
2019-10-10 19:10:14 +03:00
missingIntervals := getMissingIntervals ( from , to , existingSummaries )
heartbeats := make ( [ ] * models . Heartbeat , 0 )
for _ , interval := range missingIntervals {
hb , err := srv . HeartbeatService . GetAllWithin ( interval . Start , interval . End , user )
if err != nil {
return nil , err
}
heartbeats = append ( heartbeats , hb ... )
}
2019-05-19 22:00:19 +03:00
types := [ ] uint8 { models . SummaryProject , models . SummaryLanguage , models . SummaryEditor , models . SummaryOS }
var projectItems [ ] models . SummaryItem
var languageItems [ ] models . SummaryItem
var editorItems [ ] models . SummaryItem
var osItems [ ] models . SummaryItem
2019-07-07 11:32:28 +03:00
if err := srv . AliasService . LoadUserAliases ( user . ID ) ; err != nil {
return nil , err
}
2019-05-19 22:00:19 +03:00
c := make ( chan models . SummaryItemContainer )
for _ , t := range types {
2019-07-06 18:53:20 +03:00
go srv . aggregateBy ( heartbeats , t , user , c )
2019-05-19 22:00:19 +03:00
}
for i := 0 ; i < len ( types ) ; i ++ {
item := <- c
switch item . Type {
case models . SummaryProject :
projectItems = item . Items
case models . SummaryLanguage :
languageItems = item . Items
case models . SummaryEditor :
editorItems = item . Items
case models . SummaryOS :
osItems = item . Items
}
}
close ( c )
2019-10-10 19:10:14 +03:00
aggregatedSummary := & models . Summary {
2019-05-19 20:49:27 +03:00
UserID : user . ID ,
FromTime : & from ,
ToTime : & to ,
2019-05-19 22:00:19 +03:00
Projects : projectItems ,
Languages : languageItems ,
Editors : editorItems ,
OperatingSystems : osItems ,
2019-05-19 20:49:27 +03:00
}
2019-10-10 19:10:14 +03:00
allSummaries := [ ] * models . Summary { aggregatedSummary }
allSummaries = append ( allSummaries , existingSummaries ... )
summary , err := mergeSummaries ( allSummaries )
if err != nil {
return nil , err
}
2019-05-19 20:49:27 +03:00
return summary , nil
}
2019-10-10 19:10:14 +03:00
func mergeSummaries ( summaries [ ] * models . Summary ) ( * models . Summary , error ) {
if len ( summaries ) < 1 {
return nil , errors . New ( "no summaries given" )
}
var minTime , maxTime time . Time
minTime = time . Now ( )
finalSummary := & models . Summary {
UserID : summaries [ 0 ] . UserID ,
Projects : make ( [ ] models . SummaryItem , 0 ) ,
Languages : make ( [ ] models . SummaryItem , 0 ) ,
Editors : make ( [ ] models . SummaryItem , 0 ) ,
OperatingSystems : make ( [ ] models . SummaryItem , 0 ) ,
}
for _ , s := range summaries {
if s . UserID != finalSummary . UserID {
return nil , errors . New ( "users don't match" )
}
if s . FromTime . Before ( minTime ) {
minTime = * ( s . FromTime )
}
if s . ToTime . After ( maxTime ) {
maxTime = * ( s . ToTime )
}
// TODO: Multi-thread ?
finalSummary . Projects = mergeSummaryItems ( & ( finalSummary . Projects ) , & ( s . Projects ) )
finalSummary . Languages = mergeSummaryItems ( & ( finalSummary . Languages ) , & ( s . Languages ) )
finalSummary . Editors = mergeSummaryItems ( & ( finalSummary . Editors ) , & ( s . Editors ) )
finalSummary . OperatingSystems = mergeSummaryItems ( & ( finalSummary . OperatingSystems ) , & ( s . OperatingSystems ) )
}
finalSummary . FromTime = & minTime
finalSummary . ToTime = & maxTime
return finalSummary , nil
}
func mergeSummaryItems ( existing * [ ] models . SummaryItem , new * [ ] models . SummaryItem ) [ ] models . SummaryItem {
2019-10-10 19:32:17 +03:00
items := make ( map [ string ] * models . SummaryItem )
2019-10-10 19:10:14 +03:00
// Build map from existing
for _ , item := range * existing {
2019-10-10 19:32:17 +03:00
items [ item . Key ] = & item
2019-10-10 19:10:14 +03:00
}
for _ , item := range * new {
2019-10-10 19:32:17 +03:00
if it , ok := items [ item . Key ] ; ! ok {
items [ item . Key ] = & item
2019-10-10 19:10:14 +03:00
} else {
2019-10-10 19:32:17 +03:00
( * it ) . Total += item . Total
2019-10-10 19:10:14 +03:00
}
}
var i int
itemList := make ( [ ] models . SummaryItem , len ( items ) )
for k , v := range items {
2019-10-10 19:32:17 +03:00
itemList [ i ] = models . SummaryItem { Key : k , Total : v . Total , Type : v . Type }
2019-10-10 19:10:14 +03:00
i ++
}
sort . Slice ( itemList , func ( i , j int ) bool {
return itemList [ i ] . Total > itemList [ j ] . Total
} )
return itemList
}
2019-10-10 17:47:19 +03:00
func ( srv * SummaryService ) SaveSummary ( summary * models . Summary ) error {
fmt . Println ( "Saving summary" , summary )
if err := srv . Db . Create ( summary ) . Error ; err != nil {
return err
}
return nil
}
2019-10-10 19:10:14 +03:00
func ( srv * SummaryService ) GetByUserWithin ( user * models . User , from , to time . Time ) ( [ ] * models . Summary , error ) {
var summaries [ ] * models . Summary
if err := srv . Db .
Where ( & models . Summary { UserID : user . ID } ) .
Where ( "from_time >= ?" , from ) .
Where ( "to_time <= ?" , to ) .
2019-10-10 19:32:17 +03:00
Preload ( "Projects" , "type = ?" , models . SummaryProject ) .
Preload ( "Languages" , "type = ?" , models . SummaryLanguage ) .
Preload ( "Editors" , "type = ?" , models . SummaryEditor ) .
Preload ( "OperatingSystems" , "type = ?" , models . SummaryOS ) .
2019-10-10 19:10:14 +03:00
Find ( & summaries ) . Error ; err != nil {
return nil , err
}
return summaries , nil
}
2019-10-10 00:26:28 +03:00
func ( srv * SummaryService ) GetLatestUserSummaries ( ) ( [ ] * models . Summary , error ) {
var summaries [ ] * models . Summary
if err := srv . Db .
Table ( "summaries" ) .
Select ( "user_id, max(to_time) as to_time" ) .
Group ( "user_id" ) .
Scan ( & summaries ) . Error ; err != nil {
return nil , err
}
return summaries , nil
}
2019-07-06 18:53:20 +03:00
func ( srv * SummaryService ) aggregateBy ( heartbeats [ ] * models . Heartbeat , summaryType uint8 , user * models . User , c chan models . SummaryItemContainer ) {
2019-05-19 20:49:27 +03:00
durations := make ( map [ string ] time . Duration )
for i , h := range heartbeats {
var key string
2019-05-19 22:00:19 +03:00
switch summaryType {
2019-05-19 20:49:27 +03:00
case models . SummaryProject :
key = h . Project
case models . SummaryEditor :
key = h . Editor
case models . SummaryLanguage :
key = h . Language
case models . SummaryOS :
key = h . OperatingSystem
}
2019-05-20 19:44:16 +03:00
if key == "" {
2019-05-21 15:02:04 +03:00
key = "unknown"
2019-05-20 19:44:16 +03:00
}
2019-07-06 18:53:20 +03:00
if aliasedKey , err := srv . AliasService . GetAliasOrDefault ( user . ID , summaryType , key ) ; err == nil {
key = aliasedKey
}
2019-05-19 20:49:27 +03:00
if _ , ok := durations [ key ] ; ! ok {
durations [ key ] = time . Duration ( 0 )
}
if i == 0 {
continue
}
timePassed := h . Time . Time ( ) . Sub ( heartbeats [ i - 1 ] . Time . Time ( ) )
timeThresholded := math . Min ( float64 ( timePassed ) , float64 ( time . Duration ( 2 ) * time . Minute ) )
durations [ key ] += time . Duration ( int64 ( timeThresholded ) )
}
items := make ( [ ] models . SummaryItem , 0 )
for k , v := range durations {
items = append ( items , models . SummaryItem {
Key : k ,
Total : v / time . Second ,
2019-10-10 19:32:17 +03:00
Type : summaryType ,
2019-05-19 20:49:27 +03:00
} )
}
2019-07-07 11:37:17 +03:00
sort . Slice ( items , func ( i , j int ) bool {
return items [ i ] . Total > items [ j ] . Total
} )
2019-05-19 22:00:19 +03:00
c <- models . SummaryItemContainer { Type : summaryType , Items : items }
2019-05-19 20:49:27 +03:00
}
2019-10-10 19:10:14 +03:00
func getMissingIntervals ( from , to time . Time , existingSummaries [ ] * models . Summary ) [ ] * Interval {
if len ( existingSummaries ) == 0 {
return [ ] * Interval { & Interval { from , to } }
}
intervals := make ( [ ] * Interval , 0 )
// Pre
if from . Before ( * ( existingSummaries [ 0 ] . FromTime ) ) {
intervals = append ( intervals , & Interval { from , * ( existingSummaries [ 0 ] . FromTime ) } )
}
// Between
for i := 0 ; i < len ( existingSummaries ) - 1 ; i ++ {
if existingSummaries [ i ] . ToTime . Before ( * ( existingSummaries [ i + 1 ] . FromTime ) ) {
intervals = append ( intervals , & Interval { * ( existingSummaries [ i ] . ToTime ) , * ( existingSummaries [ i + 1 ] . FromTime ) } )
}
}
// Post
if to . After ( * ( existingSummaries [ len ( existingSummaries ) - 1 ] . ToTime ) ) {
intervals = append ( intervals , & Interval { to , * ( existingSummaries [ len ( existingSummaries ) - 1 ] . ToTime ) } )
}
return intervals
}