mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
feat: implement stats endpoint (resolve #114)
This commit is contained in:
parent
8ba3fdcaad
commit
9ff35b85d0
2
main.go
2
main.go
@ -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
|
||||||
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
78
models/compat/wakatime/v1/stats.go
Normal file
78
models/compat/wakatime/v1/stats.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
83
routes/compat/wakatime/v1/stats.go
Normal file
83
routes/compat/wakatime/v1/stats.go
Normal 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
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user