123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509 |
- package main
- import (
- "encoding/json"
- "errors"
- "fmt"
- "html/template"
- "io"
- "log"
- "net/http"
- "net/url"
- "strconv"
- "strings"
- "github.com/Masterminds/sprig/v3"
- )
- const (
- App = "Contacts.app"
- SecretKey = "hypermedia rocks"
- )
- func MakeTemplate(first string, others ...string) *template.Template {
- all := append([]string{first}, others...)
- paths := make([]string, len(all))
- for i, name := range all {
- paths[i] = fmt.Sprintf("./templates/%s.gohtml", name)
- }
- // All templates are named like "whatever.html" to match the Python version.
- tpl := template.Must(template.New(first + ".html").
- Funcs(sprig.FuncMap()).
- Funcs(template.FuncMap{
- "get_flashed_messages": func() []string {
- return []string{"some message"}
- },
- }).
- ParseFiles(paths...),
- )
- return tpl
- }
- var (
- /*
- @app.route("/")
- def index():
- */
- RouteFront = http.RedirectHandler("/contacts", http.StatusFound)
- )
- /*
- @app.route("/contacts")
- def contacts():
- */
- func MakeRouteContacts() http.HandlerFunc {
- tpl := MakeTemplate(
- "layout",
- "index",
- "archive_ui",
- "rows",
- )
- return func(w http.ResponseWriter, r *http.Request) {
- search := r.URL.Query().Get("q")
- page, err := strconv.Atoi(r.URL.Query().Get("page"))
- if err != nil || page == 0 {
- page = 1
- }
- var contactsSet []Contact
- if search != "" {
- contactsSet = (&Contact{}).Search(search)
- if r.Header.Get("HX-Trigger") == "search" {
- tpl.Execute(w, anyMap{"contacts": contactsSet})
- return
- }
- } else {
- contactsSet = (&Contact{}).All(page)
- }
- if err := tpl.Execute(w, anyMap{
- "contacts": contactsSet,
- "search": search,
- "page": page,
- "archiver": GetArchiver(),
- }); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }
- }
- /*
- @app.route("/contacts/archive", methods=["POST"])
- def start_archive():
- */
- func MakeStartArchive() http.HandlerFunc {
- tpl := MakeTemplate(
- "archive_ui",
- )
- return func(w http.ResponseWriter, r *http.Request) {
- archiver := GetArchiver()
- archiver.Run()
- tpl.Execute(w, anyMap{"archiver": archiver})
- }
- }
- /*
- @app.route("/contacts/archive", methods=["GET"])
- def archive_status():
- */
- func MakeArchiveStatus() http.HandlerFunc {
- tpl := MakeTemplate(
- "archive_ui",
- )
- return func(w http.ResponseWriter, r *http.Request) {
- archiver := GetArchiver()
- tpl.Execute(w, anyMap{"archiver": archiver})
- }
- }
- /*
- @app.route("/contacts/archive/file", methods=["GET"])
- def archive_content():
- */
- func ArchiveContent(w http.ResponseWriter, r *http.Request) {
- archiver := GetArchiver()
- w.Header().Set("Content-Disposition", `attachement; filename="archive.json"`)
- http.ServeFile(w, r, archiver.ArchiveFile())
- }
- /*
- @app.route("/contacts/archive", methods=["DELETE"])
- def reset_archive():
- */
- func MakeResetArchive() http.HandlerFunc {
- tpl := MakeTemplate(
- "archive_ui",
- )
- return func(w http.ResponseWriter, r *http.Request) {
- archiver := GetArchiver()
- archiver.Reset()
- tpl.Execute(w, anyMap{"archiver": archiver})
- }
- }
- /*
- @app.route("/contacts/count")
- def contacts_count():
- */
- func ContactsCount(w http.ResponseWriter, r *http.Request) {
- count := (&Contact{}).Count()
- _, _ = fmt.Fprintf(w, "(%d total Contacts)", count)
- }
- /*
- @app.route("/contacts/new", methods=['GET'])
- def contacts_new_get():
- */
- func MakeContactsNewGet() http.HandlerFunc {
- tpl := MakeTemplate(
- "layout",
- "new",
- )
- return func(w http.ResponseWriter, r *http.Request) {
- if err := tpl.Execute(w, anyMap{"contact": NewContact(nil)}); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }
- }
- /*
- @app.route("/contacts/new", methods=['POST'])
- def contacts_new():
- */
- func MakeContactsNew() http.HandlerFunc {
- tpl := MakeTemplate(
- "layout",
- "new",
- )
- return func(w http.ResponseWriter, r *http.Request) {
- c := NewContact(anyMap{
- "first": r.FormValue("first_name"),
- "last": r.FormValue("last_name"),
- "email": r.FormValue("phone"),
- "phone": r.FormValue("email"),
- })
- if c.Save() {
- flash("Created New Contact!")
- http.Redirect(w, r, "/contacts", http.StatusFound)
- return
- } else {
- tpl.Execute(w, anyMap{"contact": c})
- }
- }
- }
- /*
- @app.route("/contacts/<contact_id>")
- def contacts_view(contact_id=0):
- */
- func MakeContactsView() http.HandlerFunc {
- tpl := MakeTemplate(
- "layout",
- "show",
- )
- return func(w http.ResponseWriter, r *http.Request) {
- contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
- contact := (&Contact{}).Find(uint64(contact_id))
- if err := tpl.Execute(w, anyMap{"contact": contact}); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }
- }
- /*
- @app.route("/contacts/<contact_id>/edit", methods=["GET"])
- def contacts_edit_get(contact_id=0):
- */
- func MakeContactsEditGet() http.HandlerFunc {
- tpl := MakeTemplate(
- "layout",
- "edit",
- )
- return func(w http.ResponseWriter, r *http.Request) {
- contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
- contact := (&Contact{}).Find(uint64(contact_id))
- tpl.Execute(w, anyMap{"contact": contact})
- }
- }
- /*
- @app.route("/contacts/<contact_id>/edit", methods=["POST"])
- def contacts_edit_post(contact_id=0):
- */
- func MakeContactsEditPost() http.HandlerFunc {
- tpl := MakeTemplate(
- "layout",
- "edit",
- )
- return func(w http.ResponseWriter, request *http.Request) {
- contact_id, _ := strconv.Atoi(request.PathValue("contact_id"))
- c := (&Contact{}).Find(uint64(contact_id))
- c.Update(
- request.FormValue("first_name"),
- request.FormValue("last_name"),
- request.FormValue("phone"),
- request.FormValue("email"),
- )
- if c.Save() {
- flash("Updated Contact!")
- http.Redirect(w, request, fmt.Sprintf("/contacts/%d", contact_id), http.StatusFound)
- return
- } else {
- tpl.Execute(w, anyMap{"contact": c})
- }
- }
- }
- /*
- @app.route("/contacts/<contact_id>/email", methods=["GET"])
- def contacts_email_get(contact_id=0):
- */
- func ContactsEmailGet(w http.ResponseWriter, r *http.Request) {
- contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
- c := (&Contact{}).Find(uint64(contact_id))
- c.Email = r.FormValue("email")
- c.validate()
- // FIXME find what this is really expected to do in the original Python.
- for _, err := range c.Errors {
- upErr := strings.ToUpper(err.Error())
- if strings.Contains(upErr, "EMAIL") {
- fmt.Fprintln(w, err)
- }
- }
- }
- /*
- @app.route("/contacts/<contact_id>", methods=["DELETE"])
- def contacts_delete(contact_id=0):
- */
- func ContactsDelete(w http.ResponseWriter, r *http.Request) {
- contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
- contact := (&Contact{}).Find(uint64(contact_id))
- contact.Delete()
- if r.Header.Get("HX-Trigger") == "delete-btn" {
- flash("Deleted Contact!")
- http.Redirect(w, r, "/contacts", http.StatusSeeOther)
- return
- } else {
- return
- }
- }
- /*
- @app.route("/contacts/", methods=["DELETE"])
- def contacts_delete_all():
- */
- func MakeContactsDeleteAll() http.HandlerFunc {
- tpl := MakeTemplate(
- "layout",
- "index",
- "archive_ui",
- "rows",
- )
- return func(w http.ResponseWriter, r *http.Request) {
- // Per rfc7231#section-4.3.5 "A payload within a DELETE request message has no defined semantics"
- //
- // So Go does not parse forms on methods other than POST, PUT and PATCH,
- // but this handler is called with DELETE, so we need a manual parse.
- if r == nil || r.Body == nil {
- http.Error(w, "invalid void request", http.StatusBadRequest)
- return
- }
- body, err := io.ReadAll(r.Body)
- if err != nil {
- http.Error(w, fmt.Sprintf("reading request body: %v", err), http.StatusBadRequest)
- return
- }
- values, err := url.ParseQuery(string(body))
- if err != nil {
- http.Error(w, fmt.Sprintf("parsing form data: %v", err), http.StatusBadRequest)
- return
- }
- contactIDs := values["selected_contact_ids"]
- for _, sContactID := range contactIDs {
- if contactID, err := strconv.Atoi(sContactID); contactID > 0 && err == nil {
- contact := (&Contact{}).Find(uint64(contactID))
- contact.Delete()
- }
- }
- flash("Deleted contacts!")
- contacts_set := (&Contact{}).All(1)
- if err := tpl.Execute(w, anyMap{"contacts": contacts_set}); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
- }
- }
- /*
- # ===========================================================
- # JSON Data API
- # ===========================================================
- */
- /*
- @app.route("/api/v1/contacts", methods=["GET"])
- def json_contacts():
- */
- func JSONContacts(w http.ResponseWriter, r *http.Request) {
- contacts_set := (&Contact{}).All(1)
- enc := json.NewEncoder(w)
- if err := enc.Encode(anyMap{"contacts": contacts_set}); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }
- /*
- @app.route("/api/v1/contacts", methods=["POST"])
- def json_contacts_new():
- */
- func JSONContactsNew(w http.ResponseWriter, r *http.Request) {
- if r == nil || r.Body == nil {
- http.Error(w, "invalid void request", http.StatusBadRequest)
- return
- }
- body, err := io.ReadAll(r.Body)
- if err != nil {
- http.Error(w, fmt.Sprintf("error reading body: %v", err), http.StatusBadRequest)
- return
- }
- m := make(anyMap, 4)
- if err := json.Unmarshal(body, &m); err != nil {
- http.Error(w, fmt.Sprintf("parsing JSON request: %v", err), http.StatusBadRequest)
- return
- }
- c := NewContact(anyMap{
- "first": m["first_name"],
- "last": m["last_name"],
- "phone": m["phone"],
- "email": m["email"],
- })
- enc := json.NewEncoder(w)
- if c.Save() {
- if err := enc.Encode(c); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- } else {
- w.WriteHeader(http.StatusBadRequest)
- if err := enc.Encode(anyMap{"errors": c.Errors}); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }
- }
- /*
- @app.route("/api/v1/contacts/<contact_id>", methods=["GET"])
- def json_contacts_view(contact_id=0):
- */
- func JSONContactsView(w http.ResponseWriter, r *http.Request) {
- contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
- c := (&Contact{}).Find(uint64(contact_id))
- enc := json.NewEncoder(w)
- if err := enc.Encode(c); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }
- /*
- @app.route("/api/v1/contacts/<contact_id>", methods=["PUT"])
- def json_contacts_edit(contact_id):
- */
- func JSONContactsEdit(w http.ResponseWriter, r *http.Request) {
- contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
- if r == nil || r.Body == nil {
- http.Error(w, "invalid void request", http.StatusBadRequest)
- return
- }
- body, err := io.ReadAll(r.Body)
- if err != nil {
- http.Error(w, fmt.Sprintf("error reading body: %v", err), http.StatusBadRequest)
- return
- }
- m := make(map[string]string, 4)
- if err := json.Unmarshal(body, &m); err != nil {
- http.Error(w, fmt.Sprintf("parsing JSON request: %v", err), http.StatusBadRequest)
- return
- }
- c := (&Contact{}).Find(uint64(contact_id))
- c.Update(
- m["first_name"],
- m["last_name"],
- m["phone"],
- m["email"],
- )
- enc := json.NewEncoder(w)
- if c.Save() {
- if err := enc.Encode(c); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- } else {
- w.WriteHeader(http.StatusBadRequest)
- if err := enc.Encode(anyMap{"errors": c.Errors}); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }
- }
- /*
- @app.route("/api/v1/contacts/<contact_id>", methods=["DELETE"])
- def json_contacts_delete(contact_id=0):
- */
- func JSONContactsDelete(w http.ResponseWriter, r *http.Request) {
- contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
- contact := (&Contact{}).Find(uint64(contact_id))
- contact.Delete()
- enc := json.NewEncoder(w)
- _ = enc.Encode(anyMap{"success": true})
- }
- /*
- *
- TODO implement me
- */
- func flash(message string) {
- }
- func setupRoutes(mux *http.ServeMux) {
- // OK
- mux.Handle("/", RouteFront)
- mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
- mux.HandleFunc("/contacts", MakeRouteContacts())
- mux.Handle("POST /contacts/archive", MakeStartArchive())
- mux.Handle("GET /contacts/archive", MakeArchiveStatus())
- mux.HandleFunc("GET /contacts/archive/file", ArchiveContent)
- mux.Handle("DELETE /contacts/archive", MakeResetArchive())
- mux.HandleFunc("GET /contacts/count", ContactsCount)
- mux.Handle("GET /contacts/new", MakeContactsNewGet())
- mux.Handle("POST /contacts/new", MakeContactsNew())
- mux.Handle("GET /contacts/{contact_id}", MakeContactsView())
- mux.Handle("GET /contacts/{contact_id}/edit", MakeContactsEditGet())
- mux.Handle("POST /contacts/{contact_id}/edit", MakeContactsEditPost())
- mux.HandleFunc("GET /contacts/{contact_id}/email", ContactsEmailGet)
- mux.HandleFunc("DELETE /contacts/{contact_id}", ContactsDelete)
- mux.Handle("DELETE /contacts/", MakeContactsDeleteAll())
- mux.HandleFunc("GET /api/v1/contacts", JSONContacts)
- mux.HandleFunc("POST /api/v1/contacts", JSONContactsNew)
- mux.HandleFunc("GET /api/v1/contacts/{contact_id}", JSONContactsView)
- mux.HandleFunc("PUT /api/v1/contacts/{contact_id}", JSONContactsEdit)
- mux.HandleFunc("DELETE /api/v1/contacts/{contact_id}", JSONContactsDelete)
- }
- func main() {
- mux := http.NewServeMux()
- setupRoutes(mux)
- (&Contact{}).LoadDB()
- if err := http.ListenAndServe(":8080", mux); err != nil {
- if !errors.Is(err, http.ErrServerClosed) {
- log.Printf("Server error: %v\n", err)
- return
- }
- }
- log.Println("Server shutdown")
- }
|