浏览代码

All methods working, UI working.

Frédéric G. MARAND 11 月之前
父节点
当前提交
f9f43ac167

+ 23 - 0
.idea/aws.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="accountSettings">
+    <option name="activeProfile" value="profile:default" />
+    <option name="activeRegion" value="eu-west-3" />
+    <option name="recentlyUsedProfiles">
+      <list>
+        <option value="profile:default" />
+      </list>
+    </option>
+    <option name="recentlyUsedRegions">
+      <list>
+        <option value="eu-west-3" />
+      </list>
+    </option>
+  </component>
+  <component name="connectionManager">
+    <option name="activeConnectionId" value="sso;us-east-1;https://view.awsapps.com/start" />
+  </component>
+  <component name="explorerToolWindow">
+    <option name="selectedTab" value="Explorer" />
+  </component>
+</project>

+ 21 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,21 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="ignoredPackages">
+        <value>
+          <list size="8">
+            <item index="0" class="java.lang.String" itemvalue="Werkzeug" />
+            <item index="1" class="java.lang.String" itemvalue="zipp" />
+            <item index="2" class="java.lang.String" itemvalue="MarkupSafe" />
+            <item index="3" class="java.lang.String" itemvalue="itsdangerous" />
+            <item index="4" class="java.lang.String" itemvalue="click" />
+            <item index="5" class="java.lang.String" itemvalue="importlib-metadata" />
+            <item index="6" class="java.lang.String" itemvalue="Jinja2" />
+            <item index="7" class="java.lang.String" itemvalue="Flask" />
+          </list>
+        </value>
+      </option>
+    </inspection_tool>
+  </profile>
+</component>

+ 403 - 288
contactsapp/app.go

@@ -5,8 +5,10 @@ import (
 	"errors"
 	"fmt"
 	"html/template"
+	"io"
 	"log"
 	"net/http"
+	"net/url"
 	"strconv"
 	"strings"
 
@@ -14,17 +16,18 @@ import (
 )
 
 const (
-	App       = "contactsapp"
+	App       = "Contacts.app"
 	SecretKey = "hypermedia rocks"
 )
 
-func MakeTemplate(names ...string) *template.Template {
-	names = append([]string{"layout"}, names...)
-	paths := make([]string, len(names))
-	for i, name := range names {
+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)
 	}
-	tpl := template.Must(template.New("layout.html").
+	// 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 {
@@ -36,321 +39,431 @@ func MakeTemplate(names ...string) *template.Template {
 	return tpl
 }
 
-/*
-@app.route("/contacts")
-def contacts():
-*/
-func RouteContacts(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" {
-			MakeTemplate("rows").Execute(w, anyMap{"contacts": contactsSet})
-			return
-		}
-	} else {
-		contactsSet = (&Contact{}).All(page)
-	}
-	MakeTemplate(
-		"index",
-		"archive_ui",
-		"rows",
-	).Execute(w, anyMap{
-		"contacts": contactsSet,
-		"search":   search,
-		"page":     page,
-		"archiver": NewArchiver(),
-	})
-}
-
 var (
 	/*
 	   @app.route("/")
 	   def index():
 	*/
 	RouteFront = http.RedirectHandler("/contacts", http.StatusFound)
+)
 
-	/*
-		@app.route("/contacts/archive", methods=["POST"])
-		def start_archive():
-	*/
-	MakeStartArchive = func(templates *template.Template) http.HandlerFunc {
-		return func(w http.ResponseWriter, r *http.Request) {
-			archiver := NewArchiver()
-			archiver.Run()
-			templates.ExecuteTemplate(w, "archive_ui.html", anyMap{"archiver": archiver})
+/*
+@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
 		}
-	}
-
-	/*
-		@app.route("/contacts/archive", methods=["GET"])
-		def archive_status():
-	*/
-	MakeArchiveStatus = func(templates *template.Template) http.HandlerFunc {
-		return func(w http.ResponseWriter, r *http.Request) {
-			archiver := NewArchiver()
-			templates.ExecuteTemplate(w, "archive_ui.html", anyMap{"archiver": archiver})
+		var contactsSet []Contact
+		if search != "" {
+			contactsSet = (&Contact{}).Search(search)
+			if r.Header.Get("HX-Trigger") == "search" {
+				MakeTemplate(
+					"layout",
+					"rows",
+				).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/file", methods=["GET"])
-		def archive_content():
-	*/
-	ArchiveContent http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
-		archiver := NewArchiver()
-		fileName := archiver.ArchiveFile()
-		w.Header().Set("Content-Disposition", fmt.Sprintf(`attachement; filename="%s"`, fileName))
-		http.ServeFile(w, r, fileName)
+/*
+@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=["DELETE"])
-		def reset_archive():
-	*/
-	MakeResetArchive = func(templates *template.Template) http.HandlerFunc {
-		return func(w http.ResponseWriter, r *http.Request) {
-			archiver := NewArchiver()
-			archiver.Reset()
-			templates.ExecuteTemplate(w, "archive_ui.html", 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/count")
-		def contacts_count():
-	*/
-	ContactsCount http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
-		count := (&Contact{}).Count()
-		_, _ = fmt.Fprintf(w, "(%d total Contacts)", count)
+/*
+@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/new", methods=['GET'])
-		def contacts_new_get():
-	*/
-	MakeContactsNewGet = func(templates *template.Template) http.HandlerFunc {
-		return func(w http.ResponseWriter, r *http.Request) {
-			templates.ExecuteTemplate(w, "new.html", anyMap{"contact": NewContact(nil)})
+/*
+@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():
-	*/
-	MakeContactsNew = func(templates *template.Template) http.HandlerFunc {
-		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 {
-				templates.ExecuteTemplate(w, "new.html", anyMap{"contact": c})
-			}
+/*
+@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):
-	*/
-	MakeContactsView = func(templates *template.Template) http.HandlerFunc {
-		return func(w http.ResponseWriter, r *http.Request) {
-			contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
-			contact := (&Contact{}).Find(uint64(contact_id))
-			templates.ExecuteTemplate(w, "show.html", anyMap{"contact": contact})
+/*
+@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):
-	*/
-	MakeContactsEditGet = func(templates *template.Template) http.HandlerFunc {
-		return func(w http.ResponseWriter, r *http.Request) {
-			contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
-			contact := (&Contact{}).Find(uint64(contact_id))
-			MakeTemplate(
-				"edit",
-			).
-				Execute(w, anyMap{"contact": contact})
-		}
+/*
+@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):
-	*/
-	MakeContactsEditPost = func(templates *template.Template) http.HandlerFunc {
-		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 {
-				templates.ExecuteTemplate(w, "edit.html", anyMap{"contact": c})
-			}
+/*
+@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):
-	*/
-	ContactsEmailGet http.HandlerFunc = func(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 {
-			err := err.Error()
-			if strings.Contains(err, "email") {
-				fmt.Fprintln(w, err)
-			}
+/*
+@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/<contact_id>", methods=["DELETE"])
-		def contacts_delete(contact_id=0):
-	*/
-	ContactsDelete http.HandlerFunc = func(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)
+/*
+@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
-		} else {
+		}
+		body, err := io.ReadAll(r.Body)
+		if err != nil {
+			http.Error(w, fmt.Sprintf("reading request body: %v", err), http.StatusBadRequest)
 			return
 		}
-	}
-
-	/*
-		@app.route("/contacts/", methods=["DELETE"])
-		def contacts_delete_all():
-	*/
-	MakeContactsDeleteAll = func(templates *template.Template) http.HandlerFunc {
-		return func(w http.ResponseWriter, r *http.Request) {
-			sContactIDs := strings.Split(r.FormValue("selected_contact_ids"), ",")
-			for _, sContactID := range sContactIDs {
-				if contactID, err := strconv.Atoi(sContactID); contactID > 0 && err == nil {
-					contact := (&Contact{}).Find(uint64(contactID))
-					contact.Delete()
-				}
+		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)
-			templates.ExecuteTemplate(w, "index.html", anyMap{"contacts": contacts_set})
+		}
+		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():
-	*/
-	JSONContacts http.HandlerFunc = func(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 {
+/*
+	# ===========================================================
+	# 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", methods=["POST"])
-		def json_contacts_new():
-	*/
-	JSONContactsNew http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
-		c := NewContact(anyMap{
-			"first": r.FormValue("first_°name"),
-			"last":  r.FormValue("last_name"),
-			"phone": r.FormValue("phone"),
-			"email": r.FormValue("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=["GET"])
-		def json_contacts_view(contact_id=0):
-	*/
-	JSONContactsEdit http.HandlerFunc = 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"),
-		)
-		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=["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):
-	*/
-	JSONContactsDelete http.HandlerFunc = func(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})
-	}
-)
+/*
+@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})
+}
 
 /*
 *
@@ -361,26 +474,28 @@ func flash(message string) {
 }
 
 func setupRoutes(mux *http.ServeMux, templates *template.Template) {
+	// OK
 	mux.Handle("/", RouteFront)
 	mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
-	mux.HandleFunc("/contacts", RouteContacts)
-	mux.Handle("POST /contacts/archive", MakeStartArchive(templates))
-	mux.Handle("GET /contacts/archive", MakeArchiveStatus(templates))
-	mux.Handle("GET /contacts/archive/file", ArchiveContent)
-	mux.Handle("DELETE /contacts/archive", MakeResetArchive(templates))
-	mux.Handle("GET /contacts/count", ContactsCount)
-	mux.Handle("GET /contacts/new", MakeContactsNewGet(templates))
-	mux.Handle("POST /contacts/new", MakeContactsNew(templates))
-	mux.Handle("GET /contacts/{contact_id}", MakeContactsView(templates))
-	mux.Handle("GET /contacts/{contact_id}/edit", MakeContactsEditGet(templates))
-	mux.Handle("POST /contacts/{contact_id}/edit", MakeContactsEditPost(templates))
-	mux.Handle("GET /contacts/{contact_id}/email", ContactsEmailGet)
-	mux.Handle("DELETE /contacts/{contact_id}", ContactsDelete)
-	mux.Handle("DELETE /contacts/", MakeContactsDeleteAll(templates))
-	mux.Handle("GET /api/v1/contacts", JSONContacts)
-	mux.Handle("POST /api/v1/contacts", JSONContactsNew)
-	mux.Handle("GET /api/v1/contacts/{contact_id}", JSONContactsEdit)
-	mux.Handle("DELETE /api/v1/contacts/{contact_id}", JSONContactsDelete)
+	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() {

文件差异内容过多而无法显示
+ 0 - 0
contactsapp/contacts.json


+ 1 - 1
contactsapp/contacts0.json

@@ -4,7 +4,7 @@
     "first": "Carson",
     "last": "Gross",
     "phone": "123-456-7890",
-    "email": "carson@example.comz",
+    "email": "carson@example.com",
     "errors": {}
   },
   {

+ 27 - 19
contactsapp/contacts_model.go

@@ -30,12 +30,12 @@ var db = make(map[uint64]Contact, 0)
 class Contact:
 */
 type Contact struct {
-	ID     uint64
-	First  string
-	Last   string
-	Phone  string
-	Email  string
-	Errors []error `json:"-"`
+	ID     uint64  `json:"id"`
+	First  string  `json:"first"`
+	Last   string  `json:"last"`
+	Phone  string  `json:"phone"`
+	Email  string  `json:"email"`
+	Errors []error `json:"errors"`
 }
 
 /*
@@ -47,19 +47,19 @@ func NewContact(args anyMap) *Contact {
 	}
 	// Note: the "ok" are for the type assertions. The values are already set
 	// to the 0V (or a slice for errors) at this point anyway.
-	if id, ok := args["ID"].(uint64); ok {
+	if id, ok := args["id"].(uint64); ok {
 		c.ID = id
 	}
-	if first, ok := args["First"].(string); ok {
+	if first, ok := args["first"].(string); ok {
 		c.First = first
 	}
-	if last, ok := args["list"].(string); ok {
+	if last, ok := args["last"].(string); ok {
 		c.Last = last
 	}
-	if phone, ok := args["Phone"].(string); ok {
+	if phone, ok := args["phone"].(string); ok {
 		c.Phone = phone
 	}
-	if email, ok := args["Email"].(string); ok {
+	if email, ok := args["email"].(string); ok {
 		c.Email = email
 	}
 	return &c
@@ -71,7 +71,7 @@ def __str__(self):
 func (self *Contact) String() string {
 	bs, err := json.Marshal(*self)
 	if err != nil {
-		log.Println(err)
+		log.Printf("Error marshaling contact: %v", err)
 	}
 	return string(bs)
 }
@@ -108,6 +108,7 @@ def save(self):
 func (self *Contact) Save() bool {
 	if !self.validate() {
 		return false
+
 	}
 	var maxID uint64
 	if self.ID == 0 {
@@ -122,6 +123,7 @@ func (self *Contact) Save() bool {
 		}
 		self.ID = maxID + 1
 	}
+	db[self.ID] = *self
 	SaveDB()
 	return true
 }
@@ -250,6 +252,10 @@ const (
 	statusComplete = "Complete"
 )
 
+var (
+	archiver *Archiver
+)
+
 /*
 def status(self):
 */
@@ -287,15 +293,14 @@ def run_impl(self):
 func (self *Archiver) RunImpl() {
 	t0 := time.Now()
 	for i := range 10 {
-		delay := time.Duration(rand.Float64() * float64(time.Second))
-		time.Sleep(delay)
+		time.Sleep(time.Duration(float64(time.Second) * rand.Float64()))
 		self.Lock()
 		if self.archiveStatus != statusRunning {
 			self.Unlock()
 			return
 		}
-		self.archiveProgress = float64(i+1) / 10.0
-		log.Printf("Here... after delay %v, cum %v: %f", delay, time.Since(t0), self.archiveProgress)
+		self.archiveProgress = float64(i+1) / 10
+		log.Printf("Here... after %v: %f", time.Since(t0), self.archiveProgress)
 		self.Unlock()
 	}
 	time.Sleep(time.Second)
@@ -330,8 +335,11 @@ func (self *Archiver) Reset() {
 @classmethod
 def get(cls):
 */
-func NewArchiver() *Archiver {
-	return &Archiver{
-		archiveStatus: statusWaiting,
+func GetArchiver() *Archiver {
+	if archiver == nil {
+		archiver = &Archiver{
+			archiveStatus: statusWaiting,
+		}
 	}
+	return archiver
 }

+ 11 - 0
contactsapp/static/js/debug.js

@@ -0,0 +1,11 @@
+htmx.defineExtension('debug', {
+  onEvent: function (name, evt) {
+    if (console.debug) {
+      console.debug(name, evt);
+    } else if (console) {
+      console.log("DEBUG:", name, evt);
+    } else {
+      throw "NO CONSOLE SUPPORTED"
+    }
+  }
+});

+ 1 - 1
contactsapp/templates/archive_ui.gohtml

@@ -9,7 +9,7 @@
         <div hx-get="/contacts/archive" hx-trigger="load delay:500ms">
           Creating Archive...{{ printf "%15.5f" .archiver.Progress }}
           <div class="progress">
-            <div id="archive-progress" class="progress-bar" style="width:{{ mul .archiver.Progress 100 }}%"></div>
+            <div id="archive-progress" class="progress-bar" style="width:{{ mulf .archiver.Progress 100 }}%"></div>
           </div>
         </div>
       {{ else if eq .archiver.Status "Complete" }}

+ 1 - 1
contactsapp/templates/edit.gohtml

@@ -14,7 +14,7 @@
                            hx-get="/contacts/{{ .contact.ID }}/email" hx-target="next .error"
                            hx-trigger="change, keyup delay:200ms"
                            placeholder="Email" value="{{ .contact.Email }}">
-{{/*                    <span class="error">{{ .contact.Errors.email }}</span>*/}}
+                    <span class="error">{{/* {{ .contact.Errors.email }} */}}</span>
                 </p>
                 <p>
                     <label for="first_name">First Name</label>

+ 55 - 54
contactsapp/templates/index.gohtml

@@ -1,62 +1,63 @@
 {{define "index.html"}}
-{{ block "content" . }}
+    {{ block "content" . }}
 
-    {{ template "archive_ui.html" . }}
+        {{ template "archive_ui.html" . }}
 
-    <form action="/contacts" method="get" class="tool-bar">
-        <label for="search">Search Term</label>
-        <input id="search" type="search" name="q" value="{{ .search }}"
-               hx-get="/contacts"
-               hx-trigger="search, keyup delay:200ms changed"
-               hx-target="tbody"
-               hx-push-url="true"
-               hx-indicator="#spinner"/>
-        <img style="height: 20px" id="spinner" class="htmx-indicator" src="/static/img/spinning-circles.svg"/>
-        <input type="submit" value="Search"/>
-    </form>
+        <form action="/contacts" method="get" class="tool-bar">
+          <label for="search">Search Term</label>
+          <input id="search" type="search" name="q" value="{{ .search }}"
+                 hx-get="/contacts"
+                 hx-trigger="search, keyup delay:200ms changed"
+                 hx-target="tbody"
+                 hx-push-url="true"
+                 hx-indicator="#spinner"/>
+          <img style="height: 20px" id="spinner" class="htmx-indicator" src="/static/img/spinning-circles.svg"/>
+          <input type="submit" value="Search"/>
+        </form>
 
-    <form x-data="{ selected: [] }">
-    <template
-        x-if="selected.length > 0">
-        <div class="box info tool-bar flxed top">
-            <slot x-text="selected.length"></slot>
-            contacts selected
-            
-            <button type="button" class="bad bg color border"
-                @click="confirm(`Delete ${selected.length} contacts?`) &&
-                    htmx.ajax('DELETE', '/contacts', { source: $root, target: document.body })"
-            >Delete</button>
-            <hr aria-orientation="vertical">
-            <button type="button" @click="selected = []">Cancel</button> 
-        </div>
-    </template>
-    <table>
-        <thead>
-        <tr>
-            <th></th>
-            <th>First</th>
-            <th>Last</th>
-            <th>Phone</th>
-            <th>Email</th>
-            <th></th>
-        </tr>
-        </thead>
-        <tbody>
-        {{ template "rows.html" . }}
-        </tbody>
-    </table>
-        <button hx-delete="/contacts"
-                hx-confirm="Are you sure you want to delete these contacts?"
-                hx-target="body">
+        <form x-data="{ selected: [] }">
+          <template
+                  x-if="selected.length > 0">
+            <div class="box info tool-bar flxed top">
+              <slot x-text="selected.length"></slot>
+              contacts selected
+
+              <button type="button" class="bad bg color border"
+                      @click="confirm(`Delete ${selected.length} contacts?`) &&
+                    htmx.ajax('DELETE', '/contacts/', { source: $root, target: document.body })"
+              >Delete
+              </button>
+              <hr aria-orientation="vertical">
+              <button type="button" @click="selected = []">Cancel</button>
+            </div>
+          </template>
+          <table>
+            <thead>
+            <tr>
+              <th></th>
+              <th>First</th>
+              <th>Last</th>
+              <th>Phone</th>
+              <th>Email</th>
+              <th></th>
+            </tr>
+            </thead>
+            <tbody>
+            {{ template "rows.html" . }}
+            </tbody>
+          </table>
+          <button hx-delete="/contacts"
+                  hx-confirm="Are you sure you want to delete these contacts?"
+                  hx-target="body">
             Delete Selected Contacts
-        </button>
-    </form>
-    <p>
-        <a href="/contacts/new">Add Contact</a>
-        <span hx-get="/contacts/count" hx-trigger="revealed">
-          <img id="spinner" style="height: 20px"  class="htmx-indicator" src="/static/img/spinning-circles.svg"/>
+          </button>
+        </form>
+        <p>
+          <a href="/contacts/new">Add Contact</a>
+          <span hx-get="/contacts/count" hx-trigger="revealed">
+          <img id="spinner" style="height: 20px" class="htmx-indicator" src="/static/img/spinning-circles.svg"/>
         </span>
-    </p>
+        </p>
 
-{{ end }}
+    {{ end }}
 {{end}}

+ 1 - 0
contactsapp/templates/layout.gohtml

@@ -8,6 +8,7 @@
     <link rel="stylesheet" href="/static/site.css">
     <script src="/static/js/htmx-1.8.0.js"></script>
     <script src="/static/js/_hyperscript-0.9.7.js"></script>
+    <script src="/static/js/debug.js"></script>
     <script src="/static/js/rsjs-menu.js" type="module"></script>
     <!-- script defer src="https://unpkg.com/alpinejs@3/dist/cdn.min.js"></script -->
     <script defer src="https://unpkg.com/alpinejs@3.13.10/dist/cdn.min.js"></script>

+ 23 - 27
contactsapp/templates/new.gohtml

@@ -1,41 +1,37 @@
 {{define "new.html"}}
-{% extends 'layout.html' %}
+    {{ block "content" . }}
 
-{{ block "content" . }}
-
-<form action="/contacts/new" method="post">
-    <fieldset>
-        <legend>Contact Values</legend>
-        <div class="table rows">
+      <form action="/contacts/new" method="post">
+        <fieldset>
+          <legend>Contact Values</legend>
+          <div class="table rows">
             <p>
-                <label for="email">Email</label>
-                <input name="email" id="email" type="text" placeholder="Email" value="{{ .contact.email }}">
-                <span class="error">{{ .contact.errors.email }}</span>
+              <label for="email">Email</label>
+              <input name="email" id="email" type="text" placeholder="Email" value="{{ .contact.Email }}">
+                {{/*                <span class="error">{{ .contact.errors.email }}</span>*/}}
             </p>
             <p>
-                <label for="first_name">First Name</label>
-                <input name="first_name" id="first_name" type="text" placeholder="First Name" value="{{ .contact.first }}">
-                <span class="error">{{ .contact.errors.first }}</span>
+              <label for="first_name">First Name</label>
+              <input name="first_name" id="first_name" type="text" placeholder="First Name" value="{{ .contact.First }}">
+                {{/*                <span class="error">{{ .contact.errors.first }}</span>*/}}
             </p>
             <p>
-                <label for="last_name">Last Name</label>
-                <input name="last_name" id="last_name" type="text" placeholder="Last Name" value="{{ .contact.last }}">
-                <span class="error">{{ .contact.errors.last }}</span>
+              <label for="last_name">Last Name</label>
+              <input name="last_name" id="last_name" type="text" placeholder="Last Name" value="{{ .contact.Last }}">
+                {{/*                <span class="error">{{ .contact.errors.last }}</span>*/}}
             </p>
             <p>
-                <label for="phone">Phone</label>
-                <input name="phone" id="phone" type="text" placeholder="Phone" value="{{ .contact.phone }}">
-                <span class="error">{{ .contact.errors.phone }}</span>
+              <label for="phone">Phone</label>
+              <input name="phone" id="phone" type="text" placeholder="Phone" value="{{ .contact.Phone }}">
+                {{/*                <span class="error">{{ .contact.errors.phone }}</span>*/}}
             </p>
-        </div>
-        <button>Save</button>
-    </fieldset>
-</form>
+          </div>
+          <button>Save</button>
+        </fieldset>
+      </form>
 
-<p>
-    <a href="/contacts">Back</a>
-</p>
+      <p><a href="/contacts">Back</a></p>
 
 
-{{ end }}
+    {{ end }}
 {{end}}

+ 13 - 18
contactsapp/templates/show.gohtml

@@ -1,20 +1,15 @@
 {{define "show.html"}}
-{% extends 'layout.html' %}
-
-{{ block "content" . }}
-
-<h1>{{ .contact.first}} {{ .contact.last}}</h1>
-
-<div>
-    <div>Phone: {{ .contact.phone}}</div>
-    <div>Email: {{ .contact.email}}</div>
-</div>
-
-<p>
-    <a href="/contacts/{{ .contact.id}}/edit">Edit</a>
-    <a href="/contacts">Back</a>
-</p>
-
-
-{{ end }}
+    {{ block "content" . }}
+      <h1>{{ .contact.First}} {{ .contact.Last}}</h1>
+
+      <div>
+        <div>Phone: {{ .contact.Phone}}</div>
+        <div>Email: {{ .contact.Email}}</div>
+      </div>
+
+      <p>
+        <a href="/contacts/{{ .contact.ID}}/edit">Edit</a>
+        <a href="/contacts">Back</a>
+      </p>
+    {{ end }}
 {{end}}

部分文件因为文件数量过多而无法显示