From 687e522e19c42cab18c16d6dc3d1aba6974b68c6 Mon Sep 17 00:00:00 2001
From: Gregor Best <gbe@unobtanium.de>
Date: Tue, 20 Apr 2021 21:18:32 +0200
Subject: [PATCH] Make update/store more clear

---
 handler-details.go          | 60 +++++++++++++++++++++++++++++++++++++
 handler-index.go            |  7 +----
 main.go                     |  9 ++++--
 migrations/0001-initial.sql |  5 ++--
 templates/details.tpl       |  9 ++++--
 templates/index.tpl         |  4 +--
 vino.go                     | 42 +++++++++++++++++++++-----
 7 files changed, 112 insertions(+), 24 deletions(-)

diff --git a/handler-details.go b/handler-details.go
index 91286ab..d486644 100644
--- a/handler-details.go
+++ b/handler-details.go
@@ -3,6 +3,7 @@ package main
 import (
 	"bytes"
 	"errors"
+	"fmt"
 	"html/template"
 	"io"
 	"log"
@@ -52,7 +53,66 @@ func (h Handler) img(w http.ResponseWriter, r *http.Request) {
 	log.Printf("wrote image for %d: %d bytes", id, n)
 }
 
+func (h Handler) detailsPost(w http.ResponseWriter, r *http.Request) {
+	log.Println("handling", r.Method, r.URL, "from", r.RemoteAddr)
+
+	action := r.FormValue("action")
+	switch action {
+	case "save", "delete":
+	default:
+		httpError(w, "invalid parameters", fmt.Errorf("action missing or unknown: %q", action), http.StatusBadRequest)
+		return
+	}
+
+	id, err := strconv.Atoi(r.FormValue("id"))
+	if err != nil {
+		httpError(w, "can't parse id", err, http.StatusBadRequest)
+		return
+	}
+
+	if action == "delete" {
+		err = DeleteVino(r.Context(), h.db, id)
+		if err != nil {
+			httpError(w, "can't delete wine", err, http.StatusInternalServerError)
+			return
+		}
+
+		http.Redirect(w, r, "/", http.StatusSeeOther)
+		return
+	}
+
+	name := r.FormValue("name")
+	if name == "" {
+		httpError(w, "name can't be empty", nil, http.StatusBadRequest)
+		return
+	}
+
+	v, err := LoadVino(r.Context(), h.db, id)
+	if err != nil {
+		// Only log error here, just create a new entry.
+		log.Println("can't load vino with id", id, ":", err)
+	}
+
+	v.Name = name
+
+	err = v.Store(r.Context(), h.db, Update)
+	if err != nil {
+		httpError(w, "can't store wine", err, http.StatusInternalServerError)
+		return
+	}
+
+	// TODO: Redirect to newly created/updated vino
+
+	http.Redirect(w, r, "/details?id="+r.FormValue("id"), http.StatusSeeOther)
+	return
+}
+
 func (h Handler) details(w http.ResponseWriter, r *http.Request) {
+	if r.Method == "POST" {
+		h.detailsPost(w, r)
+		return
+	}
+
 	log.Println("handling", r.Method, r.URL, "from", r.RemoteAddr)
 
 	if r.Method != "GET" {
diff --git a/handler-index.go b/handler-index.go
index a1ac263..aec3cd5 100644
--- a/handler-index.go
+++ b/handler-index.go
@@ -46,11 +46,6 @@ func (h Handler) index(w http.ResponseWriter, r *http.Request) {
 	}
 
 	name := r.FormValue("name")
-	if name == "" {
-		httpError(w, "bad name", errors.New("empty or missing"), http.StatusBadRequest)
-		return
-	}
-
 	if len(name) > 80 {
 		httpError(w, "bad name", errors.New("name too long, max length is 80"), http.StatusBadRequest)
 		return
@@ -112,7 +107,7 @@ func (h Handler) index(w http.ResponseWriter, r *http.Request) {
 		vino.Picture = img
 	}
 
-	err = vino.Store(r.Context(), h.db)
+	err = vino.Store(r.Context(), h.db, Add)
 	if err != nil {
 		httpError(w, fmt.Sprintf("can't store wine %q", vino), err, http.StatusInternalServerError)
 		return
diff --git a/main.go b/main.go
index b9a914c..0ab4957 100644
--- a/main.go
+++ b/main.go
@@ -20,8 +20,13 @@ type Handler struct {
 }
 
 func httpError(w http.ResponseWriter, msg string, err error, status int) {
-	log.Println(msg+":", err)
-	http.Error(w, msg+": "+err.Error(), status)
+	if err != nil {
+		msg += ": " + err.Error()
+	}
+
+	log.Println(msg)
+
+	http.Error(w, msg, status)
 }
 
 func main() {
diff --git a/migrations/0001-initial.sql b/migrations/0001-initial.sql
index 9e41275..29d7eaf 100644
--- a/migrations/0001-initial.sql
+++ b/migrations/0001-initial.sql
@@ -1,6 +1,5 @@
 CREATE TABLE wines (
 	name TEXT,
-	rating INT, -- number of stars
-	picture BINARY, -- jpeg/png image of the label on the bottle
-	UNIQUE(name)
+	rating INT, 		-- number of stars
+	picture BINARY	-- jpeg/png image of the label on the bottle
 );
\ No newline at end of file
diff --git a/templates/details.tpl b/templates/details.tpl
index 73609da..9e2d3a4 100644
--- a/templates/details.tpl
+++ b/templates/details.tpl
@@ -33,9 +33,12 @@
 
 	<footer class="small pure-u-1">
 		<div class="maxwidth box">
-			<!-- TODO: give these buttons values so that the Go code can decide what to do -->
-			<button type="submit" class="pure-button pure-button-primary">Save Changes</button>
-			<button type="submit" class="right pure-button button-warning">Delete</button>
+			<button type="submit" name="action" value="save" class="pure-button pure-button-primary">
+				Save Changes
+			</button>
+			<button type="submit" name="action" value="delete" class="right pure-button button-warning">
+				Delete
+			</button>
 		</div>
 	</footer>
 </form>
diff --git a/templates/index.tpl b/templates/index.tpl
index d7009d3..d81ad15 100644
--- a/templates/index.tpl
+++ b/templates/index.tpl
@@ -10,7 +10,7 @@
 			<tbody>
 			{{ range .Wines }}
 				<tr>
-					<td>{{ .ID }}</td>
+					<td><a href="/details?id={{ .ID }}">{{ .ID }}</a></td>
 					<td>{{ .Rating }}</td>
 					<td><a href="/details?id={{ .ID }}">{{ .Name }}</a></td>
 				</tr>
@@ -27,7 +27,7 @@
 				<div class="pure-g">
 					<div class="pure-u-3-4">
 						<div class="pad-bottom pad-right">
-							<input class="name" type="text" name="name" placeholder="Name" required>
+							<input class="name" type="text" name="name" placeholder="Name">
 						</div>
 						<div class="upload-btn-wrapper">
 							<button class="pure-button">Select Picture</button>
diff --git a/vino.go b/vino.go
index af960eb..eb4f248 100644
--- a/vino.go
+++ b/vino.go
@@ -13,6 +13,13 @@ import (
 	"golang.org/x/image/draw"
 )
 
+type storeOperation int
+
+const (
+	Add storeOperation = iota
+	Update
+)
+
 type Vino struct {
 	ID      int         `db:"rowid"`
 	Name    string      `db:"name"`
@@ -20,6 +27,22 @@ type Vino struct {
 	Picture image.Image `db:"-"`
 }
 
+func DeleteVino(ctx context.Context, db *sqlx.DB, id int) error {
+	query, args, err := squirrel.Delete("wines").
+		Where(squirrel.Eq{"rowid": id}).
+		ToSql()
+	if err != nil {
+		return err
+	}
+
+	_, err = db.ExecContext(ctx, query, args...)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
 func LoadVino(ctx context.Context, db *sqlx.DB, id int) (Vino, error) {
 	cols := []string{"rowid", "name", "rating"}
 
@@ -56,12 +79,14 @@ func (v Vino) String() string {
 	return fmt.Sprintf("{Name: %q, Rating: %d}", v.Name, v.Rating)
 }
 
-func (v Vino) Store(ctx context.Context, db *sqlx.DB) (err error) {
+func (v Vino) Store(ctx context.Context, db *sqlx.DB, op storeOperation) (err error) {
 	values := map[string]interface{}{
 		"name":   v.Name,
 		"rating": v.Rating,
 	}
 
+	log.Println("image data:", values)
+
 	if v.Picture != nil {
 		// Scale image down and encode as PNG
 		// Get aspect ratio of incoming picture
@@ -107,20 +132,21 @@ func (v Vino) Store(ctx context.Context, db *sqlx.DB) (err error) {
 		}
 	}()
 
-	_, err = squirrel.Insert("wines").
-		SetMap(values).
-		RunWith(tx).
-		ExecContext(ctx)
-
-	if err != nil {
+	if op == Update {
 		// Looks like we need to do an update instead
 		_, err = squirrel.Update("wines").
-			Where("name = ?", v.Name).
+			Where("rowid = ?", v.ID).
 			SetMap(values).
 			RunWith(tx).
 			ExecContext(ctx)
+		return err
 	}
 
+	_, err = squirrel.Insert("wines").
+		SetMap(values).
+		RunWith(tx).
+		ExecContext(ctx)
+
 	return err
 }
 
-- 
GitLab