mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
fix: make stats endpoint default to user-chosen time range (resolve #508)
chore: include more properties in status model for better compatibility
This commit is contained in:
parent
938290b2da
commit
ec65847d0c
File diff suppressed because it is too large
Load Diff
107
helpers/interval.go
Normal file
107
helpers/interval.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"github.com/muety/wakapi/utils"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseInterval(interval string) (*models.IntervalKey, error) {
|
||||||
|
for _, i := range models.AllIntervals {
|
||||||
|
if i.HasAlias(interval) {
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("not a valid interval")
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustParseInterval(interval string) *models.IntervalKey {
|
||||||
|
key, _ := ParseInterval(interval)
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustResolveIntervalRawTZ(interval string, tz *time.Location) (from, to time.Time) {
|
||||||
|
_, from, to = ResolveIntervalRawTZ(interval, tz)
|
||||||
|
return from, to
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveIntervalRawTZ(interval string, tz *time.Location) (err error, from, to time.Time) {
|
||||||
|
parsed, err := ParseInterval(interval)
|
||||||
|
if err != nil {
|
||||||
|
return err, time.Time{}, time.Time{}
|
||||||
|
}
|
||||||
|
return ResolveIntervalTZ(parsed, tz)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveIntervalTZ(interval *models.IntervalKey, tz *time.Location) (err error, from, to time.Time) {
|
||||||
|
now := time.Now().In(tz)
|
||||||
|
to = now
|
||||||
|
|
||||||
|
switch interval {
|
||||||
|
case models.IntervalToday:
|
||||||
|
from = utils.BeginOfToday(tz)
|
||||||
|
case models.IntervalYesterday:
|
||||||
|
from = utils.BeginOfToday(tz).Add(-24 * time.Hour)
|
||||||
|
to = utils.BeginOfToday(tz)
|
||||||
|
case models.IntervalPastDay:
|
||||||
|
from = now.Add(-24 * time.Hour)
|
||||||
|
case models.IntervalThisWeek:
|
||||||
|
from = utils.BeginOfThisWeek(tz)
|
||||||
|
case models.IntervalLastWeek:
|
||||||
|
from = utils.BeginOfThisWeek(tz).AddDate(0, 0, -7)
|
||||||
|
to = utils.BeginOfThisWeek(tz)
|
||||||
|
case models.IntervalThisMonth:
|
||||||
|
from = utils.BeginOfThisMonth(tz)
|
||||||
|
case models.IntervalLastMonth:
|
||||||
|
from = utils.BeginOfThisMonth(tz).AddDate(0, -1, 0)
|
||||||
|
to = utils.BeginOfThisMonth(tz)
|
||||||
|
case models.IntervalThisYear:
|
||||||
|
from = utils.BeginOfThisYear(tz)
|
||||||
|
case models.IntervalPast7Days:
|
||||||
|
from = now.AddDate(0, 0, -7)
|
||||||
|
case models.IntervalPast7DaysYesterday:
|
||||||
|
from = utils.BeginOfToday(tz).AddDate(0, 0, -1).AddDate(0, 0, -7)
|
||||||
|
to = utils.BeginOfToday(tz).AddDate(0, 0, -1)
|
||||||
|
case models.IntervalPast14Days:
|
||||||
|
from = now.AddDate(0, 0, -14)
|
||||||
|
case models.IntervalPast30Days:
|
||||||
|
from = now.AddDate(0, 0, -30)
|
||||||
|
case models.IntervalPast6Months:
|
||||||
|
from = now.AddDate(0, -6, 0)
|
||||||
|
case models.IntervalPast12Months:
|
||||||
|
from = now.AddDate(0, -12, 0)
|
||||||
|
case models.IntervalAny:
|
||||||
|
from = time.Time{}
|
||||||
|
default:
|
||||||
|
err = errors.New("invalid interval")
|
||||||
|
}
|
||||||
|
|
||||||
|
return err, from, to
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveClosestRange returns the interval label (e.g. "last_7_days") of the maximum allowed range when having opted to share this many days or an error for days == 0.
|
||||||
|
func ResolveMaximumRange(days int) (error, *models.IntervalKey) {
|
||||||
|
if days == 0 {
|
||||||
|
return errors.New("no matching interval"), nil
|
||||||
|
}
|
||||||
|
if days < 0 {
|
||||||
|
return nil, models.IntervalAny
|
||||||
|
}
|
||||||
|
if days < 7 {
|
||||||
|
return nil, models.IntervalPastDay
|
||||||
|
}
|
||||||
|
if days < 14 {
|
||||||
|
return nil, models.IntervalPast7Days
|
||||||
|
}
|
||||||
|
if days < 30 {
|
||||||
|
return nil, models.IntervalPast14Days
|
||||||
|
}
|
||||||
|
if days < 181 { // 3*31 + 2*30 + 1*28
|
||||||
|
return nil, models.IntervalPast30Days
|
||||||
|
}
|
||||||
|
if days < 365 { // 7*31 + 4*30 + 1*28
|
||||||
|
return nil, models.IntervalPast6Months
|
||||||
|
}
|
||||||
|
return nil, models.IntervalPast12Months
|
||||||
|
}
|
27
helpers/interval_test.go
Normal file
27
helpers/interval_test.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveMaximumRange_Default(t *testing.T) {
|
||||||
|
for i := 1; i <= 366; i++ {
|
||||||
|
err1, maximumInterval := ResolveMaximumRange(i)
|
||||||
|
err2, from, to := ResolveIntervalTZ(maximumInterval, time.UTC)
|
||||||
|
|
||||||
|
assert.Nil(t, err1)
|
||||||
|
assert.Nil(t, err2)
|
||||||
|
assert.LessOrEqual(t, to.Sub(from), time.Duration(i*24)*time.Hour)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveMaximumRange_EdgeCases(t *testing.T) {
|
||||||
|
err, _ := ResolveMaximumRange(0)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
_, maximumInterval := ResolveMaximumRange(-1)
|
||||||
|
assert.Equal(t, models.IntervalAny, maximumInterval)
|
||||||
|
}
|
@ -3,7 +3,6 @@ package helpers
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"github.com/muety/wakapi/utils"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -82,69 +81,3 @@ func extractUser(r *http.Request) *models.User {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseInterval(interval string) (*models.IntervalKey, error) {
|
|
||||||
for _, i := range models.AllIntervals {
|
|
||||||
if i.HasAlias(interval) {
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, errors.New("not a valid interval")
|
|
||||||
}
|
|
||||||
|
|
||||||
func MustResolveIntervalRawTZ(interval string, tz *time.Location) (from, to time.Time) {
|
|
||||||
_, from, to = ResolveIntervalRawTZ(interval, tz)
|
|
||||||
return from, to
|
|
||||||
}
|
|
||||||
|
|
||||||
func ResolveIntervalRawTZ(interval string, tz *time.Location) (err error, from, to time.Time) {
|
|
||||||
parsed, err := ParseInterval(interval)
|
|
||||||
if err != nil {
|
|
||||||
return err, time.Time{}, time.Time{}
|
|
||||||
}
|
|
||||||
return ResolveIntervalTZ(parsed, tz)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ResolveIntervalTZ(interval *models.IntervalKey, tz *time.Location) (err error, from, to time.Time) {
|
|
||||||
now := time.Now().In(tz)
|
|
||||||
to = now
|
|
||||||
|
|
||||||
switch interval {
|
|
||||||
case models.IntervalToday:
|
|
||||||
from = utils.BeginOfToday(tz)
|
|
||||||
case models.IntervalYesterday:
|
|
||||||
from = utils.BeginOfToday(tz).Add(-24 * time.Hour)
|
|
||||||
to = utils.BeginOfToday(tz)
|
|
||||||
case models.IntervalThisWeek:
|
|
||||||
from = utils.BeginOfThisWeek(tz)
|
|
||||||
case models.IntervalLastWeek:
|
|
||||||
from = utils.BeginOfThisWeek(tz).AddDate(0, 0, -7)
|
|
||||||
to = utils.BeginOfThisWeek(tz)
|
|
||||||
case models.IntervalThisMonth:
|
|
||||||
from = utils.BeginOfThisMonth(tz)
|
|
||||||
case models.IntervalLastMonth:
|
|
||||||
from = utils.BeginOfThisMonth(tz).AddDate(0, -1, 0)
|
|
||||||
to = utils.BeginOfThisMonth(tz)
|
|
||||||
case models.IntervalThisYear:
|
|
||||||
from = utils.BeginOfThisYear(tz)
|
|
||||||
case models.IntervalPast7Days:
|
|
||||||
from = now.AddDate(0, 0, -7)
|
|
||||||
case models.IntervalPast7DaysYesterday:
|
|
||||||
from = utils.BeginOfToday(tz).AddDate(0, 0, -1).AddDate(0, 0, -7)
|
|
||||||
to = utils.BeginOfToday(tz).AddDate(0, 0, -1)
|
|
||||||
case models.IntervalPast14Days:
|
|
||||||
from = now.AddDate(0, 0, -14)
|
|
||||||
case models.IntervalPast30Days:
|
|
||||||
from = now.AddDate(0, 0, -30)
|
|
||||||
case models.IntervalPast6Months:
|
|
||||||
from = now.AddDate(0, -6, 0)
|
|
||||||
case models.IntervalPast12Months:
|
|
||||||
from = now.AddDate(0, -12, 0)
|
|
||||||
case models.IntervalAny:
|
|
||||||
from = time.Time{}
|
|
||||||
default:
|
|
||||||
err = errors.New("invalid interval")
|
|
||||||
}
|
|
||||||
|
|
||||||
return err, from, to
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/muety/wakapi/helpers"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
@ -18,9 +19,16 @@ type StatsData struct {
|
|||||||
UserId string `json:"user_id"`
|
UserId string `json:"user_id"`
|
||||||
Start time.Time `json:"start"`
|
Start time.Time `json:"start"`
|
||||||
End time.Time `json:"end"`
|
End time.Time `json:"end"`
|
||||||
|
Status string `json:"status"`
|
||||||
TotalSeconds float64 `json:"total_seconds"`
|
TotalSeconds float64 `json:"total_seconds"`
|
||||||
DailyAverage float64 `json:"daily_average"`
|
DailyAverage float64 `json:"daily_average"`
|
||||||
DaysIncludingHolidays int `json:"days_including_holidays"`
|
DaysIncludingHolidays int `json:"days_including_holidays"`
|
||||||
|
Range string `json:"range"`
|
||||||
|
HumanReadableRange string `json:"human_readable_range"`
|
||||||
|
HumanReadableTotal string `json:"human_readable_total"`
|
||||||
|
HumanReadableDailyAverage string `json:"human_readable_daily_average"`
|
||||||
|
IsCodingActivityVisible bool `json:"is_coding_activity_visible"`
|
||||||
|
IsOtherUsageVisible bool `json:"is_other_usage_visible"`
|
||||||
Editors []*SummariesEntry `json:"editors"`
|
Editors []*SummariesEntry `json:"editors"`
|
||||||
Languages []*SummariesEntry `json:"languages"`
|
Languages []*SummariesEntry `json:"languages"`
|
||||||
Machines []*SummariesEntry `json:"machines"`
|
Machines []*SummariesEntry `json:"machines"`
|
||||||
@ -38,11 +46,16 @@ func NewStatsFrom(summary *models.Summary, filters *models.Filters) *StatsViewMo
|
|||||||
UserId: summary.UserID,
|
UserId: summary.UserID,
|
||||||
Start: summary.FromTime.T(),
|
Start: summary.FromTime.T(),
|
||||||
End: summary.ToTime.T(),
|
End: summary.ToTime.T(),
|
||||||
|
Status: "ok",
|
||||||
TotalSeconds: totalTime.Seconds(),
|
TotalSeconds: totalTime.Seconds(),
|
||||||
DailyAverage: totalTime.Seconds() / float64(numDays),
|
|
||||||
DaysIncludingHolidays: numDays,
|
DaysIncludingHolidays: numDays,
|
||||||
|
HumanReadableTotal: helpers.FmtWakatimeDuration(totalTime),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if numDays > 0 {
|
||||||
|
data.DailyAverage = totalTime.Seconds() / float64(numDays)
|
||||||
|
data.HumanReadableDailyAverage = helpers.FmtWakatimeDuration(totalTime / time.Duration(numDays))
|
||||||
|
}
|
||||||
if math.IsInf(data.DailyAverage, 0) || math.IsNaN(data.DailyAverage) {
|
if math.IsInf(data.DailyAverage, 0) || math.IsNaN(data.DailyAverage) {
|
||||||
data.DailyAverage = 0
|
data.DailyAverage = 0
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,33 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
// Support Wakapi and WakaTime range / interval identifiers
|
// Support Wakapi and WakaTime range / interval identifiers
|
||||||
// See https://wakatime.com/developers/#summaries
|
// See https://wakatime.com/developers/#summaries
|
||||||
var (
|
var (
|
||||||
IntervalToday = &IntervalKey{"today", "Today"}
|
IntervalToday = &IntervalKey{"today", "Today"}
|
||||||
IntervalYesterday = &IntervalKey{"day", "yesterday", "Yesterday"}
|
IntervalYesterday = &IntervalKey{"day", "yesterday", "Yesterday"}
|
||||||
|
IntervalPastDay = &IntervalKey{"24_hours", "last_24_hours", "last_day", "Last 24 Hours"} // non-official one
|
||||||
IntervalThisWeek = &IntervalKey{"week", "This Week"}
|
IntervalThisWeek = &IntervalKey{"week", "This Week"}
|
||||||
IntervalLastWeek = &IntervalKey{"Last Week"}
|
IntervalLastWeek = &IntervalKey{"last_week", "Last Week"}
|
||||||
IntervalThisMonth = &IntervalKey{"month", "This Month"}
|
IntervalThisMonth = &IntervalKey{"month", "This Month"}
|
||||||
IntervalLastMonth = &IntervalKey{"Last Month"}
|
IntervalLastMonth = &IntervalKey{"last_month", "Last Month"}
|
||||||
IntervalThisYear = &IntervalKey{"year"}
|
IntervalThisYear = &IntervalKey{"year", "This Year"}
|
||||||
IntervalPast7Days = &IntervalKey{"7_days", "last_7_days", "Last 7 Days"}
|
IntervalPast7Days = &IntervalKey{"7_days", "last_7_days", "Last 7 Days"}
|
||||||
IntervalPast7DaysYesterday = &IntervalKey{"Last 7 Days from Yesterday"}
|
IntervalPast7DaysYesterday = &IntervalKey{"Last 7 Days from Yesterday"}
|
||||||
IntervalPast14Days = &IntervalKey{"Last 14 Days"}
|
IntervalPast14Days = &IntervalKey{"14_days", "last_14_days", "Last 14 Days"}
|
||||||
IntervalPast30Days = &IntervalKey{"30_days", "last_30_days", "Last 30 Days"}
|
IntervalPast30Days = &IntervalKey{"30_days", "last_30_days", "Last 30 Days"}
|
||||||
IntervalPast6Months = &IntervalKey{"6_months", "last_6_months"}
|
IntervalPast6Months = &IntervalKey{"6_months", "last_6_months", "Last 6 Months"}
|
||||||
IntervalPast12Months = &IntervalKey{"12_months", "last_12_months", "last_year"}
|
IntervalPast12Months = &IntervalKey{"12_months", "last_12_months", "last_year", "Last 12 Months"}
|
||||||
IntervalAny = &IntervalKey{"any", "all_time"}
|
IntervalAny = &IntervalKey{"any", "all_time", "All Time"}
|
||||||
)
|
)
|
||||||
|
|
||||||
var AllIntervals = []*IntervalKey{
|
var AllIntervals = []*IntervalKey{
|
||||||
IntervalToday,
|
IntervalToday,
|
||||||
IntervalYesterday,
|
IntervalYesterday,
|
||||||
|
IntervalPastDay,
|
||||||
IntervalThisWeek,
|
IntervalThisWeek,
|
||||||
IntervalLastWeek,
|
IntervalLastWeek,
|
||||||
IntervalThisMonth,
|
IntervalThisMonth,
|
||||||
@ -46,3 +52,12 @@ func (k *IntervalKey) HasAlias(s string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (k *IntervalKey) GetHumanReadable() string {
|
||||||
|
for _, s := range *k {
|
||||||
|
if unicode.IsUpper(rune(s[0])) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
@ -163,6 +163,10 @@ func (u *User) MinDataAge() time.Time {
|
|||||||
return time.Now().AddDate(0, -retentionMonths, 0)
|
return time.Now().AddDate(0, -retentionMonths, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) AnyDataShared() bool {
|
||||||
|
return u.ShareDataMaxDays != 0 && (u.ShareEditors || u.ShareLanguages || u.ShareProjects || u.ShareOSs || u.ShareMachines || u.ShareLabels)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *CredentialsReset) IsValid() bool {
|
func (c *CredentialsReset) IsValid() bool {
|
||||||
return ValidatePassword(c.PasswordNew) &&
|
return ValidatePassword(c.PasswordNew) &&
|
||||||
c.PasswordNew == c.PasswordRepeat
|
c.PasswordNew == c.PasswordRepeat
|
||||||
|
@ -76,8 +76,14 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if no range was requested, get the maximum allowed range given the users max shared days, otherwise default to past 7 days (which will fail in the next step, because user didn't allow any sharing)
|
||||||
|
// this "floors" the user's maximum shared date to the supported range buckets (e.g. if user opted to share 12 days, we'll still fallback to "last_7_days") for consistency with wakatime
|
||||||
if rangeParam == "" {
|
if rangeParam == "" {
|
||||||
rangeParam = (*models.IntervalPast7Days)[0]
|
if _, userRange := helpers.ResolveMaximumRange(requestedUser.ShareDataMaxDays); userRange != nil {
|
||||||
|
rangeParam = (*userRange)[1]
|
||||||
|
} else {
|
||||||
|
rangeParam = (*models.IntervalPast7Days)[1]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err, rangeFrom, rangeTo := helpers.ResolveIntervalRawTZ(rangeParam, requestedUser.TZ())
|
err, rangeFrom, rangeTo := helpers.ResolveIntervalRawTZ(rangeParam, requestedUser.TZ())
|
||||||
@ -103,6 +109,10 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stats := v1.NewStatsFrom(summary, &models.Filters{})
|
stats := v1.NewStatsFrom(summary, &models.Filters{})
|
||||||
|
stats.Data.Range = rangeParam
|
||||||
|
stats.Data.HumanReadableRange = helpers.MustParseInterval(rangeParam).GetHumanReadable()
|
||||||
|
stats.Data.IsCodingActivityVisible = requestedUser.ShareDataMaxDays != 0
|
||||||
|
stats.Data.IsOtherUsageVisible = requestedUser.AnyDataShared()
|
||||||
|
|
||||||
// post filter stats according to user's given sharing permissions
|
// post filter stats according to user's given sharing permissions
|
||||||
if !requestedUser.ShareEditors {
|
if !requestedUser.ShareEditors {
|
||||||
|
Loading…
Reference in New Issue
Block a user