chore: added model delete related to #2
This commit is contained in:
parent
af62db6ad1
commit
b8278bacf6
@ -7,7 +7,7 @@ tmp_dir = "tmp"
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ."
|
||||
delay = 0
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testData", "savedData"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,4 @@
|
||||
tmp/
|
||||
testData/
|
||||
savedData/
|
||||
!savedData/.keep
|
||||
|
1
go.mod
1
go.mod
@ -3,6 +3,7 @@ module andr3h3nriqu3s.com/m
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
golang.org/x/crypto v0.13.0 // indirect
|
||||
)
|
||||
|
2
go.sum
2
go.sum
@ -1,3 +1,5 @@
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
|
373
handler.go
373
handler.go
@ -8,6 +8,8 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@ -78,11 +80,56 @@ func LoadHtml(writer http.ResponseWriter, path string, data interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func LoadError(writer http.ResponseWriter, path string, base string, data AnyMap) {
|
||||
if data == nil {
|
||||
data = map[string]interface{} {
|
||||
"Error": true,
|
||||
}
|
||||
} else {
|
||||
data["Error"] = true
|
||||
}
|
||||
|
||||
tmpl, err := template.New("").Parse("{{template \"" + base + "\" . }}")
|
||||
if err != nil {
|
||||
panic("Lol")
|
||||
}
|
||||
|
||||
tmpl, err = tmpl.ParseFiles(
|
||||
"./views/"+path,
|
||||
"./views/partials/header.html",
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to load template %s\n", path)
|
||||
fmt.Println(err)
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
if path == "500.html" {
|
||||
writer.Write([]byte("<h1>Failed to load 500.html check console for more info</h1>"))
|
||||
} else {
|
||||
LoadHtml(writer, "500.html", nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(writer, data); err != nil {
|
||||
fmt.Printf("Failed to execute template %s\n", path)
|
||||
fmt.Println(err)
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
if path == "500.html" {
|
||||
writer.Write([]byte("<h1>Failed to load 500.html check console for more info</h1>"))
|
||||
} else {
|
||||
LoadHtml(writer, "500.html", nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type AnyMap = map[string]interface{}
|
||||
|
||||
type Error struct {
|
||||
code int
|
||||
msg *string
|
||||
data AnyMap
|
||||
}
|
||||
|
||||
type AnswerType int
|
||||
@ -133,9 +180,10 @@ type Handler interface {
|
||||
}
|
||||
|
||||
type Handle struct {
|
||||
db *sql.DB
|
||||
gets []HandleFunc
|
||||
posts []HandleFunc
|
||||
db *sql.DB
|
||||
gets []HandleFunc
|
||||
posts []HandleFunc
|
||||
deletes []HandleFunc
|
||||
}
|
||||
|
||||
func decodeBody(r *http.Request) (string, *Error) {
|
||||
@ -149,9 +197,8 @@ func decodeBody(r *http.Request) (string, *Error) {
|
||||
|
||||
func handleError(err *Error, w http.ResponseWriter, context *Context) {
|
||||
|
||||
data := context.toMap()
|
||||
|
||||
if err != nil {
|
||||
data := context.addMap(err.data)
|
||||
w.WriteHeader(err.code)
|
||||
if err.code == http.StatusNotFound {
|
||||
LoadBasedOnAnswer(context.Mode, w, "404.html", data)
|
||||
@ -168,69 +215,39 @@ func handleError(err *Error, w http.ResponseWriter, context *Context) {
|
||||
}
|
||||
|
||||
func (x *Handle) Get(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||
nhandler :=
|
||||
HandleFunc{
|
||||
fn: fn,
|
||||
path: path,
|
||||
mode: NORMAL | HTML | HTMLFULL | JSON,
|
||||
}
|
||||
|
||||
x.gets = append(x.gets, nhandler)
|
||||
x.gets = append(x.gets, HandleFunc{path, NORMAL | HTML | HTMLFULL | JSON, fn})
|
||||
}
|
||||
|
||||
func (x *Handle) GetHTML(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||
nhandler :=
|
||||
HandleFunc{
|
||||
fn: fn,
|
||||
path: path,
|
||||
mode: NORMAL | HTML | HTMLFULL,
|
||||
}
|
||||
|
||||
x.gets = append(x.gets, nhandler)
|
||||
x.gets = append(x.gets, HandleFunc{path, NORMAL | HTML | HTMLFULL, fn})
|
||||
}
|
||||
|
||||
func (x *Handle) GetJSON(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||
nhandler :=
|
||||
HandleFunc{
|
||||
fn: fn,
|
||||
path: path,
|
||||
mode: JSON,
|
||||
}
|
||||
|
||||
x.gets = append(x.gets, nhandler)
|
||||
x.gets = append(x.gets, HandleFunc{path, JSON, fn})
|
||||
}
|
||||
|
||||
func (x *Handle) Post(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||
nhandler :=
|
||||
HandleFunc{
|
||||
fn: fn,
|
||||
path: path,
|
||||
mode: NORMAL | HTML | HTMLFULL | JSON,
|
||||
}
|
||||
|
||||
x.posts = append(x.posts, nhandler)
|
||||
x.posts = append(x.posts, HandleFunc{path, NORMAL | HTML | HTMLFULL | JSON, fn})
|
||||
}
|
||||
|
||||
func (x *Handle) PostHTML(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||
nhandler :=
|
||||
HandleFunc{
|
||||
fn: fn,
|
||||
path: path,
|
||||
mode: NORMAL | HTML | HTMLFULL,
|
||||
}
|
||||
|
||||
x.posts = append(x.posts, nhandler)
|
||||
x.posts = append(x.posts, HandleFunc{path, NORMAL | HTML | HTMLFULL, fn})
|
||||
}
|
||||
|
||||
func (x *Handle) PostJSON(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||
nhandler :=
|
||||
HandleFunc{
|
||||
fn: fn,
|
||||
path: path,
|
||||
mode: JSON,
|
||||
}
|
||||
x.posts = append(x.posts, HandleFunc{path, JSON, fn})
|
||||
}
|
||||
|
||||
x.posts = append(x.posts, nhandler)
|
||||
func (x *Handle) Delete(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||
x.deletes = append(x.deletes, HandleFunc{path, NORMAL | HTML | HTMLFULL | JSON, fn})
|
||||
}
|
||||
|
||||
func (x *Handle) DeleteHTML(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||
x.deletes = append(x.deletes, HandleFunc{path, NORMAL | HTML | HTMLFULL, fn})
|
||||
}
|
||||
|
||||
func (x *Handle) DeleteJSON(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||
x.deletes = append(x.deletes, HandleFunc{path, JSON, fn})
|
||||
}
|
||||
|
||||
func (x *Handle) handleGets(w http.ResponseWriter, r *http.Request, context *Context) {
|
||||
@ -243,9 +260,7 @@ func (x *Handle) handleGets(w http.ResponseWriter, r *http.Request, context *Con
|
||||
if context.Mode != HTMLFULL {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
LoadBasedOnAnswer(context.Mode, w, "404.html", map[string]interface{}{
|
||||
"context": context,
|
||||
})
|
||||
LoadBasedOnAnswer(context.Mode, w, "404.html", context.addMap(nil))
|
||||
}
|
||||
|
||||
func (x *Handle) handlePosts(w http.ResponseWriter, r *http.Request, context *Context) {
|
||||
@ -258,18 +273,42 @@ func (x *Handle) handlePosts(w http.ResponseWriter, r *http.Request, context *Co
|
||||
if context.Mode != HTMLFULL {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
LoadBasedOnAnswer(context.Mode, w, "404.html", map[string]interface{}{
|
||||
"context": context,
|
||||
})
|
||||
LoadBasedOnAnswer(context.Mode, w, "404.html", context.addMap(nil))
|
||||
}
|
||||
|
||||
func AnswerTemplate(path string, data AnyMap) func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||
func (x *Handle) handleDeletes(w http.ResponseWriter, r *http.Request, context *Context) {
|
||||
for _, s := range x.deletes {
|
||||
if s.path == r.URL.Path && context.Mode&s.mode != 0 {
|
||||
handleError(s.fn(w, r, context), w, context)
|
||||
return
|
||||
}
|
||||
}
|
||||
if context.Mode != HTMLFULL {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
LoadBasedOnAnswer(context.Mode, w, "404.html", context.addMap(nil))
|
||||
}
|
||||
|
||||
func checkAuthLevel(authLevel int, w http.ResponseWriter, r *http.Request, c *Context) bool {
|
||||
if authLevel > 0 {
|
||||
if c.requireAuth(w, r) {
|
||||
logoff(c.Mode, w, r)
|
||||
return false
|
||||
}
|
||||
if c.User.user_type < authLevel {
|
||||
notAuth(c.Mode, w, r)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func AnswerTemplate(path string, data AnyMap, authLevel int) func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||
return func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||
if data == nil {
|
||||
LoadBasedOnAnswer(c.Mode, w, path, c.toMap())
|
||||
} else {
|
||||
LoadBasedOnAnswer(c.Mode, w, path, c.addMap(data))
|
||||
}
|
||||
if !checkAuthLevel(authLevel, w, r, c) {
|
||||
return nil
|
||||
}
|
||||
LoadBasedOnAnswer(c.Mode, w, path, c.addMap(data))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -277,25 +316,24 @@ func AnswerTemplate(path string, data AnyMap) func(w http.ResponseWriter, r *htt
|
||||
type Context struct {
|
||||
Token *string
|
||||
User *User
|
||||
Mode AnswerType
|
||||
Mode AnswerType
|
||||
}
|
||||
|
||||
func (c Context) addMap(m AnyMap) AnyMap {
|
||||
m["Context"] = c;
|
||||
return m;
|
||||
}
|
||||
|
||||
func (c *Context) toMap() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"Context": c,
|
||||
if m == nil {
|
||||
return map[string]interface{}{
|
||||
"Context": c,
|
||||
}
|
||||
}
|
||||
m["Context"] = c
|
||||
return m
|
||||
}
|
||||
|
||||
func (c *Context) requireAuth(w http.ResponseWriter, r *http.Request) bool {
|
||||
if c.User == nil {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if c.User == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var LogoffError = errors.New("Invalid token!")
|
||||
@ -310,10 +348,12 @@ func (x Handle) createContext(mode AnswerType, r *http.Request) (*Context, error
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check that the token is still valid
|
||||
|
||||
if token == nil {
|
||||
return &Context{
|
||||
Mode: mode,
|
||||
}, nil
|
||||
Mode: mode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
user, err := userFromToken(x.db, *token)
|
||||
@ -324,35 +364,114 @@ func (x Handle) createContext(mode AnswerType, r *http.Request) (*Context, error
|
||||
return &Context{token, user, mode}, nil
|
||||
}
|
||||
|
||||
func logoff(mode AnswerType, w http.ResponseWriter, r *http.Request) {
|
||||
// Delete cookie
|
||||
cookie := &http.Cookie{
|
||||
Name: "auth",
|
||||
Value: "",
|
||||
Expires: time.Unix(0, 0),
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
// TODO check if I can use http.Redirect
|
||||
func redirect(path string, mode AnswerType, w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Location", path)
|
||||
if mode == JSON {
|
||||
w.WriteHeader(http.StatusSeeOther)
|
||||
w.Write([]byte(path))
|
||||
return
|
||||
}
|
||||
if mode&(HTMLFULL|HTML) != 0 {
|
||||
w.WriteHeader(http.StatusSeeOther)
|
||||
w.Write([]byte(path))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup response
|
||||
w.Header().Set("Location", "/login")
|
||||
if mode == JSON {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("\"Bye Bye\""));
|
||||
return
|
||||
}
|
||||
if mode & (HTMLFULL | HTML) != 0 {
|
||||
w.WriteHeader(http.StatusUnauthorized);
|
||||
w.Write([]byte("Bye Bye"));
|
||||
} else {
|
||||
w.WriteHeader(http.StatusSeeOther);
|
||||
}
|
||||
func logoff(mode AnswerType, w http.ResponseWriter, r *http.Request) {
|
||||
// Delete cookie
|
||||
cookie := &http.Cookie{
|
||||
Name: "auth",
|
||||
Value: "",
|
||||
Expires: time.Unix(0, 0),
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
redirect("/login", mode, w, r)
|
||||
}
|
||||
|
||||
func notAuth(mode AnswerType, w http.ResponseWriter, r *http.Request) {
|
||||
if mode == JSON {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("\"You can not access this resource!\""))
|
||||
return
|
||||
}
|
||||
if mode&(HTMLFULL|HTML) != 0 {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("You can not access this resource!"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
|
||||
func (x Handle) staticFiles(pathTest string, fileType string, contentType string) {
|
||||
http.HandleFunc(pathTest, func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path[len(pathTest):]
|
||||
|
||||
if !strings.HasSuffix(path, fileType) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("File not found"))
|
||||
return
|
||||
}
|
||||
|
||||
t, err := template.ParseFiles("./views" + pathTest + path)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Failed to load template"))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
|
||||
t.Execute(w, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func errorCode(err error, code int, data AnyMap) *Error {
|
||||
// TODO Improve Logging
|
||||
if err != nil {
|
||||
fmt.Printf("Something went wrong returning with: %d\n.Err:\n", code)
|
||||
fmt.Println(err)
|
||||
}
|
||||
return &Error{code, nil, data}
|
||||
}
|
||||
|
||||
func error500(err error) *Error {
|
||||
return errorCode(err, http.StatusInternalServerError, nil)
|
||||
}
|
||||
|
||||
func (x Handle) readFiles(pathTest string, baseFilePath string, fileType string, contentType string) {
|
||||
http.HandleFunc(pathTest, func(w http.ResponseWriter, r *http.Request) {
|
||||
user_path := r.URL.Path[len(pathTest):]
|
||||
|
||||
fmt.Printf("Requested path: %s\n", user_path)
|
||||
|
||||
if !strings.HasSuffix(user_path, fileType) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("File not found"))
|
||||
return
|
||||
}
|
||||
|
||||
bytes, err := os.ReadFile(path.Join(baseFilePath, pathTest, user_path))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Failed to load file"))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Write(bytes)
|
||||
})
|
||||
}
|
||||
|
||||
func NewHandler(db *sql.DB) *Handle {
|
||||
|
||||
var gets []HandleFunc
|
||||
var posts []HandleFunc
|
||||
x := &Handle{ db, gets, posts, }
|
||||
|
||||
var gets []HandleFunc
|
||||
var posts []HandleFunc
|
||||
var deletes []HandleFunc
|
||||
x := &Handle{db, gets, posts, deletes}
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Decide answertype
|
||||
@ -380,53 +499,11 @@ func NewHandler(db *sql.DB) *Handle {
|
||||
x.handlePosts(w, r, context)
|
||||
return
|
||||
}
|
||||
panic("TODO handle: " + r.Method)
|
||||
})
|
||||
|
||||
// TODO Handle this in other way
|
||||
http.HandleFunc("/styles/", func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path[len("/styles/"):]
|
||||
if !strings.HasSuffix(path, ".css") {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("File not found"))
|
||||
if r.Method == "DELETE" {
|
||||
x.handleDeletes(w, r, context)
|
||||
return
|
||||
}
|
||||
|
||||
t, err := template.ParseFiles("./views/styles/" + path)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Failed to load template"))
|
||||
fmt.Println("Error:")
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/css; charset=utf-8")
|
||||
t.Execute(w, nil)
|
||||
})
|
||||
|
||||
// TODO Handle this in other way
|
||||
http.HandleFunc("/js/", func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path[len("/js/"):]
|
||||
if !strings.HasSuffix(path, ".js") {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("File not found"))
|
||||
return
|
||||
}
|
||||
|
||||
t, err := template.ParseFiles("./views/js/" + path)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Failed to load template"))
|
||||
fmt.Println("Error:")
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
|
||||
t.Execute(w, nil)
|
||||
panic("TODO handle method: " + r.Method)
|
||||
})
|
||||
|
||||
return x
|
||||
|
14
main.go
14
main.go
@ -1,8 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
@ -27,11 +27,21 @@ func main() {
|
||||
defer db.Close()
|
||||
fmt.Println("Starting server on :8000!")
|
||||
|
||||
//TODO check if file structure exists to save data
|
||||
|
||||
handle := NewHandler(db)
|
||||
|
||||
handle.GetHTML("/", AnswerTemplate("index.html", nil))
|
||||
|
||||
// TODO Handle this in other way
|
||||
handle.staticFiles("/styles/", ".css", "text/css");
|
||||
handle.staticFiles("/js/", ".js", "text/javascript");
|
||||
handle.readFiles("/imgs/", "views", ".png", "image/png;");
|
||||
handle.readFiles("/savedData/", ".", ".png", "image/png;");
|
||||
|
||||
handle.GetHTML("/", AnswerTemplate("index.html", nil, 0))
|
||||
|
||||
usersEndpints(db, handle)
|
||||
handleModelsEndpoints(handle)
|
||||
|
||||
handle.Startup()
|
||||
}
|
||||
|
405
models.go
Normal file
405
models.go
Normal file
@ -0,0 +1,405 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
FAILED_PREPARING = -1
|
||||
|
||||
PREPARING = iota
|
||||
CONFIRM_PRE_TRAINING
|
||||
)
|
||||
|
||||
type BaseModel struct {
|
||||
Name string
|
||||
Status int
|
||||
Id string
|
||||
}
|
||||
|
||||
func modelUpdateStatus(handle *Handle, id string, status int) {
|
||||
_, err := handle.db.Exec("update models set status = $1 where id = $2", status, id)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to update model status")
|
||||
fmt.Println(err)
|
||||
panic("TODO handle better")
|
||||
}
|
||||
}
|
||||
|
||||
func loadBaseImage(handle *Handle, id string) {
|
||||
// TODO handle more types than png
|
||||
infile, err := os.Open(path.Join("savedData", id, "baseimage.png"))
|
||||
if err != nil {
|
||||
// TODO better logging
|
||||
fmt.Println(err)
|
||||
fmt.Printf("Failed to read image for model with id %s\n", id)
|
||||
modelUpdateStatus(handle, id, -1)
|
||||
return
|
||||
}
|
||||
defer infile.Close()
|
||||
|
||||
src, format, err := image.Decode(infile)
|
||||
if err != nil {
|
||||
// TODO better logging
|
||||
fmt.Println(err)
|
||||
fmt.Printf("Failed to load image for model with id %s\n", id)
|
||||
modelUpdateStatus(handle, id, -1)
|
||||
return
|
||||
}
|
||||
if format != "png" {
|
||||
// TODO better logging
|
||||
fmt.Printf("Found unkown format '%s'\n", format)
|
||||
panic("Handle diferent files than .png")
|
||||
}
|
||||
|
||||
var model_color string
|
||||
|
||||
bounds := src.Bounds()
|
||||
width, height := bounds.Max.X, bounds.Max.Y
|
||||
|
||||
switch src.ColorModel() {
|
||||
case color.Gray16Model:
|
||||
fallthrough
|
||||
case color.GrayModel:
|
||||
model_color = "greyscale"
|
||||
default:
|
||||
fmt.Println("Do not know how to handle this color model")
|
||||
|
||||
if src.ColorModel() == color.RGBA64Model {
|
||||
fmt.Println("Color is rgb")
|
||||
} else if src.ColorModel() == color.NRGBAModel {
|
||||
fmt.Println("Color is nrgb")
|
||||
} else if src.ColorModel() == color.YCbCrModel {
|
||||
fmt.Println("Color is ycbcr")
|
||||
} else if src.ColorModel() == color.AlphaModel {
|
||||
fmt.Println("Color is alpha")
|
||||
} else if src.ColorModel() == color.CMYKModel {
|
||||
fmt.Println("Color is cmyk")
|
||||
} else {
|
||||
fmt.Println("Other so assuming color")
|
||||
}
|
||||
|
||||
modelUpdateStatus(handle, id, -1)
|
||||
return
|
||||
}
|
||||
|
||||
// Note: this also updates the status to 2
|
||||
_, err = handle.db.Exec("update models set width=$1, height=$2, color_mode=$3, status=$4 where id=$5", width, height, model_color, CONFIRM_PRE_TRAINING, id)
|
||||
if err != nil {
|
||||
// TODO better logging
|
||||
fmt.Println(err)
|
||||
fmt.Printf("Could not update model\n")
|
||||
modelUpdateStatus(handle, id, -1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func deleteModel(handle *Handle, id string, w http.ResponseWriter, c *Context, model BaseModel) {
|
||||
fmt.Printf("Removing model with id: %s\n", id)
|
||||
_, err := handle.db.Exec("delete from models where id=$1;", id)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
panic("TODO handle better deleteModel failed delete database query")
|
||||
}
|
||||
|
||||
model_path := path.Join("./savedData", id)
|
||||
fmt.Printf("Removing folder of model with id: %s at %s\n", id, model_path)
|
||||
err = os.RemoveAll(model_path)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
panic("TODO handle better deleteModel failed to delete folder")
|
||||
}
|
||||
|
||||
if c.Mode == HTML {
|
||||
// TODO move this to a constant so i don't forget
|
||||
w.WriteHeader(309)
|
||||
c.Mode = HTMLFULL
|
||||
}
|
||||
|
||||
LoadBasedOnAnswer(c.Mode, w, "/models/delete.html", c.addMap(AnyMap{
|
||||
"Model": model,
|
||||
}))
|
||||
}
|
||||
|
||||
func handleModelsEndpoints(handle *Handle) {
|
||||
|
||||
// TODO json
|
||||
handle.GetHTML("/models", func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||
if c.Mode == JSON {
|
||||
panic("TODO JSON")
|
||||
}
|
||||
if !checkAuthLevel(1, w, r, c) {
|
||||
return nil
|
||||
}
|
||||
|
||||
rows, err := handle.db.Query("select id, name from models where user_id=$1;", c.User.id)
|
||||
if err != nil {
|
||||
return error500(err)
|
||||
}
|
||||
|
||||
type row struct {
|
||||
Name string
|
||||
Id string
|
||||
}
|
||||
|
||||
got := []row{}
|
||||
|
||||
for rows.Next() {
|
||||
var r row
|
||||
err = rows.Scan(&r.Id, &r.Name)
|
||||
if err != nil {
|
||||
return error500(err)
|
||||
}
|
||||
got = append(got, r)
|
||||
}
|
||||
|
||||
LoadBasedOnAnswer(c.Mode, w, "/models/list.html", c.addMap(AnyMap{
|
||||
"List": got,
|
||||
}))
|
||||
return nil
|
||||
})
|
||||
|
||||
handle.GetHTML("/models/add", AnswerTemplate("models/add.html", nil, 1))
|
||||
// TODO json
|
||||
handle.Post("/models/add", func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||
if c.Mode == JSON {
|
||||
panic("TODO JSON")
|
||||
}
|
||||
if !checkAuthLevel(1, w, r, c) {
|
||||
return nil
|
||||
}
|
||||
|
||||
read_form, err := r.MultipartReader()
|
||||
if err != nil {
|
||||
LoadBasedOnAnswer(c.Mode, w, "models/add.html", c.addMap(nil))
|
||||
return nil
|
||||
}
|
||||
|
||||
var name string
|
||||
var file []byte
|
||||
|
||||
for {
|
||||
part, err_part := read_form.NextPart()
|
||||
if err_part == io.EOF {
|
||||
break
|
||||
} else if err_part != nil {
|
||||
return &Error{code: http.StatusBadRequest}
|
||||
}
|
||||
if part.FormName() == "name" {
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(part)
|
||||
name = buf.String()
|
||||
}
|
||||
if part.FormName() == "file" {
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(part)
|
||||
file = buf.Bytes()
|
||||
}
|
||||
}
|
||||
|
||||
if name == "" || len(file) == 0 {
|
||||
LoadBasedOnAnswer(c.Mode, w, "models/add.html", c.addMap(nil))
|
||||
return nil
|
||||
}
|
||||
|
||||
row, err := handle.db.Query("select id from models where name=$1 and user_id=$2;", name, c.User.id)
|
||||
if err != nil {
|
||||
return error500(err)
|
||||
}
|
||||
|
||||
if row.Next() {
|
||||
LoadBasedOnAnswer(c.Mode, w, "models/add.html", c.addMap(AnyMap{
|
||||
"NameFoundError": true,
|
||||
"Name": name,
|
||||
}))
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = handle.db.Exec("insert into models (user_id, name) values ($1, $2)", c.User.id, name)
|
||||
if err != nil {
|
||||
return error500(err)
|
||||
}
|
||||
|
||||
row, err = handle.db.Query("select id from models where name=$1 and user_id=$2;", name, c.User.id)
|
||||
if err != nil {
|
||||
return error500(err)
|
||||
}
|
||||
|
||||
if !row.Next() {
|
||||
return &Error{code: http.StatusInternalServerError}
|
||||
}
|
||||
|
||||
var id string
|
||||
err = row.Scan(&id)
|
||||
if err != nil {
|
||||
return error500(err)
|
||||
}
|
||||
|
||||
// TODO mk this path configurable
|
||||
dir_path := path.Join("savedData", id)
|
||||
|
||||
err = os.Mkdir(dir_path, os.ModePerm)
|
||||
if err != nil {
|
||||
return error500(err)
|
||||
}
|
||||
f, err := os.Create(path.Join(dir_path, "baseimage.png"))
|
||||
if err != nil {
|
||||
return error500(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
f.Write(file)
|
||||
|
||||
fmt.Printf("Created model with id %s! Started to proccess image!\n", id)
|
||||
go loadBaseImage(handle, id)
|
||||
|
||||
redirect("/models/edit?id="+id, c.Mode, w, r)
|
||||
return nil
|
||||
})
|
||||
|
||||
handle.GetHTML("/models/edit", func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||
if !checkAuthLevel(1, w, r, c) {
|
||||
return nil
|
||||
}
|
||||
|
||||
id, err := getIdFromUrl(r, "id")
|
||||
if err != nil {
|
||||
return errorCode(nil, http.StatusNotFound, AnyMap{
|
||||
"NotFoundMessage": "Model not found",
|
||||
"GoBackLink": "/models",
|
||||
})
|
||||
}
|
||||
|
||||
// TODO handle admin users
|
||||
rows, err := handle.db.Query("select name, status, width, height, color_mode from models where id=$1 and user_id=$2;", id, c.User.id)
|
||||
if err != nil {
|
||||
return error500(err)
|
||||
}
|
||||
|
||||
if !rows.Next() {
|
||||
return errorCode(nil, http.StatusNotFound, AnyMap{
|
||||
"NotFoundMessage": "Model not found",
|
||||
"GoBackLink": "/models",
|
||||
})
|
||||
}
|
||||
|
||||
type rowmodel struct {
|
||||
Name string
|
||||
Status int
|
||||
Id string
|
||||
Width *int
|
||||
Height *int
|
||||
Color_mode *string
|
||||
}
|
||||
|
||||
var model rowmodel = rowmodel{}
|
||||
model.Id = id
|
||||
|
||||
err = rows.Scan(&model.Name, &model.Status, &model.Width, &model.Height, &model.Color_mode)
|
||||
if err != nil {
|
||||
return error500(err)
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
// All errors will be negative
|
||||
if model.Status < 0 {
|
||||
LoadBasedOnAnswer(c.Mode, w, "/models/edit.html", c.addMap(AnyMap{
|
||||
"Model": model,
|
||||
}))
|
||||
return nil
|
||||
}
|
||||
|
||||
switch model.Status {
|
||||
case PREPARING:
|
||||
LoadBasedOnAnswer(c.Mode, w, "/models/edit.html", c.addMap(AnyMap{
|
||||
"Model": model,
|
||||
}))
|
||||
case CONFIRM_PRE_TRAINING:
|
||||
LoadBasedOnAnswer(c.Mode, w, "/models/edit.html", c.addMap(AnyMap{
|
||||
"Model": model,
|
||||
}))
|
||||
|
||||
default:
|
||||
fmt.Printf("Unkown Status: %d\n", model.Status)
|
||||
return error500(nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
handle.Delete("/models/delete", func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||
if c.Mode == JSON {
|
||||
panic("TODO handle json on models/delete")
|
||||
}
|
||||
|
||||
f, err := MyParseForm(r)
|
||||
if err != nil {
|
||||
return errorCode(err, 400, nil)
|
||||
}
|
||||
|
||||
if !checkId(f, "id") {
|
||||
return errorCode(nil, http.StatusNotFound, AnyMap{
|
||||
"NotFoundMessage": "Model not found",
|
||||
"GoBackLink": "/models",
|
||||
})
|
||||
}
|
||||
|
||||
id := f.Get("id")
|
||||
|
||||
// TODO handle admin users
|
||||
rows, err := handle.db.Query("select name, status from models where id=$1 and user_id=$2;", id, c.User.id)
|
||||
if err != nil {
|
||||
return error500(err)
|
||||
}
|
||||
|
||||
if !rows.Next() {
|
||||
return errorCode(nil, http.StatusNotFound, AnyMap{
|
||||
"NotFoundMessage": "Model not found",
|
||||
"GoBackLink": "/models",
|
||||
})
|
||||
}
|
||||
|
||||
var model BaseModel = BaseModel{}
|
||||
model.Id = id
|
||||
|
||||
err = rows.Scan(&model.Name, &model.Status)
|
||||
if err != nil {
|
||||
return error500(err)
|
||||
}
|
||||
|
||||
switch model.Status {
|
||||
case FAILED_PREPARING:
|
||||
deleteModel(handle, id, w, c, model)
|
||||
return nil
|
||||
case CONFIRM_PRE_TRAINING:
|
||||
|
||||
if checkEmpty(f, "name") {
|
||||
// TODO improve result
|
||||
return errorCode(nil, http.StatusBadRequest, nil)
|
||||
}
|
||||
|
||||
name := f.Get("name")
|
||||
if name != model.Name {
|
||||
LoadError(w, "/models/edit.html", "delete-model-card", c.addMap(AnyMap{
|
||||
"NameDoesNotMatch": true,
|
||||
"Model": model,
|
||||
}))
|
||||
return nil
|
||||
}
|
||||
|
||||
deleteModel(handle, id, w, c, model)
|
||||
return nil
|
||||
default:
|
||||
panic("Do not know how to handle model in status:" + strconv.Itoa(model.Status))
|
||||
}
|
||||
})
|
||||
}
|
@ -4,10 +4,10 @@ in
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.mkShell {
|
||||
nativeBuildInputs = with unstable; [
|
||||
go
|
||||
gopls
|
||||
air
|
||||
go
|
||||
gopls
|
||||
air
|
||||
nodePackages.vscode-css-languageserver-bin
|
||||
nodePackages.vscode-html-languageserver-bin
|
||||
];
|
||||
];
|
||||
}
|
||||
|
20
sql/models.sql
Normal file
20
sql/models.sql
Normal file
@ -0,0 +1,20 @@
|
||||
-- drop table if exists model_defenitions
|
||||
-- drop table if exists models;
|
||||
create table if not exists models (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
user_id uuid references users (id) not null,
|
||||
name varchar (70) not null,
|
||||
-- Status:
|
||||
-- -1: failed preparing
|
||||
-- 1: preparing
|
||||
status integer default 1,
|
||||
|
||||
width integer,
|
||||
height integer,
|
||||
color_mode varchar (20)
|
||||
);
|
||||
|
||||
-- create table model_defenitions (
|
||||
-- id uuid primary key default gen_random_uuid(),
|
||||
-- model_id uuid references models (id) not null,
|
||||
-- )
|
@ -1,8 +1,9 @@
|
||||
-- drop table if exists tokens;
|
||||
-- drop table if exists models;
|
||||
-- drop table if exists users;
|
||||
create table if not exists users (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
user_type integer default 0,
|
||||
user_type integer default 1,
|
||||
username varchar (120) not null,
|
||||
email varchar (120) not null,
|
||||
salt char (8) not null,
|
||||
|
4
users.go
4
users.go
@ -108,7 +108,7 @@ func generateToken(db *sql.DB, email string, password string) (string, bool) {
|
||||
}
|
||||
|
||||
func usersEndpints(db *sql.DB, handle *Handle) {
|
||||
handle.GetHTML("/login", AnswerTemplate("login.html", nil))
|
||||
handle.GetHTML("/login", AnswerTemplate("login.html", nil, 0))
|
||||
handle.Post("/login", func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||
if c.Mode == JSON {
|
||||
fmt.Println("Handle JSON")
|
||||
@ -148,7 +148,7 @@ func usersEndpints(db *sql.DB, handle *Handle) {
|
||||
return nil
|
||||
})
|
||||
|
||||
handle.GetHTML("/register", AnswerTemplate("register.html", nil))
|
||||
handle.GetHTML("/register", AnswerTemplate("register.html", nil, 0))
|
||||
handle.Post("/register", func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||
if c.Mode == JSON {
|
||||
return &Error{code: http.StatusNotFound}
|
||||
|
147
utils.go
147
utils.go
@ -1,7 +1,150 @@
|
||||
package main
|
||||
|
||||
import "net/url"
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func checkEmpty(f url.Values, path string) bool {
|
||||
return !f.Has(path) || f.Get(path) == ""
|
||||
return !f.Has(path) || f.Get(path) == ""
|
||||
}
|
||||
|
||||
func checkId(f url.Values, path string) bool {
|
||||
return !checkEmpty(f, path) && isValidUUID(f.Get(path))
|
||||
}
|
||||
|
||||
func isValidUUID(u string) bool {
|
||||
_, err := uuid.Parse(u)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func getIdFromUrl(r *http.Request, target string) (string, error) {
|
||||
if !r.URL.Query().Has(target) {
|
||||
return "", errors.New("Query does not have " + target)
|
||||
}
|
||||
|
||||
id := r.URL.Query().Get("id")
|
||||
if len(id) == 0 {
|
||||
return "", errors.New("Query is empty for " + target)
|
||||
}
|
||||
|
||||
if !isValidUUID(id) {
|
||||
return "", errors.New("Value of query is not a valid uuid for " + target)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
type maxBytesReader struct {
|
||||
w http.ResponseWriter
|
||||
r io.ReadCloser // underlying reader
|
||||
i int64 // max bytes initially, for MaxBytesError
|
||||
n int64 // max bytes remaining
|
||||
err error // sticky error
|
||||
}
|
||||
|
||||
type MaxBytesError struct {
|
||||
Limit int64
|
||||
}
|
||||
|
||||
func (e *MaxBytesError) Error() string {
|
||||
// Due to Hyrum's law, this text cannot be changed.
|
||||
return "http: request body too large"
|
||||
}
|
||||
|
||||
func (l *maxBytesReader) Read(p []byte) (n int, err error) {
|
||||
if l.err != nil {
|
||||
return 0, l.err
|
||||
}
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
// If they asked for a 32KB byte read but only 5 bytes are
|
||||
// remaining, no need to read 32KB. 6 bytes will answer the
|
||||
// question of the whether we hit the limit or go past it.
|
||||
// 0 < len(p) < 2^63
|
||||
if int64(len(p))-1 > l.n {
|
||||
p = p[:l.n+1]
|
||||
}
|
||||
n, err = l.r.Read(p)
|
||||
|
||||
if int64(n) <= l.n {
|
||||
l.n -= int64(n)
|
||||
l.err = err
|
||||
return n, err
|
||||
}
|
||||
|
||||
n = int(l.n)
|
||||
l.n = 0
|
||||
|
||||
// The server code and client code both use
|
||||
// maxBytesReader. This "requestTooLarge" check is
|
||||
// only used by the server code. To prevent binaries
|
||||
// which only using the HTTP Client code (such as
|
||||
// cmd/go) from also linking in the HTTP server, don't
|
||||
// use a static type assertion to the server
|
||||
// "*response" type. Check this interface instead:
|
||||
type requestTooLarger interface {
|
||||
requestTooLarge()
|
||||
}
|
||||
if res, ok := l.w.(requestTooLarger); ok {
|
||||
res.requestTooLarge()
|
||||
}
|
||||
l.err = &MaxBytesError{l.i}
|
||||
return n, l.err
|
||||
}
|
||||
|
||||
func (l *maxBytesReader) Close() error {
|
||||
return l.r.Close()
|
||||
}
|
||||
|
||||
func MyParseForm(r *http.Request) (vs url.Values, err error) {
|
||||
if r.Body == nil {
|
||||
err = errors.New("missing form body")
|
||||
return
|
||||
}
|
||||
ct := r.Header.Get("Content-Type")
|
||||
// RFC 7231, section 3.1.1.5 - empty type
|
||||
// MAY be treated as application/octet-stream
|
||||
if ct == "" {
|
||||
ct = "application/octet-stream"
|
||||
}
|
||||
ct, _, err = mime.ParseMediaType(ct)
|
||||
switch {
|
||||
case ct == "application/x-www-form-urlencoded":
|
||||
var reader io.Reader = r.Body
|
||||
maxFormSize := int64(1<<63 - 1)
|
||||
if _, ok := r.Body.(*maxBytesReader); !ok {
|
||||
maxFormSize = int64(10 << 20) // 10 MB is a lot of text.
|
||||
reader = io.LimitReader(r.Body, maxFormSize+1)
|
||||
}
|
||||
b, e := io.ReadAll(reader)
|
||||
if e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
}
|
||||
break
|
||||
}
|
||||
if int64(len(b)) > maxFormSize {
|
||||
err = errors.New("http: POST too large")
|
||||
return
|
||||
}
|
||||
vs, e = url.ParseQuery(string(b))
|
||||
if err == nil {
|
||||
err = e
|
||||
}
|
||||
case ct == "multipart/form-data":
|
||||
// handled by ParseMultipartForm (which is calling us, or should be)
|
||||
// TODO(bradfitz): there are too many possible
|
||||
// orders to call too many functions here.
|
||||
// Clean this up and write more tests.
|
||||
// request_test.go contains the start of this,
|
||||
// in TestParseMultipartFormOrder and others.
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -4,12 +4,25 @@
|
||||
<h1>
|
||||
404
|
||||
</h1>
|
||||
<h2>
|
||||
Page Not found
|
||||
</h2>
|
||||
<div class="description">
|
||||
The page you were looking for does not exist
|
||||
</div>
|
||||
{{ if .NotFoundMessage }}
|
||||
<h2>
|
||||
{{ .NotFoundMessage }}
|
||||
</h2>
|
||||
{{ if .GoBackLink }}
|
||||
<div class="description">
|
||||
<a hx-get="{{ .GoBackLink }}" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
|
||||
👈 Go back
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<h2>
|
||||
Page Not found
|
||||
</h2>
|
||||
<div class="description">
|
||||
The page you were looking for does not exist
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
BIN
views/imgs/upload-icon.png
Normal file
BIN
views/imgs/upload-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
@ -1,14 +1,49 @@
|
||||
function load() {
|
||||
for (const elm of document.querySelectorAll("form > button")) {
|
||||
elm.addEventListener('click', (e) => {
|
||||
e.target.parentElement.classList.add("submitted");
|
||||
});
|
||||
}
|
||||
for (const elm of document.querySelectorAll("form > button")) {
|
||||
elm.addEventListener('click', (e) => {
|
||||
e.target.parentElement.classList.add("submitted");
|
||||
});
|
||||
}
|
||||
for (const elm of document.querySelectorAll("button.icon")) {
|
||||
elm.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
const input = document.querySelectorAll('form .file-upload input[type="file"]')[0];
|
||||
if (input) {
|
||||
input.click();
|
||||
input.addEventListener('change', (e) => {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
elm.setAttribute("disabled", "true");
|
||||
|
||||
const spanToReplace = document.querySelector('.file-upload .icon span');
|
||||
const imgToReplace = document.querySelector('.file-upload .icon img');
|
||||
if (!imgToReplace || !spanToReplace) return;
|
||||
|
||||
if (imgToReplace.getAttribute("replace")) {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onloadend = () => {
|
||||
imgToReplace.setAttribute("src", fileReader.result)
|
||||
elm.classList.add("adapt");
|
||||
}
|
||||
fileReader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
if (spanToReplace.getAttribute("replace")) {
|
||||
spanToReplace.innerHTML = spanToReplace.getAttribute("replace")
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
window.onload = load;
|
||||
htmx.on('htmx:afterSwap', load);
|
||||
htmx.on('htmx:beforeSwap', (env) => {
|
||||
if (env.detail.xhr.status === 401) {
|
||||
window.location = "/login"
|
||||
}
|
||||
} else
|
||||
// 309 is the code I selected for html to htmlfull change
|
||||
if (env.detail.xhr.status === 309) {
|
||||
env.detail.target = htmx.find(".app")
|
||||
}
|
||||
});
|
||||
|
41
views/models/add.html
Normal file
41
views/models/add.html
Normal file
@ -0,0 +1,41 @@
|
||||
{{define "title"}}
|
||||
Create New Model : AI Stuff
|
||||
{{end}}
|
||||
|
||||
{{define "mainbody"}}
|
||||
<main>
|
||||
<h1>
|
||||
Create new Model
|
||||
</h1>
|
||||
<form enctype="multipart/form-data" action="/models/add" method="POST" {{if .Submited}}class="submitted"{{end}} >
|
||||
<fieldset>
|
||||
<label for="name">Name</label>
|
||||
<input id="name" name="name" required {{if .Name}} value="{{.Name}}" {{end}} />
|
||||
{{if .NameFoundError}}
|
||||
<span class="form-msg error">
|
||||
You already have a model with that name.
|
||||
</span>
|
||||
{{end}}
|
||||
</fieldset>
|
||||
<fieldset class="file-upload" >
|
||||
<label for="file">Base image</label>
|
||||
<div class="form-msg">
|
||||
Please provide a base image.<br/>
|
||||
This image is a sample of the images that you are going to classfy.
|
||||
</div>
|
||||
<div class="icon-holder">
|
||||
<button class="icon">
|
||||
<img replace="true" src="/imgs/upload-icon.png" />
|
||||
<span replace="Image selected">
|
||||
Upload image
|
||||
</span>
|
||||
</button>
|
||||
<input id="file" name="file" type="file" required accept="image/png" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<button>
|
||||
Create
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
{{end}}
|
12
views/models/delete.html
Normal file
12
views/models/delete.html
Normal file
@ -0,0 +1,12 @@
|
||||
{{ define "mainbody" }}
|
||||
<main>
|
||||
<h2 class="text-center">
|
||||
Model {{ .Model.Name }} was deleted!
|
||||
</h2>
|
||||
<div class="description text-center">
|
||||
<a hx-get="/models" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
|
||||
👈 Go back
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
{{ end }}
|
83
views/models/edit.html
Normal file
83
views/models/edit.html
Normal file
@ -0,0 +1,83 @@
|
||||
{{ define "title" }}
|
||||
Model: {{ .Model.Name }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "base-model-card" }}
|
||||
<div class="card model-card">
|
||||
<h1>
|
||||
{{ .Model.Name }}
|
||||
</h1>
|
||||
<div class="second-line">
|
||||
<img src="/savedData/{{ .Model.Id }}/baseimage.png" />
|
||||
<div class="info">
|
||||
<div>
|
||||
<span class="bold bigger">Image Type:</span> {{ .Model.Color_mode }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="bold bigger">Image Size:</span> {{ .Model.Width }}x{{ .Model.Height }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ define "delete-model-card" }}
|
||||
<form hx-delete="/models/delete" hx-headers='{"REQUEST-TYPE": "html"}' hx-swap="outerHTML" {{ if .Error }} class="submitted" {{end}} >
|
||||
<fieldset>
|
||||
<span>
|
||||
To delete this model please type "{{ .Model.Name }}":
|
||||
</span>
|
||||
<label for="name">Name</label>
|
||||
<input name="name" id="name" required />
|
||||
{{ if .NameDoesNotMatch }}
|
||||
<span class="form-msg red">
|
||||
Name does not match "{{ .Model.Name }}"
|
||||
</span>
|
||||
{{ end }}
|
||||
</fieldset>
|
||||
<input type="hidden" name="id" value="{{ .Model.Id }}" />
|
||||
<button class="danger">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
|
||||
{{ define "mainbody" }}
|
||||
<main>
|
||||
{{ if (eq .Model.Status 1) }}
|
||||
<div>
|
||||
<h1 class="text-center">
|
||||
{{ .Model.Name }}
|
||||
</h1>
|
||||
<!-- TODO add cool animation -->
|
||||
<h2 class="text-center">
|
||||
Preparing the model
|
||||
</h2>
|
||||
</div>
|
||||
{{ else if (eq .Model.Status -1) }}
|
||||
<div>
|
||||
<h1 class="text-center">
|
||||
{{ .Model.Name }}
|
||||
</h1>
|
||||
<!-- TODO improve message -->
|
||||
<h2 class="text-center">
|
||||
Failed to prepare model
|
||||
</h2>
|
||||
|
||||
<form hx-delete="/models/delete">
|
||||
<input type="hidden" value="{{ .Model.Id }}" />
|
||||
<button class="danger">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{{ else if (eq .Model.Status 2) }}
|
||||
{{ template "base-model-card" . }}
|
||||
{{ template "delete-model-card" . }}
|
||||
{{ else }}
|
||||
<h1>
|
||||
Unknown Status of the model.
|
||||
</h1>
|
||||
{{ end }}
|
||||
</main>
|
||||
{{ end }}
|
52
views/models/list.html
Normal file
52
views/models/list.html
Normal file
@ -0,0 +1,52 @@
|
||||
{{define "title"}}
|
||||
Models : AI Stuff
|
||||
{{end}}
|
||||
|
||||
{{define "mainbody"}}
|
||||
<main>
|
||||
{{ if (lt 0 (len .List)) }}
|
||||
<div class="list-header">
|
||||
<h2>My Models</h2>
|
||||
<div class="expand"></div>
|
||||
<a class="button" hx-get="/models/add" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
|
||||
New
|
||||
</a>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
<!-- Open Button -->
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .List}}
|
||||
<tr>
|
||||
<td>
|
||||
{{.Name}}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a class="button simple" hx-get="/models/edit?id={{.Id}}" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
|
||||
Edit
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<h2 class="text-center">
|
||||
You don't have any model
|
||||
</h2>
|
||||
<div class="text-center">
|
||||
<a class="button padded" hx-get="/models/add" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
|
||||
Create a new model
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</main>
|
||||
{{end}}
|
@ -5,6 +5,13 @@
|
||||
Index
|
||||
</a>
|
||||
</li>
|
||||
{{ if .Context.User }}
|
||||
<li>
|
||||
<a hx-get="/models" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
|
||||
Models
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
<li class="expand"></li>
|
||||
{{ if .Context.User }}
|
||||
<li>
|
||||
|
@ -1,7 +1,4 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
*{box-sizing: border-box;font-family: 'Roboto', sans-serif;}
|
||||
|
||||
:root {
|
||||
--white: #ffffff;
|
||||
@ -18,6 +15,10 @@ body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 20px 15vw;
|
||||
}
|
||||
|
||||
.w100 {
|
||||
width: 100%;
|
||||
display: block;
|
||||
@ -32,6 +33,38 @@ body {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bigger {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Generic */
|
||||
.button,
|
||||
button {
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
padding: 2px;
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px 1px #66666655;
|
||||
background: var(--main);
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
.button.padded,
|
||||
button.padded {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.button.danger,
|
||||
button.danger {
|
||||
background: rgb(var(--red));
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Nav bar */
|
||||
|
||||
nav {
|
||||
@ -49,6 +82,11 @@ nav ul {
|
||||
|
||||
nav ul li {
|
||||
list-style: none;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
nav ul li:first-child {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav ul .expand {
|
||||
@ -102,6 +140,7 @@ a {
|
||||
|
||||
form {
|
||||
padding: 30px;
|
||||
margin: 20px 0;
|
||||
border-radius: 10px;
|
||||
box-shadow: 2px 5px 8px 2px #66666655;
|
||||
}
|
||||
@ -124,6 +163,7 @@ form input:invalid:focus,
|
||||
form.submitted input:invalid {
|
||||
box-shadow: 0 2px 5px 1px rgba(var(--red), 0.2);
|
||||
}
|
||||
|
||||
form.submitted input:valid {
|
||||
box-shadow: 0 2px 5px 1px rgba(var(--green), 0.2);
|
||||
}
|
||||
@ -146,16 +186,150 @@ form fieldset .error {
|
||||
}
|
||||
|
||||
form button {
|
||||
border-radius: 9px 10px;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px 1px #66666655;
|
||||
font-size: 1.2rem;
|
||||
margin-left: 50%;
|
||||
width: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--main);
|
||||
color: var(--black);
|
||||
font-size: 1.2rem;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Upload files */
|
||||
|
||||
form fieldset.file-upload input[type="file"] {
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
form fieldset.file-upload .icon-holder {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
form fieldset.file-upload .icon-holder .icon {
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
background: none;
|
||||
transform: none;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
transition: all 1s;
|
||||
}
|
||||
|
||||
form fieldset.file-upload .icon-holder .icon.adapt {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
min-height: 150px;
|
||||
min-width: 150px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
form fieldset.file-upload:has(input[type="file"]:invalid:focus) .icon,
|
||||
form.submitted fieldset.file-upload:has(input[type="file"]:invalid:focus) .icon {
|
||||
box-shadow: 0 2px 5px 1px rgba(var(--red), 0.2);
|
||||
}
|
||||
|
||||
form.submitted fieldset.file-upload:has(input[type="file"]:valid:focus) .icon{
|
||||
box-shadow: 0 2px 5px 1px rgba(var(--green), 0.2);
|
||||
}
|
||||
|
||||
form fieldset.file-upload .icon-holder .icon img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
object-fit: contain;
|
||||
text-align: center;
|
||||
transition: all 1s;
|
||||
}
|
||||
|
||||
form fieldset.file-upload .icon-holder .icon span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.list-header h2 {
|
||||
margin: 0;
|
||||
padding: 10px 5px;
|
||||
}
|
||||
|
||||
.list-header .expand {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.list-header .button,
|
||||
.list-header button {
|
||||
padding: 10px 10px;
|
||||
height: calc(100% - 20px);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Table */
|
||||
table {
|
||||
width: 100%;
|
||||
box-shadow: 0 2px 8px 1px #66666622;
|
||||
border-radius: 10px;
|
||||
border-collapse: collapse;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
table thead {
|
||||
background: #60606022;
|
||||
}
|
||||
|
||||
table tr td,
|
||||
table tr th {
|
||||
border-left: 1px solid #22222244;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
table tr td:first-child,
|
||||
table tr th:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
table tr td button,
|
||||
table tr td .button {
|
||||
padding: 5px 10px;
|
||||
box-shadow: 0 2px 5px 1px #66666655;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 2px 5px 1px #66666655;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Model stuff */
|
||||
.model-card h1 {
|
||||
margin: 0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.model-card img {
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.model-card .second-line {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user