1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00

MVP for custom rules support

This commit is contained in:
Roch D'Amour 2020-10-25 02:22:10 -04:00
parent 06b3fdd17c
commit fdf2289f8e
11 changed files with 240 additions and 39 deletions

10
main.go
View File

@ -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)

View File

@ -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;

18
models/customrule.go Normal file
View File

@ -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
}

View File

@ -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]
}
}

View File

@ -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)

View File

@ -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()

53
services/custom_rule.go Normal file
View File

@ -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{})
}

View File

@ -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,
}
}

View File

@ -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()
})
})

View File

@ -32,8 +32,7 @@
<label class="inline-block text-sm mb-1 text-gray-500" for="password_new">New Password</label>
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
type="password" id="password_new"
name="password_new" placeholder="Choose a password" minlength="6" required>
</div>
name="password_new" placeholder="Choose a password" minlength="6" required> </div>
<div class="mb-8">
<label class="inline-block text-sm mb-1 text-gray-500" for="password_repeat">And again ...</label>
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
@ -66,6 +65,58 @@
</form>
</div>
<div class="w-full mt-4 mb-8 pb-8">
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
Custom Rules
</div>
{{ if .Rules }}
{{ range $i, $rule := .Rules }}
<div class="text-white border-1 w-full border-green-700 inline-block my-1 py-1 text-align">
<label class="inline-block text-sm mb-1 text-gray-500" >When filename ends in:</label>
{{ $rule.Extension }}
<label class="inline-block text-sm mb-1 text-gray-500" >Change the language to:</label>
{{ $rule.Language }}
<form class="float-right" action="settings/customrules/delete" method="post">
<input type="hidden" id="ruleid" name="ruleid" required value="{{ $rule.ID }}">
<button type="submit" class="py-1 px-3 rounded bg-red-500 hover:bg-red-600 text-white text-sm">
Remove
</button>
</form>
</div>
{{end}}
{{else}}
<div class="text-white border-1 w-full border-green-700 inline-block my-1 py-1">
No rules.
</div>
{{end}}
<form class="mt-10" action="settings/customrules" method="post">
<div class="flex justify-around mt-4">
<label class="inline-block text-sm mb-1 text-gray-500" for="extension">When filename ends in:</label>
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
type="text" id="extension"
name="extension" placeholder="py" minlength="1" required>
</div>
<div class="flex justify-around mt-4">
<label class="inline-block text-sm mb-1 text-gray-500" for="language">Change the language to:</label>
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
type="text" id="language"
name="language" placeholder="Python" minlength="1" required>
</div>
<div class="flex justify-between float-right">
<button type="submit" class="py-1 px-3 my-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">
Add rule
</button>
</div>
</form>
</div>
<div class="w-full mt-4 mb-8 pb-8">
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
Badges
@ -103,7 +154,8 @@
</div>
</div>
<p>You can also add <span class="text-xs bg-gray-900 rounded py-1 px-2 font-mono">/project:your-cool-project</span> to the URL to filter by project.</p>
<
zzp>You can also add <span class="text-xs bg-gray-900 rounded py-1 px-2 font-mono">/project:your-cool-project</span> to the URL to filter by project.</p>
{{ else }}
<p>You have the ability to create badges from your coding statistics using <a href="https://shields.io" target="_blank" class="border-b border-green-800" rel="noopener noreferrer">Shields.io</a>. To do so, you need to grant public, unauthorized access to the respective endpoint.</p>
<div class="flex justify-around mt-4">
@ -136,4 +188,4 @@
{{ template "foot.tpl.html" . }}
</body>
</html>
</html>

View File

@ -1,9 +1,9 @@
#!/bin/bash
if [ "$WAKAPI_DB_TYPE" != "sqlite3" ]; then
echo "Waiting 10 Seconds for DB to start"
sleep 10;
echo "Waiting 3 Seconds for DB to start"
sleep 3;
fi
echo "Starting Application"
./wakapi
./wakapi