feat: implement stats endpoint (resolve #114)

This commit is contained in:
Ferdinand Mütsch 2021-02-06 16:05:34 +01:00
parent 8ba3fdcaad
commit 9ff35b85d0
8 changed files with 188 additions and 25 deletions

View File

@ -132,6 +132,7 @@ func main() {
// Compat Handlers // Compat Handlers
wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(summaryService) wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(summaryService)
wakatimeV1SummariesHandler := wtV1Routes.NewSummariesHandler(summaryService) wakatimeV1SummariesHandler := wtV1Routes.NewSummariesHandler(summaryService)
wakatimeV1StatsHandler := wtV1Routes.NewStatsHandler(summaryService)
shieldV1BadgeHandler := shieldsV1Routes.NewBadgeHandler(summaryService, userService) shieldV1BadgeHandler := shieldsV1Routes.NewBadgeHandler(summaryService, userService)
// MVC Handlers // MVC Handlers
@ -172,6 +173,7 @@ func main() {
// Compat route registrations // Compat route registrations
wakatimeV1AllHandler.RegisterRoutes(compatApiRouter) wakatimeV1AllHandler.RegisterRoutes(compatApiRouter)
wakatimeV1SummariesHandler.RegisterRoutes(compatApiRouter) wakatimeV1SummariesHandler.RegisterRoutes(compatApiRouter)
wakatimeV1StatsHandler.RegisterRoutes(compatApiRouter)
shieldV1BadgeHandler.RegisterRoutes(compatApiRouter) shieldV1BadgeHandler.RegisterRoutes(compatApiRouter)
// Static Routes // Static Routes

View File

@ -10,16 +10,16 @@ type HeartbeatsViewModel struct {
// that is actually required for the import // that is actually required for the import
type HeartbeatEntry struct { type HeartbeatEntry struct {
Id string Id string `json:"id"`
Branch string Branch string `json:"branch"`
Category string Category string `json:"category"`
Entity string Entity string `json:"entity"`
IsWrite bool `json:"is_write"` IsWrite bool `json:"is_write"`
Language string Language string `json:"language"`
Project string Project string `json:"project"`
Time models.CustomTime Time models.CustomTime `json:"time"`
Type string Type string `json:"type"`
UserId string `json:"user_id"` UserId string `json:"user_id"`
MachineNameId string `json:"machine_name_id"` MachineNameId string `json:"machine_name_id"`
UserAgentId string `json:"user_agent_id"` UserAgentId string `json:"user_agent_id"`
} }

View File

@ -0,0 +1,78 @@
package v1
import (
"github.com/muety/wakapi/models"
"time"
)
// https://wakatime.com/api/v1/users/current/stats/last_7_days
// https://pastr.de/p/f2fxg6ragj7z5e7fhsow9rb6
type StatsViewModel struct {
Data *StatsData `json:"data"`
}
type StatsData struct {
Username string `json:"username"`
UserId string `json:"user_id"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
TotalSeconds float64 `json:"total_seconds"`
DailyAverage float64 `json:"daily_average"`
DaysIncludingHolidays int `json:"days_including_holidays"`
Editors []*SummariesEntry `json:"editors"`
Languages []*SummariesEntry `json:"languages"`
Machines []*SummariesEntry `json:"machines"`
Projects []*SummariesEntry `json:"projects"`
OperatingSystems []*SummariesEntry `json:"operating_systems"`
}
func NewStatsFrom(summary *models.Summary, filters *models.Filters) *StatsViewModel {
totalTime := summary.TotalTime()
numDays := int(summary.ToTime.T().Sub(summary.FromTime.T()).Hours() / 24)
data := &StatsData{
Username: summary.UserID,
UserId: summary.UserID,
Start: summary.FromTime.T(),
End: summary.ToTime.T(),
TotalSeconds: totalTime.Seconds(),
DailyAverage: totalTime.Seconds() / float64(numDays),
DaysIncludingHolidays: numDays,
}
editors := make([]*SummariesEntry, len(summary.Editors))
for i, e := range summary.Editors {
editors[i] = convertEntry(e, summary.TotalTimeBy(models.SummaryEditor))
}
languages := make([]*SummariesEntry, len(summary.Languages))
for i, e := range summary.Languages {
languages[i] = convertEntry(e, summary.TotalTimeBy(models.SummaryLanguage))
}
machines := make([]*SummariesEntry, len(summary.Machines))
for i, e := range summary.Machines {
machines[i] = convertEntry(e, summary.TotalTimeBy(models.SummaryMachine))
}
projects := make([]*SummariesEntry, len(summary.Projects))
for i, e := range summary.Projects {
projects[i] = convertEntry(e, summary.TotalTimeBy(models.SummaryProject))
}
oss := make([]*SummariesEntry, len(summary.OperatingSystems))
for i, e := range summary.OperatingSystems {
oss[i] = convertEntry(e, summary.TotalTimeBy(models.SummaryOS))
}
data.Editors = editors
data.Languages = languages
data.Machines = machines
data.Projects = projects
data.OperatingSystems = oss
return &StatsViewModel{
Data: data,
}
}

View File

@ -5,8 +5,8 @@ type UserAgentsViewModel struct {
} }
type UserAgentEntry struct { type UserAgentEntry struct {
Id string Id string `json:"id"`
Editor string Editor string `json:"editor"`
Os string Os string `json:"os"`
Value string Value string `json:"value"`
} }

View File

@ -16,13 +16,13 @@ const (
const ( const (
IntervalToday string = "today" IntervalToday string = "today"
IntervalYesterday string = "day" IntervalYesterday string = "yesterday"
IntervalThisWeek string = "week" IntervalThisWeek string = "week"
IntervalThisMonth string = "month" IntervalThisMonth string = "month"
IntervalThisYear string = "year" IntervalThisYear string = "year"
IntervalPast7Days string = "7_days" IntervalPast7Days string = "last_7_days"
IntervalPast30Days string = "30_days" IntervalPast30Days string = "last_30_days"
IntervalPast12Months string = "12_months" IntervalPast12Months string = "last_12_months"
IntervalAny string = "any" IntervalAny string = "any"
// https://wakatime.com/developers/#summaries // https://wakatime.com/developers/#summaries

View File

@ -0,0 +1,83 @@
package v1
import (
"errors"
"github.com/gorilla/mux"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
"net/http"
"time"
)
type StatsHandler struct {
config *conf.Config
summarySrvc services.ISummaryService
}
func NewStatsHandler(summaryService services.ISummaryService) *StatsHandler {
return &StatsHandler{
summarySrvc: summaryService,
config: conf.Get(),
}
}
func (h *StatsHandler) RegisterRoutes(router *mux.Router) {
router.Path("/wakatime/v1/users/{user}/stats/{range}").Methods(http.MethodGet).HandlerFunc(h.Get)
}
// TODO: support filtering (requires https://github.com/muety/wakapi/issues/108)
func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
requestedUser := vars["user"]
requestedRange := vars["range"]
user := r.Context().Value(models.UserKey).(*models.User)
if requestedUser != user.ID && requestedUser != "current" {
w.WriteHeader(http.StatusForbidden)
return
}
summary, err, status := h.loadUserSummary(user, requestedRange)
if err != nil {
w.WriteHeader(status)
w.Write([]byte(err.Error()))
return
}
filters := &models.Filters{}
if projectQuery := r.URL.Query().Get("project"); projectQuery != "" {
filters.Project = projectQuery
}
vm := v1.NewStatsFrom(summary, filters)
utils.RespondJSON(w, http.StatusOK, vm)
}
func (h *StatsHandler) loadUserSummary(user *models.User, rangeKey string) (*models.Summary, error, int) {
var start, end time.Time
if err, parsedFrom, parsedTo := utils.ResolveInterval(rangeKey); err == nil {
start, end = parsedFrom, parsedTo
} else {
return nil, errors.New("invalid 'range' parameter"), http.StatusBadRequest
}
overallParams := &models.SummaryParams{
From: start,
To: end,
User: user,
Recompute: false,
}
summary, err := h.summarySrvc.Aliased(overallParams.From, overallParams.To, user, h.summarySrvc.Retrieve)
if err != nil {
return nil, err, http.StatusInternalServerError
}
return summary, nil, http.StatusOK
}

View File

@ -22,7 +22,7 @@
class="text-green-700">Your</span> Coding Time 🕓</h1> class="text-green-700">Your</span> Coding Time 🕓</h1>
<p class="text-center text-gray-500 text-xl my-2">Wakapi is an open-source tool that helps you keep track of the <p class="text-center text-gray-500 text-xl my-2">Wakapi is an open-source tool that helps you keep track of the
time you have spent coding on different projects in different programming languages and more. Ideal for time you have spent coding on different projects in different programming languages and more. Ideal for
statistics freaks any anyone else.</p> statistics freaks and anyone else.</p>
<p class="text-center text-gray-500 text-xl my-4"> <p class="text-center text-gray-500 text-xl my-4">
<span class="mr-1">💡 The system has tracked a total of </span> <span class="mr-1">💡 The system has tracked a total of </span>

View File

@ -41,13 +41,13 @@
<div class="text-white text-sm flex items-center justify-center mt-4 self-center max-w-lg flex-wrap"> <div class="text-white text-sm flex items-center justify-center mt-4 self-center max-w-lg flex-wrap">
<a href="summary?interval=today" class="mx-2 my-1 border-b border-green-700">Today</a> <a href="summary?interval=today" class="mx-2 my-1 border-b border-green-700">Today</a>
<a href="summary?interval=day" class="mx-2 my-1 border-b border-green-700">Yesterday</a> <a href="summary?interval=yesterday" class="mx-2 my-1 border-b border-green-700">Yesterday</a>
<a href="summary?interval=week" class="mx-2 my-1 border-b border-green-700">This Week</a> <a href="summary?interval=week" class="mx-2 my-1 border-b border-green-700">This Week</a>
<a href="summary?interval=month" class="mx-2 my-1 border-b border-green-700">This Month</a> <a href="summary?interval=month" class="mx-2 my-1 border-b border-green-700">This Month</a>
<a href="summary?interval=year" class="mx-2 my-1 border-b border-green-700">This Year</a> <a href="summary?interval=year" class="mx-2 my-1 border-b border-green-700">This Year</a>
<a href="summary?interval=7_days" class="mx-2 my-1 border-b border-green-700">Past 7 Days</a> <a href="summary?interval=last_7_days" class="mx-2 my-1 border-b border-green-700">Past 7 Days</a>
<a href="summary?interval=30_days" class="mx-2 my-1 border-b border-green-700">Past 30 Days</a> <a href="summary?interval=last_30_days" class="mx-2 my-1 border-b border-green-700">Past 30 Days</a>
<a href="summary?interval=12_months" class="mx-2 my-1 border-b border-green-700">Past 12 Months</a> <a href="summary?interval=last_12_months" class="mx-2 my-1 border-b border-green-700">Past 12 Months</a>
<a href="summary?interval=any" class="mx-2 my-1 border-b border-green-700">All Time</a> <a href="summary?interval=any" class="mx-2 my-1 border-b border-green-700">All Time</a>
</div> </div>