unstable: initial tags

This commit is contained in:
ston1th 2019-08-20 23:57:43 +02:00
commit b3b62af3a4
28 changed files with 503 additions and 91 deletions

View file

@ -7,12 +7,16 @@ const (
TotpURI = "/totp"
LoginURI = "/login"
LogoutURI = "/logout"
SearchURI = "/search"
TagsURI = "/tags"
UserURI = "/user"
RawPrefix = "/raw"
IndexPrefix = "/index"
UploadPrefix = "/upload"
NewPrefix = "/new"
MovePrefix = "/move"
TagsPrefix = "/tags"
DeletePrefix = "/delete"
Favicon = "/favicon.ico"

View file

@ -14,12 +14,17 @@ type User struct {
type Result struct {
ID string
Path string
Keyword string
Tags string
HTML template.HTML
}
type Results []Result
type Tag struct {
Name string
Regex string
}
type Path struct {
Name string
Abs string

View file

@ -37,7 +37,14 @@ func New(cfg core.Config) (db *DB, err error) {
if err != nil && err != store.ErrKeyNotFound {
return
}
err = db.RunMigrations()
err = db.store.Get(versionKey, nil)
if err == store.ErrKeyNotFound {
err = db.addDefaultTags()
if err != nil {
return
}
}
err = db.runMigrations()
return
}

View file

@ -2,7 +2,12 @@
package db
import "crypto/sha256"
import (
"crypto/sha256"
"regexp"
)
var nameRe = regexp.MustCompile("[^a-zA-Z0-9]+")
// Parts taken from strings.TrimPrefix
func hasTrimPrefix(s, prefix string) (string, bool) {

View file

@ -49,24 +49,35 @@ func (db *DB) DeleteID(path string) error {
return db.store.Delete(idPrefix + path)
}
func (db *DB) NewFile(id, path string) (err error) {
func (db *DB) NewFile(id, path string, tags []string) (err error) {
err = db.store.Set(idPrefix+path, id)
if err != nil {
return
}
err = db.store.Set(pathPrefix+id, path)
if err != nil {
return
}
return db.SetTag(path, tags)
}
func (db *DB) MoveFile(op, np string) error {
func (db *DB) MoveFile(op, np string) (err error) {
if !db.IsIndexed(op) {
return nil
}
id, err := db.DeleteFile(op)
if err != nil {
return err
return
}
return db.NewFile(id, np)
tags, err := db.GetTag(op)
if err != nil {
return
}
err = db.NewFile(id, np, tags)
if err != nil {
return
}
return db.UnsetTag(op)
}
func (db *DB) DeleteFile(path string) (id string, err error) {
@ -81,6 +92,10 @@ func (db *DB) DeleteFile(path string) (id string, err error) {
if err != nil {
return
}
err = db.UnsetTag(path)
if err != nil {
return
}
err = db.DeletePath(id)
return
}

View file

@ -9,18 +9,25 @@ import (
const (
versionKey = "version/version"
currentVersion = 1
currentVersion = 2
)
type migrator func(*DB) (int, error)
type migrator func(*DB) error
var migrators = []migrator{
func(db *DB) (int, error) {
return 0, nil
func(db *DB) error {
return nil
},
func(db *DB) error {
// dummy placeholder
return nil
},
func(db *DB) error {
return db.addDefaultTags()
},
}
func (db *DB) RunMigrations() (err error) {
func (db *DB) runMigrations() (err error) {
var version int
err = db.store.Get(versionKey, &version)
if err == store.ErrKeyNotFound {
@ -37,11 +44,11 @@ func (db *DB) RunMigrations() (err error) {
for version < currentVersion {
v := version + 1
log.Printf("db: running migration %d", v)
version, err = migrators[v](db)
err = migrators[v](db)
if err != nil {
return
}
err = db.store.Set(versionKey, version)
err = db.store.Set(versionKey, v)
if err != nil {
return
}

69
pkg/db/tags.go Normal file
View file

@ -0,0 +1,69 @@
// Copyright (C) 2019 Marius Schellenberger
package db
import (
"git.giftfish.de/ston1th/docstore/pkg/core"
"regexp"
)
const (
tagsPrefix = "tags/"
tagPrefix = "tag/"
)
var defaultTags = []core.Tag{
{"invoice", `(?i)[^e]rechnung|quittung|beleg|invoice|bill|check`},
{"tax", `(?i)steuer|lohn|tax|salary`},
{"contract", `(?i)vertrag|kündigung|vereinbarung|contract|termination|agreement`},
{"insurance", `(?i)versicherung|versichert|insurance|insured`},
}
func (db *DB) addDefaultTags() (err error) {
for _, t := range defaultTags {
err = db.NewTag(t.Name, t.Regex)
if err != nil {
return
}
}
return
}
func (db *DB) GetAllTags() (tags []core.Tag, err error) {
err = db.store.ForEachPrefix(tagsPrefix, func(k string, _ []byte) error {
var t core.Tag
err := db.store.Get(tagsPrefix+k, &t)
if err == nil {
tags = append(tags, t)
}
return nil
})
return
}
func (db *DB) DeleteTag(name string) error {
return db.store.Delete(tagsPrefix + name)
}
func (db *DB) NewTag(name, regex string) (err error) {
_, err = regexp.Compile(regex)
if err != nil {
return
}
name = nameRe.ReplaceAllString(name, "")
err = db.store.Set(tagsPrefix+name, core.Tag{name, regex})
return
}
func (db *DB) GetTag(path string) (tags []string, err error) {
err = db.store.Get(tagPrefix+path, &tags)
return
}
func (db *DB) SetTag(path string, tags []string) error {
return db.store.Set(tagPrefix+path, tags)
}
func (db *DB) UnsetTag(path string) error {
return db.store.Delete(tagPrefix + path)
}

View file

@ -7,7 +7,6 @@ import (
"git.giftfish.de/ston1th/docstore/pkg/core"
"git.giftfish.de/ston1th/docstore/pkg/otp"
"golang.org/x/crypto/bcrypt"
"regexp"
"strings"
)
@ -20,8 +19,6 @@ var (
errWrongLogin = errors.New("db: wrong login")
errLocked = errors.New("db: user locked")
errUserExists = errors.New("db: user already exist")
userRe = regexp.MustCompile("[^a-zA-Z0-9]+")
)
func (db *DB) UserExists() (b bool) {
@ -53,7 +50,7 @@ func (db *DB) GetUser() (u core.User, err error) {
}
func (db *DB) CreateUser(username, password string) error {
username = userRe.ReplaceAllString(username, "")
username = nameRe.ReplaceAllString(username, "")
if db.UserExists() {
return errUserExists
}

View file

@ -24,6 +24,9 @@ var reserved = []string{
core.TotpURI,
core.LoginURI,
core.LogoutURI,
core.SearchURI,
core.TagsURI,
core.UserURI,
core.RawPrefix,
core.IndexPrefix,
core.UploadPrefix,

View file

@ -1,32 +0,0 @@
// Copyright (C) 2019 Marius Schellenberger
package index
import (
"regexp"
)
var classes = []class{
{regexp.MustCompile(`(?i)[^e]rechnung|quittung|beleg|invoice|bill|check`), []string{"invoice", "rechnung"}},
{regexp.MustCompile(`(?i)steuer|lohn|tax|salary`), []string{"tax", "steuer"}},
{regexp.MustCompile(`(?i)vertrag|kündigung|vereinbarung|contract|termination|agreement`), []string{"contract", "vertrag"}},
{regexp.MustCompile(`(?i)versicherung|versichert|insurance|insured`), []string{"insurance", "versicherung"}},
}
type class struct {
r *regexp.Regexp
k []string
}
func classify(text string) (keyword []string) {
keyword = []string{"", ""}
n := 0
for _, v := range classes {
num := len(v.r.FindAllString(text, -1))
if num > n && num >= 2 {
keyword = v.k
n = num
}
}
return
}

View file

@ -19,7 +19,7 @@ const docType = "document"
type document struct {
Text string `json:"text"`
Keyword []string `json:"keyword"`
Tags []string `json:"tags"`
Created time.Time `json:"created"`
}
@ -61,7 +61,7 @@ func (i *Index) Close() error {
return i.i.Close()
}
func (i *Index) Add(text string) (id string, err error) {
func (i *Index) Add(text string, tags []string) (id string, err error) {
id = newID()
for {
doc, _ := i.i.Document(id)
@ -70,8 +70,7 @@ func (i *Index) Add(text string) (id string, err error) {
}
id = newID()
}
k := classify(text)
err = i.i.Index(id, &document{text, k, time.Now()})
err = i.i.Index(id, &document{text, tags, time.Now()})
return
}
@ -97,12 +96,12 @@ func (i *Index) Search(search string) (results core.Results, err error) {
continue
}
for fragField, frags := range hit.Fragments {
if fragField == "keyword" {
var k string
if fragField == "tags" {
var t string
for _, f := range frags {
k += f
t += f
}
r.Keyword = strings.ReplaceAll(strings.ReplaceAll(k, "<mark>", ""), "</mark>", "")
r.Tags = strings.ReplaceAll(strings.ReplaceAll(t, "<mark>", ""), "</mark>", "")
}
if fragField == "text" || fragField == "created" {
for _, f := range frags {

View file

@ -7,6 +7,7 @@ import (
"git.giftfish.de/ston1th/docstore/pkg/fs"
"git.giftfish.de/ston1th/docstore/pkg/log"
"git.giftfish.de/ston1th/docstore/pkg/otp"
"git.giftfish.de/ston1th/docstore/pkg/tags"
"git.giftfish.de/ston1th/jwt/v3"
"html/template"
"net/http"
@ -197,12 +198,17 @@ func indexHandler(ctx *Context) {
log.Printf("scanner: %s: %s", path, err)
return
}
id, err := ctx.Srv.DB.Index.Add(txt)
t, err := ctx.Srv.DB.GetAllTags()
if err != nil {
log.Printf("getTags: %s: %s", path, err)
}
found := tags.Find(txt, t)
id, err := ctx.Srv.DB.Index.Add(txt, found)
if err != nil {
log.Printf("index: %s: %s", path, err)
return
}
err = ctx.Srv.DB.NewFile(id, file)
err = ctx.Srv.DB.NewFile(id, file, found)
if err != nil {
log.Printf("newFile: %s: %s", path, err)
}
@ -687,6 +693,94 @@ func userTotpHandler(ctx *Context) {
}
}
func tagsHandler(ctx *Context) {
ctx.Template("tagsHandler")
ctx.Data = webData{
Title: "Tags",
BodyTitle: "Tags",
}
t, err := ctx.Srv.DB.GetAllTags()
if err != nil {
ctx.Error(err)
return
}
ctx.Data.Data = t
switch ctx.Method() {
case "GET":
ctx.Exec()
}
}
func tagsEditHandler(ctx *Context) {
ctx.Template("tagsEditHandler")
ctx.Data = webData{
Title: "Edit User",
BodyTitle: "Edit User",
}
u, err := ctx.Srv.DB.GetUserWithoutPassword()
if err != nil {
ctx.Error(err)
return
}
ctx.Data.Data = u
switch ctx.Method() {
case "GET":
ctx.Exec()
case "POST":
if !ctx.CheckXsrf() {
return
}
password := ctx.Form("password")
repeat := ctx.Form("repeat")
if password != repeat {
ctx.Error("passwords do not match")
return
}
if err := ctx.Srv.DB.UpdateUserPassword(password); err != nil {
ctx.Error(err)
return
}
log.Printf("update: %s updated\n", ctx.User())
ctx.Redirect(core.IndexURI, http.StatusFound)
}
}
func tagsDelHandler(ctx *Context) {
ctx.Template("tagsDelHandler")
ctx.Data = webData{
Title: "Edit User",
BodyTitle: "Edit User",
}
u, err := ctx.Srv.DB.GetUserWithoutPassword()
if err != nil {
ctx.Error(err)
return
}
ctx.Data.Data = u
switch ctx.Method() {
case "GET":
ctx.Exec()
case "POST":
if !ctx.CheckXsrf() {
return
}
password := ctx.Form("password")
repeat := ctx.Form("repeat")
if password != repeat {
ctx.Error("passwords do not match")
return
}
if err := ctx.Srv.DB.UpdateUserPassword(password); err != nil {
ctx.Error(err)
return
}
log.Printf("update: %s updated\n", ctx.User())
ctx.Redirect(core.IndexURI, http.StatusFound)
}
}
func logoutHandler(ctx *Context) {
user := ctx.User()
if user == "" {

View file

@ -89,7 +89,7 @@ var prefixRoutes = []route{
var routes = append(static, []route{
{
"/search",
core.SearchURI,
jwtHandler(
authHandler(
searchHandler)),
@ -122,6 +122,27 @@ var routes = append(static, []route{
userTotpHandler)),
[]string{"GET", "POST"},
},
{
"/tags",
jwtHandler(
authHandler(
tagsHandler)),
[]string{"GET"},
},
{
"/tags/{tag:[a-zA-Z0-9]+}/edit",
jwtHandler(
authHandler(
tagsEditHandler)),
[]string{"GET", "POST"},
},
{
"/tags/{tag:[a-zA-Z0-9]+}/del",
jwtHandler(
authHandler(
tagsDelHandler)),
[]string{"GET", "POST"},
},
{
core.LogoutURI,
jwtHandler(

View file

@ -127,6 +127,7 @@ const (
<a href="/index{{.Data.Name}}" class="btn btn-sm btn-success">Index</a>
{{end}}
{{end}}
<a target="_blank" href="{{.Data.Abs}}" class="btn btn-sm btn-primary">Raw</a>
<a href="/move{{.Data.Name}}" class="btn btn-sm btn-primary">Move</a>
<a href="/delete{{.Data.Name}}" class="btn btn-sm btn-danger">Delete</a>
</div>
@ -159,7 +160,7 @@ const (
<title>DocStore | {{.Title}}</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" integrity="sha256-bgotk/IPq4Yvt6s8AQGPTM5ZjPjQ6rrBRa2EyxpnFSc="></link>
<link rel="stylesheet" type="text/css" href="/bootstrap.css" media="screen" integrity="sha256-3YeveuySvBgp2GItUS8eV1R/9T1zcPMyvw22SyVf8qo="></link>
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-VMvzVve7KY7ia4G8uBrASNEZ/Qc/5jCqVV3WWU+Y9ao="></link>
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-NLGEBzpF8t2UDEPzWyOYGGP/HWLNb29ar1MdYlwJroQ="></link>
<meta name="theme-color" content="#375a7f">
<meta name="description" content="{{.Title}}">
</head>
@ -265,6 +266,9 @@ const (
menu = `{{define "menu"}}
<ul class="navbar-nav">
<li class="nav-item"><a class="nav-link" href="/">Root</a></li>
{{if .Login}}
<li class="nav-item"><a class="nav-link" href="/tags">Tags</a></li>
{{end}}
</ul>
<form class="form-inline" method="post" action="/search">
<input type="hidden" name="token" value="{{.Token}}">
@ -426,6 +430,86 @@ const (
</tbody>
</table>
</div>
{{end}}`
tagsDel = `{{define "body"}}
<div class="row">
<div class="col-xs-4 offset-4">
<div class="card border-primary mx-auto">
<div class="card-header">
<strong>{{.BodyTitle}}</strong>
</div>
<div class="card-body">
<form class="form-horizontal" action="/tags/{{.Data.Name}}/edit" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<div class="form-group">
<label class="col-form-label" for="name">Name</label>
<input class="form-control input-sm" type="text" id="name" name="name" disabled>
</div>
<div class="form-group">
<label class="col-form-label" for="regex">Regex</label>
<input class="form-control input-sm" type="text" id="regex" name="regex" value="{{.Data.Regex}}">
</div>
<button class="btn btn-sm btn-primary" type="submit">Update</button>
<a href="/tags" class="btn btn-sm btn-primary">Back</a>
</form>
</div>
</div>
</div>
</div>
{{end}}`
tagsEdit = `{{define "body"}}
<div class="row">
<div class="col-xs-4 offset-4">
<div class="card border-primary mx-auto">
<div class="card-header">
<strong>{{.BodyTitle}}</strong>
</div>
<div class="card-body">
<form class="form-horizontal" action="/tags/{{.Data.Name}}/edit" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<div class="form-group">
<label class="col-form-label" for="name">Name</label>
<input class="form-control input-sm" type="text" id="name" name="name" disabled>
</div>
<div class="form-group">
<label class="col-form-label" for="regex">Regex</label>
<input class="form-control input-sm" type="text" id="regex" name="regex" value="{{.Data.Regex}}">
</div>
<button class="btn btn-sm btn-primary" type="submit">Update</button>
<a href="/tags" class="btn btn-sm btn-primary">Back</a>
</form>
</div>
</div>
</div>
</div>
{{end}}`
tagsList = `{{define "body"}}
<div class="row">
<table class="table table-stripped table-hover">
<thead>
<tr>
<th>Name</th>
<th>Regex</th>
<th>Options</th>
</tr>
</thead>
<tbody>
{{if .Data}}
{{range $item := .Data}}
{{$len := len $item.Regex}}
<tr>
<td>{{$item.Name}}</td>
<td><code>{{printf "%.50s" $item.Regex}}{{if gt $len 50}}...{{end}}</code></td>
<td><div class="btn-group">
<a href="/tags/{{$item.Name}}/edit" class="btn btn-sm btn-primary">Edit</a>
<a href="/tags/{{$item.Name}}/del" class="btn btn-sm btn-danger">Delete</a>
</div></td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</div>
{{end}}`
upload = `{{define "body"}}
<div class="row">
@ -579,7 +663,7 @@ a.anchor {
top: -70px;
visibility: hidden;
}
a.mono {
.mono {
font-family: monospace;
}
.alert-primary {
@ -744,6 +828,10 @@ func (s *HTTPServer) loadTemplates() {
s.templ["userEditHandler"] = parse(index, menu, userEdit)
// totp
s.templ["userTotpHandler"] = parse(index, menu, userTotp)
// tags
s.templ["tagsHandler"] = parse(index, menu, tagsList)
s.templ["tagsEditHandler"] = parse(index, menu, tagsEdit)
s.templ["tagsDelHandler"] = parse(index, menu, tagsDel)
if fatal {
log.Fatal("parse: ", errors.New("parsing templates failed"))

View file

@ -119,6 +119,15 @@ func (bs *BoltStore) ForEach(f func(string, []byte) error) error {
})
}
func (bs *BoltStore) ForEachPrefix(prefix string, f func(string, []byte) error) error {
return bs.ForEach(func(k string, v []byte) error {
if trim, ok := hasTrimPrefix(k, prefix); ok {
return f(trim, v)
}
return nil
})
}
func (bs *BoltStore) Delete(key string) error {
return bs.db.Update(func(tx *bolt.Tx) error {
return tx.Bucket([]byte(defaultBoltBucket)).Delete([]byte(key))

25
pkg/store/gob.go Normal file
View file

@ -0,0 +1,25 @@
// Copyright (C) 2019 Marius Schellenberger
package store
import (
"bytes"
"encoding/gob"
)
type gobMarshaler struct{}
func NewGOB() Marshaler {
return gobMarshaler{}
}
func (gobMarshaler) Marshal(v interface{}) (b []byte, err error) {
buf := new(bytes.Buffer)
err = gob.NewEncoder(buf).Encode(v)
b = buf.Bytes()
return
}
func (gobMarshaler) Unmarshal(data []byte, v interface{}) error {
return gob.NewDecoder(bytes.NewBuffer(data)).Decode(v)
}

11
pkg/store/helper.go Normal file
View file

@ -0,0 +1,11 @@
// Copyright (C) 2019 Marius Schellenberger
package store
// Parts taken from strings.TrimPrefix
func hasTrimPrefix(s, prefix string) (string, bool) {
if len(s) >= len(prefix) && s[0:len(prefix)] == prefix {
return s[len(prefix):], true
}
return s, false
}

View file

@ -2,29 +2,7 @@
package store
import (
"bytes"
"encoding/gob"
)
type Marshaler interface {
Marshal(v interface{}) ([]byte, error)
Unmarshal(data []byte, v interface{}) error
}
type gobMarshaler struct{}
func NewGOB() Marshaler {
return gobMarshaler{}
}
func (gobMarshaler) Marshal(v interface{}) (b []byte, err error) {
buf := new(bytes.Buffer)
err = gob.NewEncoder(buf).Encode(v)
b = buf.Bytes()
return
}
func (gobMarshaler) Unmarshal(data []byte, v interface{}) error {
return gob.NewDecoder(bytes.NewBuffer(data)).Decode(v)
}

View file

@ -17,6 +17,7 @@ type Store interface {
Get(string, interface{}) error
Set(string, interface{}) error
ForEach(func(string, []byte) error) error
ForEachPrefix(string, func(string, []byte) error) error
Delete(string) error
Close() error
}

18
pkg/tags/tags.go Normal file
View file

@ -0,0 +1,18 @@
// Copyright (C) 2019 Marius Schellenberger
package tags
import (
"git.giftfish.de/ston1th/docstore/pkg/core"
"regexp"
)
func Find(text string, tags []core.Tag) (found []string) {
for _, t := range tags {
re := regexp.MustCompile(t.Regex)
if re.MatchString(text) {
found = append(found, t.Name)
}
}
return
}

View file

@ -96,6 +96,10 @@ func (s *HTTPServer) loadTemplates() {
s.templ["userEditHandler"] = parse(index, menu, userEdit)
// totp
s.templ["userTotpHandler"] = parse(index, menu, userTotp)
// tags
s.templ["tagsHandler"] = parse(index, menu, tagsList)
s.templ["tagsEditHandler"] = parse(index, menu, tagsEdit)
s.templ["tagsDelHandler"] = parse(index, menu, tagsDel)
if fatal {
log.Fatal("parse: ", errors.New("parsing templates failed"))

View file

@ -41,7 +41,7 @@ a.anchor {
top: -70px;
visibility: hidden;
}
a.mono {
.mono {
font-family: monospace;
}
.alert-primary {

View file

@ -16,6 +16,7 @@
<a href="/index{{.Data.Name}}" class="btn btn-sm btn-success">Index</a>
{{end}}
{{end}}
<a target="_blank" href="{{.Data.Abs}}" class="btn btn-sm btn-primary">Raw</a>
<a href="/move{{.Data.Name}}" class="btn btn-sm btn-primary">Move</a>
<a href="/delete{{.Data.Name}}" class="btn btn-sm btn-danger">Delete</a>
</div>

View file

@ -4,7 +4,7 @@
<title>DocStore | {{.Title}}</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" integrity="sha256-bgotk/IPq4Yvt6s8AQGPTM5ZjPjQ6rrBRa2EyxpnFSc="></link>
<link rel="stylesheet" type="text/css" href="/bootstrap.css" media="screen" integrity="sha256-3YeveuySvBgp2GItUS8eV1R/9T1zcPMyvw22SyVf8qo="></link>
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-VMvzVve7KY7ia4G8uBrASNEZ/Qc/5jCqVV3WWU+Y9ao="></link>
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-NLGEBzpF8t2UDEPzWyOYGGP/HWLNb29ar1MdYlwJroQ="></link>
<meta name="theme-color" content="#375a7f">
<meta name="description" content="{{.Title}}">
</head>

View file

@ -1,6 +1,9 @@
{{define "menu"}}
<ul class="navbar-nav">
<li class="nav-item"><a class="nav-link" href="/">Root</a></li>
{{if .Login}}
<li class="nav-item"><a class="nav-link" href="/tags">Tags</a></li>
{{end}}
</ul>
<form class="form-inline" method="post" action="/search">
<input type="hidden" name="token" value="{{.Token}}">

26
templates/tagsDel.html Normal file
View file

@ -0,0 +1,26 @@
{{define "body"}}
<div class="row">
<div class="col-xs-4 offset-4">
<div class="card border-primary mx-auto">
<div class="card-header">
<strong>{{.BodyTitle}}</strong>
</div>
<div class="card-body">
<form class="form-horizontal" action="/tags/{{.Data.Name}}/edit" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<div class="form-group">
<label class="col-form-label" for="name">Name</label>
<input class="form-control input-sm" type="text" id="name" name="name" disabled>
</div>
<div class="form-group">
<label class="col-form-label" for="regex">Regex</label>
<input class="form-control input-sm" type="text" id="regex" name="regex" value="{{.Data.Regex}}">
</div>
<button class="btn btn-sm btn-primary" type="submit">Update</button>
<a href="/tags" class="btn btn-sm btn-primary">Back</a>
</form>
</div>
</div>
</div>
</div>
{{end}}

26
templates/tagsEdit.html Normal file
View file

@ -0,0 +1,26 @@
{{define "body"}}
<div class="row">
<div class="col-xs-4 offset-4">
<div class="card border-primary mx-auto">
<div class="card-header">
<strong>{{.BodyTitle}}</strong>
</div>
<div class="card-body">
<form class="form-horizontal" action="/tags/{{.Data.Name}}/edit" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<div class="form-group">
<label class="col-form-label" for="name">Name</label>
<input class="form-control input-sm" type="text" id="name" name="name" disabled>
</div>
<div class="form-group">
<label class="col-form-label" for="regex">Regex</label>
<input class="form-control input-sm" type="text" id="regex" name="regex" value="{{.Data.Regex}}">
</div>
<button class="btn btn-sm btn-primary" type="submit">Update</button>
<a href="/tags" class="btn btn-sm btn-primary">Back</a>
</form>
</div>
</div>
</div>
</div>
{{end}}

28
templates/tagsList.html Normal file
View file

@ -0,0 +1,28 @@
{{define "body"}}
<div class="row">
<table class="table table-stripped table-hover">
<thead>
<tr>
<th>Name</th>
<th>Regex</th>
<th>Options</th>
</tr>
</thead>
<tbody>
{{if .Data}}
{{range $item := .Data}}
{{$len := len $item.Regex}}
<tr>
<td>{{$item.Name}}</td>
<td><code>{{printf "%.50s" $item.Regex}}{{if gt $len 50}}...{{end}}</code></td>
<td><div class="btn-group">
<a href="/tags/{{$item.Name}}/edit" class="btn btn-sm btn-primary">Edit</a>
<a href="/tags/{{$item.Name}}/del" class="btn btn-sm btn-danger">Delete</a>
</div></td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</div>
{{end}}