From 06b3fdd17ca4293bc3fd654c2a584a6fd7ae3aff Mon Sep 17 00:00:00 2001
From: Roch D'Amour
Date: Sun, 25 Oct 2020 02:21:41 -0400
Subject: [PATCH 1/6] Improved Dockerfile and docker-compose for dev
---
Dockerfile | 9 ++++++---
docker-compose.yml | 25 +++++++++++++++++++++++++
2 files changed, 31 insertions(+), 3 deletions(-)
create mode 100644 docker-compose.yml
diff --git a/Dockerfile b/Dockerfile
index 91e8863..9619b1a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,12 @@
# Build Stage
FROM golang:1.13 AS build-env
-ADD . /src
-RUN cd /src && go build -o wakapi
+WORKDIR /src
+ADD ./go.mod .
+RUN go mod download
+ADD . .
+RUN go build -o wakapi
# Final Stage
@@ -45,4 +48,4 @@ ADD wait-for-it.sh .
VOLUME /data
-ENTRYPOINT ./wait-for-it.sh
\ No newline at end of file
+ENTRYPOINT ./wait-for-it.sh
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..36ce414
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,25 @@
+version: '3.7'
+
+services:
+ wakapi:
+ build: .
+ ports:
+ - 3000:3000
+ restart: always
+ environment:
+ WAKAPI_DB_TYPE: "postgres"
+ WAKAPI_DB_NAME: "wakapi"
+ WAKAPI_DB_USER: "wakapi"
+ WAKAPI_DB_PASSWORD: "asdfasdfasdf"
+ WAKAPI_DB_HOST: "db"
+ WAKAPI_DB_PORT: "5432"
+ ENV: "dev"
+
+ db:
+ image: postgres:12.3
+ ports:
+ - 5432:5432
+ environment:
+ POSTGRES_USER: "wakapi"
+ POSTGRES_PASSWORD: "asdfasdfasdf"
+ POSTGRES_DB: "wakapi"
From fdf2289f8e1b13b41564f983bcfa7fbdcd32dd54 Mon Sep 17 00:00:00 2001
From: Roch D'Amour
Date: Sun, 25 Oct 2020 02:22:10 -0400
Subject: [PATCH 2/6] MVP for custom rules support
---
main.go | 10 ++--
migrations/sqlite3/6_customrule_table.sql | 15 ++++++
models/customrule.go | 18 +++++++
models/heartbeat.go | 19 +++----
routes/heartbeat.go | 17 ++++--
routes/settings.go | 63 +++++++++++++++++++++--
services/custom_rule.go | 53 +++++++++++++++++++
services/summary.go | 14 ++---
static/assets/app.js | 4 +-
views/settings.tpl.html | 60 +++++++++++++++++++--
wait-for-it.sh | 6 +--
11 files changed, 240 insertions(+), 39 deletions(-)
create mode 100644 migrations/sqlite3/6_customrule_table.sql
create mode 100644 models/customrule.go
create mode 100644 services/custom_rule.go
diff --git a/main.go b/main.go
index 1683071..470fb8b 100644
--- a/main.go
+++ b/main.go
@@ -31,6 +31,7 @@ var (
aliasService *services.AliasService
heartbeatService *services.HeartbeatService
userService *services.UserService
+ customRuleService *services.CustomRuleService
summaryService *services.SummaryService
aggregationService *services.AggregationService
keyValueService *services.KeyValueService
@@ -74,7 +75,8 @@ func main() {
aliasService = services.NewAliasService(db)
heartbeatService = services.NewHeartbeatService(db)
userService = services.NewUserService(db)
- summaryService = services.NewSummaryService(db, heartbeatService, aliasService)
+ customRuleService = services.NewCustomRuleService(db)
+ summaryService = services.NewSummaryService(db, heartbeatService, aliasService, customRuleService)
aggregationService = services.NewAggregationService(db, userService, summaryService, heartbeatService)
keyValueService = services.NewKeyValueService(db)
@@ -88,10 +90,10 @@ func main() {
// TODO: move endpoint registration to the respective routes files
// Handlers
- heartbeatHandler := routes.NewHeartbeatHandler(heartbeatService)
summaryHandler := routes.NewSummaryHandler(summaryService)
healthHandler := routes.NewHealthHandler(db)
- settingsHandler := routes.NewSettingsHandler(userService)
+ heartbeatHandler := routes.NewHeartbeatHandler(heartbeatService, customRuleService)
+ settingsHandler := routes.NewSettingsHandler(userService, customRuleService)
publicHandler := routes.NewIndexHandler(userService, keyValueService)
wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(summaryService)
wakatimeV1SummariesHandler := wtV1Routes.NewSummariesHandler(summaryService)
@@ -136,6 +138,8 @@ func main() {
// Settings Routes
settingsRouter.Methods(http.MethodGet).HandlerFunc(settingsHandler.GetIndex)
settingsRouter.Path("/credentials").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostCredentials)
+ settingsRouter.Path("/customrules").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostCreateCustomRule)
+ settingsRouter.Path("/customrules/delete").Methods(http.MethodPost).HandlerFunc(settingsHandler.DeleteCustomRule)
settingsRouter.Path("/reset").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostResetApiKey)
settingsRouter.Path("/badges").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostToggleBadges)
diff --git a/migrations/sqlite3/6_customrule_table.sql b/migrations/sqlite3/6_customrule_table.sql
new file mode 100644
index 0000000..6977e6a
--- /dev/null
+++ b/migrations/sqlite3/6_customrule_table.sql
@@ -0,0 +1,15 @@
+-- +migrate Up
+-- SQL in section 'Up' is executed when this migration is applied
+
+create table custom_rules
+(
+ id integer primary key autoincrement,
+ user_id varchar(255) not null REFERENCES users (id) ON DELETE RESTRICT ON UPDATE RESTRICT,
+ extension varchar(255),
+ language varchar(255)
+);
+
+-- +migrate Down
+-- SQL section 'Down' is executed when this migration is rolled back
+
+DROP TABLE custom_rules;
diff --git a/models/customrule.go b/models/customrule.go
new file mode 100644
index 0000000..c97a633
--- /dev/null
+++ b/models/customrule.go
@@ -0,0 +1,18 @@
+package models
+
+
+type CustomRule struct {
+ ID uint `json:"id" gorm:"primary_key"`
+ User *User `json:"-" gorm:"not null"`
+ UserID string `json:"-" gorm:"not null; index:idx_customrule_user"`
+ Extension string `json:"extension"`
+ Language string `json:"language"`
+}
+
+func validateLanguage(language string) bool {
+ return len(language) >= 1
+}
+
+func validateExtension(extension string) bool {
+ return len(extension) >= 2
+}
diff --git a/models/heartbeat.go b/models/heartbeat.go
index 8c4088f..bc17624 100644
--- a/models/heartbeat.go
+++ b/models/heartbeat.go
@@ -1,6 +1,7 @@
package models
import (
+ "fmt"
"regexp"
"time"
)
@@ -27,19 +28,13 @@ func (h *Heartbeat) Valid() bool {
return h.User != nil && h.UserID != "" && h.Time != CustomTime(time.Time{})
}
-func (h *Heartbeat) Augment(customLangs map[string]string) {
- if h.Language == "" {
- if h.languageRegex == nil {
- h.languageRegex = regexp.MustCompile(`^.+\.(.+)$`)
- }
- groups := h.languageRegex.FindAllStringSubmatch(h.Entity, -1)
- if len(groups) == 0 || len(groups[0]) != 2 {
+func (h *Heartbeat) Augment(customRules []*CustomRule) {
+ for _, lang := range customRules {
+ reg := fmt.Sprintf(".*%s$", lang.Extension)
+ match, err := regexp.MatchString(reg, h.Entity)
+ if match && err == nil {
+ h.Language = lang.Language
return
}
- ending := groups[0][1]
- if _, ok := customLangs[ending]; !ok {
- return
- }
- h.Language, _ = customLangs[ending]
}
}
diff --git a/routes/heartbeat.go b/routes/heartbeat.go
index 5a08cb0..5c52e8e 100644
--- a/routes/heartbeat.go
+++ b/routes/heartbeat.go
@@ -13,14 +13,16 @@ import (
)
type HeartbeatHandler struct {
- config *config2.Config
- heartbeatSrvc *services.HeartbeatService
+ config *config2.Config
+ heartbeatSrvc *services.HeartbeatService
+ customRuleSrvc *services.CustomRuleService
}
-func NewHeartbeatHandler(heartbeatService *services.HeartbeatService) *HeartbeatHandler {
+func NewHeartbeatHandler(heartbeatService *services.HeartbeatService, customRuleService *services.CustomRuleService) *HeartbeatHandler {
return &HeartbeatHandler{
config: config2.Get(),
heartbeatSrvc: heartbeatService,
+ customRuleSrvc: customRuleService,
}
}
@@ -41,13 +43,20 @@ func (h *HeartbeatHandler) ApiPost(w http.ResponseWriter, r *http.Request) {
return
}
+ rules, err := h.customRuleSrvc.GetCustomRuleForUser(user.ID)
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte(err.Error()))
+ return
+ }
+
for _, hb := range heartbeats {
hb.OperatingSystem = opSys
hb.Editor = editor
hb.Machine = machineName
hb.User = user
hb.UserID = user.ID
- hb.Augment(h.config.App.CustomLanguages)
+ hb.Augment(rules)
if !hb.Valid() {
w.WriteHeader(http.StatusBadRequest)
diff --git a/routes/settings.go b/routes/settings.go
index 7993e35..cc61d4d 100644
--- a/routes/settings.go
+++ b/routes/settings.go
@@ -2,6 +2,7 @@ package routes
import (
"fmt"
+ "strconv"
"github.com/gorilla/schema"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
@@ -14,13 +15,15 @@ import (
type SettingsHandler struct {
config *conf.Config
userSrvc *services.UserService
+ customRuleSrvc *services.CustomRuleService
}
var credentialsDecoder = schema.NewDecoder()
-func NewSettingsHandler(userService *services.UserService) *SettingsHandler {
+func NewSettingsHandler(userService *services.UserService, customRuleService *services.CustomRuleService) *SettingsHandler {
return &SettingsHandler{
config: conf.Get(),
+ customRuleSrvc: customRuleService,
userSrvc: userService,
}
}
@@ -31,14 +34,14 @@ func (h *SettingsHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
}
user := r.Context().Value(models.UserKey).(*models.User)
+ rules, _ := h.customRuleSrvc.GetCustomRuleForUser(user.ID)
data := map[string]interface{}{
"User": user,
+ "Rules": rules,
+ "Success": r.FormValue("success"),
+ "Error": r.FormValue("error"),
}
- // TODO: when alerts are present, other data will not be passed to the template
- if handleAlerts(w, r, conf.SettingsTemplate) {
- return
- }
templates[conf.SettingsTemplate].Execute(w, data)
}
@@ -105,6 +108,56 @@ func (h *SettingsHandler) PostCredentials(w http.ResponseWriter, r *http.Request
http.Redirect(w, r, fmt.Sprintf("%s/settings?success=%s", h.config.Server.BasePath, msg), http.StatusFound)
}
+func (h *SettingsHandler) DeleteCustomRule(w http.ResponseWriter, r *http.Request) {
+ if h.config.IsDev() {
+ loadTemplates()
+ }
+
+ user := r.Context().Value(models.UserKey).(*models.User)
+ ruleId, err := strconv.Atoi(r.PostFormValue("ruleid"))
+ if err != nil {
+ respondAlert(w, "internal server error", "", "settings.tpl.html", http.StatusInternalServerError)
+ return
+ }
+
+ rule := &models.CustomRule{
+ ID: uint(ruleId),
+ UserID: user.ID,
+ };
+ h.customRuleSrvc.Delete(rule)
+ msg := url.QueryEscape("Custom rule deleted successfully.");
+
+ http.Redirect(w, r, fmt.Sprintf("%s/settings?success=%s", h.config.Server.BasePath, msg), http.StatusFound)
+}
+
+func (h *SettingsHandler) PostCreateCustomRule(w http.ResponseWriter, r *http.Request) {
+ if h.config.IsDev() {
+ loadTemplates()
+ }
+ user := r.Context().Value(models.UserKey).(*models.User)
+ extension := r.PostFormValue("extension")
+ language := r.PostFormValue("language")
+
+ if extension[0] == '.' {
+ extension = extension[1:]
+ }
+
+ rule := &models.CustomRule{
+ UserID: user.ID,
+ Extension: extension,
+ Language: language,
+ };
+
+ if _, err := h.customRuleSrvc.Create(rule); err != nil {
+ respondAlert(w, "internal server error", "", "settings.tpl.html", http.StatusInternalServerError)
+ return
+ }
+
+ msg := url.QueryEscape("Custom rule saved successfully.");
+
+ http.Redirect(w, r, fmt.Sprintf("%s/settings?success=%s", h.config.Server.BasePath, msg), http.StatusFound)
+}
+
func (h *SettingsHandler) PostResetApiKey(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
diff --git a/services/custom_rule.go b/services/custom_rule.go
new file mode 100644
index 0000000..6926b51
--- /dev/null
+++ b/services/custom_rule.go
@@ -0,0 +1,53 @@
+package services
+
+import (
+ "github.com/jinzhu/gorm"
+ "github.com/muety/wakapi/models"
+)
+
+type CustomRuleService struct {
+ Config *models.Config
+ Db *gorm.DB
+}
+
+func NewCustomRuleService(db *gorm.DB) *CustomRuleService {
+ return &CustomRuleService{
+ Config: models.GetConfig(),
+ Db: db,
+ }
+}
+
+func (srv *CustomRuleService) GetCustomRuleById(CustomRuleId uint) (*models.CustomRule, error) {
+ r := &models.CustomRule{}
+ if err := srv.Db.Where(&models.CustomRule{ID: CustomRuleId}).First(r).Error; err != nil {
+ return r, err
+ }
+ return r, nil
+}
+
+func (srv *CustomRuleService) GetCustomRuleForUser(userId string) ([]*models.CustomRule, error) {
+ var rules []*models.CustomRule
+ if err := srv.Db.
+ Where(&models.CustomRule{UserID: userId}).
+ Find(&rules).Error; err != nil {
+ return rules, err
+ }
+
+ return rules, nil
+}
+
+func (srv *CustomRuleService) Create(rule *models.CustomRule) (*models.CustomRule, error) {
+ result := srv.Db.Create(rule)
+ if err := result.Error; err != nil {
+ return nil, err
+ }
+
+ return rule, nil
+}
+
+func (srv *CustomRuleService) Delete(rule *models.CustomRule) {
+ srv.Db.
+ Where("id = ?", rule.ID).
+ Where("user_id = ?", rule.UserID).
+ Delete(models.CustomRule{})
+}
diff --git a/services/summary.go b/services/summary.go
index a061812..2d6b99d 100644
--- a/services/summary.go
+++ b/services/summary.go
@@ -17,20 +17,22 @@ import (
const HeartbeatDiffThreshold = 2 * time.Minute
type SummaryService struct {
- Config *config.Config
- Cache *cache.Cache
- Db *gorm.DB
- HeartbeatService *HeartbeatService
- AliasService *AliasService
+ Config *config.Config
+ Cache *cache.Cache
+ Db *gorm.DB
+ HeartbeatService *HeartbeatService
+ AliasService *AliasService
+ CustomRuleService *CustomRuleService
}
-func NewSummaryService(db *gorm.DB, heartbeatService *HeartbeatService, aliasService *AliasService) *SummaryService {
+func NewSummaryService(db *gorm.DB, heartbeatService *HeartbeatService, aliasService *AliasService, customRuleService *CustomRuleService) *SummaryService {
return &SummaryService{
Config: config.Get(),
Cache: cache.New(24*time.Hour, 24*time.Hour),
Db: db,
HeartbeatService: heartbeatService,
AliasService: aliasService,
+ CustomRuleService: customRuleService,
}
}
diff --git a/static/assets/app.js b/static/assets/app.js
index 1fd2931..5a980de 100644
--- a/static/assets/app.js
+++ b/static/assets/app.js
@@ -203,7 +203,7 @@ function togglePlaceholders(mask) {
}
function getPresentDataMask() {
- return data.map(list => list.reduce((acc, e) => acc + e.total, 0) > 0)
+ return data.map(list => list ? list.reduce((acc, e) => acc + e.total, 0) : 0 > 0)
}
function getContainer(chart) {
@@ -303,4 +303,4 @@ window.addEventListener('load', function () {
setTopLabels()
togglePlaceholders(getPresentDataMask())
draw()
-})
\ No newline at end of file
+})
diff --git a/views/settings.tpl.html b/views/settings.tpl.html
index 869c793..511a9da 100644
--- a/views/settings.tpl.html
+++ b/views/settings.tpl.html
@@ -32,8 +32,7 @@
New Password
-
+ name="password_new" placeholder="Choose a password" minlength="6" required>
And again ...
+
+
+ Custom Rules
+
+
+ {{ if .Rules }}
+
+ {{ range $i, $rule := .Rules }}
+
+ When filename ends in:
+ {{ $rule.Extension }}
+ Change the language to:
+ {{ $rule.Language }}
+
+
+
+ {{end}}
+ {{else}}
+
+ No rules.
+
+ {{end}}
+
+
+
+
+
+
Badges
@@ -103,7 +154,8 @@
- You can also add /project:your-cool-project to the URL to filter by project.
+ <
+ zzp>You can also add /project:your-cool-project to the URL to filter by project.
{{ else }}
You have the ability to create badges from your coding statistics using Shields.io . To do so, you need to grant public, unauthorized access to the respective endpoint.
@@ -136,4 +188,4 @@
{{ template "foot.tpl.html" . }}