From b2db89fc98b99e7792362688b59efa5280b694a2 Mon Sep 17 00:00:00 2001
From: Gregor Best <gbe@unobtanium.de>
Date: Tue, 20 Apr 2021 23:10:23 +0200
Subject: [PATCH] Add comments

---
 handler-details.go           | 14 +++++++++++
 migrations/0002-comments.sql |  5 ++++
 templates/details.tpl        | 19 ++++++++++++---
 vino.go                      | 45 +++++++++++++++++++++++++++++++++---
 4 files changed, 77 insertions(+), 6 deletions(-)
 create mode 100644 migrations/0002-comments.sql

diff --git a/handler-details.go b/handler-details.go
index a19f0f7..634627a 100644
--- a/handler-details.go
+++ b/handler-details.go
@@ -9,6 +9,7 @@ import (
 	"log"
 	"net/http"
 	"strconv"
+	"strings"
 )
 
 var detailsTemplate = template.Must(template.ParseFS(templateFS, "templates/base.tpl", "templates/details.tpl"))
@@ -102,6 +103,19 @@ func (h Handler) detailsPost(w http.ResponseWriter, r *http.Request) {
 	v.Name = name
 	v.Rating = rating
 
+	comment := strings.TrimSpace(r.FormValue("comment"))
+	if comment != "" {
+		if len(comment) > 1024 {
+			comment = comment[:1024] + "..."
+		}
+
+		err = v.StoreComment(r.Context(), h.db, comment)
+		if err != nil {
+			httpError(w, "can't add comment", err, http.StatusInternalServerError)
+			return
+		}
+	}
+
 	err = r.ParseMultipartForm(16 * 1024 * 1024)
 	if err != nil {
 		httpError(w, "can't parse multipart form data", err, http.StatusInternalServerError)
diff --git a/migrations/0002-comments.sql b/migrations/0002-comments.sql
new file mode 100644
index 0000000..14a5e59
--- /dev/null
+++ b/migrations/0002-comments.sql
@@ -0,0 +1,5 @@
+CREATE TABLE comments (
+	content TEXT,
+	wine INTEGER,
+	FOREIGN KEY (wine) REFERENCES wines(rowid)
+);
\ No newline at end of file
diff --git a/templates/details.tpl b/templates/details.tpl
index a59053c..a16e783 100644
--- a/templates/details.tpl
+++ b/templates/details.tpl
@@ -35,14 +35,27 @@
 					<input id="form-new-picture" type="file" name="picture" accept="image/png, image/jpeg">
 					<!-- TODO: add a button to remove the image -->
 				</div>
+
+				<div class="pure-control-group">
+					<label for="form-new-comment">Add comment</label>
+					<textarea id="form-new-comment" name="comment" placeholder="Etaoin Shrdlu">
+					</textarea>
+				</div>
 			</fieldset>
 
-			<details>
-				Etaoin Shrdlu, here be metadata
+			{{ if .Wine.Comments }}
+			<details open>
+				<summary>Comments</summary>
+				<ul>
+					{{ range .Wine.Comments }}
+					<li>{{ .Content }}</li>
+					{{ end }}
+				</ul>
 			</details>
+			{{ end }}
 
 			{{ if .Wine.HasPicture }}
-			<p>And here's a picture:<br/>
+			<p>Here's a picture:<br/>
 
 			<img class="foto pure-img" src="/details/img?id={{ .Wine.ID }}"/>
 			{{ end }}
diff --git a/vino.go b/vino.go
index 4d84c31..8922127 100644
--- a/vino.go
+++ b/vino.go
@@ -26,12 +26,17 @@ const (
 	Update
 )
 
+type Comment struct {
+	Content string `db:"content"`
+}
+
 type Vino struct {
 	ID         int         `db:"rowid"`
 	Name       string      `db:"name"`
 	Rating     int         `db:"rating"`
 	Picture    image.Image `db:"-"`
 	HasPicture bool        `db:"has_picture"` // Set to true if there's picture data for this vino
+	Comments   []Comment   `db:"-"`
 }
 
 func DeleteVino(ctx context.Context, db *sqlx.DB, id int) error {
@@ -51,7 +56,10 @@ func DeleteVino(ctx context.Context, db *sqlx.DB, id int) error {
 }
 
 func LoadVino(ctx context.Context, db *sqlx.DB, id int) (Vino, error) {
-	cols := []string{"rowid", "name", "rating", "case when picture is not null then true else false end as has_picture"}
+	cols := []string{
+		"rowid", "name", "rating",
+		"CASE WHEN picture IS NOT NULL THEN true ELSE false END AS has_picture",
+	}
 
 	query, args, err := squirrel.Select(cols...).
 		From("wines").
@@ -61,8 +69,28 @@ func LoadVino(ctx context.Context, db *sqlx.DB, id int) (Vino, error) {
 		return Vino{}, err
 	}
 
+	tx, err := db.Beginx()
+	if err != nil {
+		return Vino{}, err
+	}
+	defer tx.Rollback() // The tx is readonly anyway, no need to commit anything
+
 	var v Vino
-	err = db.GetContext(ctx, &v, query, args...)
+	err = tx.GetContext(ctx, &v, query, args...)
+	if err != nil {
+		return v, err
+	}
+
+	query, args, err = squirrel.Select("content").
+		From("comments").
+		Where(squirrel.Eq{"wine": id}).
+		OrderBy("rowid ASC").
+		ToSql()
+	if err != nil {
+		return v, err
+	}
+
+	err = tx.SelectContext(ctx, &v.Comments, query, args...)
 	if err != nil {
 		return v, err
 	}
@@ -85,7 +113,7 @@ func LoadPictureData(ctx context.Context, db *sqlx.DB, id int) ([]byte, error) {
 // AddPicture loads picture data (PNG or JPEG) from fh and sets v's picture to it.
 // If something goes wrong during loading, or the image is neither PNG nor JPEG, an error
 // is returned. If contentType is not the empty string, it is validated to be either
-// image/png or image/jpeg
+// image/png or image/jpeg.
 func (v *Vino) AddPicture(fh io.Reader, contentType string) error {
 	switch contentType {
 	case "", "image/jpeg", "image/png":
@@ -196,6 +224,17 @@ func (v *Vino) Store(ctx context.Context, db *sqlx.DB, op storeOperation) (err e
 	return nil
 }
 
+func (v *Vino) StoreComment(ctx context.Context, db *sqlx.DB, comment string) error {
+	query := `INSERT INTO comments (content, wine) VALUES (?, ?)`
+
+	_, err := db.ExecContext(ctx, query, comment, v.ID)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
 func ListWines(ctx context.Context, db *sqlx.DB) ([]Vino, error) {
 	query := `SELECT rowid, name, rating FROM wines ORDER BY rating DESC;`
 
-- 
GitLab