diff --git a/handler-details.go b/handler-details.go index 023380a9dd745c0532e39596e1d49046d19f3081..23399dd62718ea85b99e23fe6d24d80bce193416 100644 --- a/handler-details.go +++ b/handler-details.go @@ -174,7 +174,7 @@ func (h Handler) detailsPost(w http.ResponseWriter, r *http.Request) { }, Country: sql.NullString{ String: country.String(), - Valid: true, + Valid: country.Valid(), }, } diff --git a/storage/query/query.sql b/storage/query/query.sql index 9258a34dd4ed73485005e415ce40c69091759731..672c3f29d900be6f2a373301700e37cad634be64 100644 --- a/storage/query/query.sql +++ b/storage/query/query.sql @@ -65,7 +65,7 @@ select wine_id, name, rating, country, cast(picture is not null as bool) as has_ where wine_id = @wine_id; -- name: ListWines :many -select wine_id, name, country, cast(picture is not null as bool) as has_picture from wines; +select wine_id, name, rating, country, cast(picture is not null as bool) as has_picture from wines; -- name: InsertWine :execresult insert into wines (name, rating, country) values (@name, @rating, @country); diff --git a/storage/query/query.sql.go b/storage/query/query.sql.go index 349c20fdf2bf45d9339d91cd1b0e3441f9ff1f24..d721394cdcb723839511bb1f0b28724222651ac4 100644 --- a/storage/query/query.sql.go +++ b/storage/query/query.sql.go @@ -286,12 +286,13 @@ func (q *Queries) ListUsers(ctx context.Context) ([]string, error) { } const listWines = `-- name: ListWines :many -select wine_id, name, country, cast(picture is not null as bool) as has_picture from wines +select wine_id, name, rating, country, cast(picture is not null as bool) as has_picture from wines ` type ListWinesRow struct { WineID int32 Name string + Rating sql.NullInt32 Country sql.NullString HasPicture bool } @@ -308,6 +309,7 @@ func (q *Queries) ListWines(ctx context.Context) ([]ListWinesRow, error) { if err := rows.Scan( &i.WineID, &i.Name, + &i.Rating, &i.Country, &i.HasPicture, ); err != nil { diff --git a/storage/storage.go b/storage/storage.go index 10c8672c47ee7e0f73a1bcfc900ed40cb34409cb..babf9eefeb28921cc003ed7cbcf6ead68d1f9c7c 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -5,7 +5,6 @@ import ( "context" "database/sql" "embed" - "errors" "fmt" "image" "image/png" @@ -15,106 +14,11 @@ import ( "git.c3pb.de/gbe/invinoveritas/log" "git.c3pb.de/gbe/invinoveritas/storage/query" - "git.c3pb.de/gbe/invinoveritas/vino" kitlog "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" - "github.com/jmoiron/sqlx" "golang.org/x/image/draw" ) -var errNotFound = errors.New("not found") - -type Backend struct { - DB *sqlx.DB -} - -func (b Backend) DeleteVino(ctx context.Context, id int) (err error) { - tx, err := b.DB.Begin() - if err != nil { - return err - } - - defer func() { - if err != nil { - rerr := tx.Rollback() - if rerr != nil { - level.Error(log.GetContext(ctx)). - Log("error", rerr, - "msg", "can't roll back transaction") - } - - return - } - - err = tx.Commit() - }() - - _, err = tx.ExecContext(ctx, `DELETE FROM comments WHERE wine = ?1`, id) - if err != nil { - return fmt.Errorf("deleting comments for %d: %w", id, err) - } - - _, err = tx.ExecContext(ctx, `DELETE FROM wines WHERE id() = ?1`, id) - if err != nil { - return fmt.Errorf("deleting wine %d: %w", id, err) - } - - return nil -} - -func loadComments(ctx context.Context, v *vino.Vino, tx *sqlx.Tx) error { - err := tx.SelectContext(ctx, &v.Comments, ` - SELECT id() as id, content - FROM comments - WHERE wine = ?1`, v.ID) - if err != nil { - return err - } - - return nil -} - -// loadVino uses the given read-only bolt transaction to load the data for the wine with the given id. When -// there are no wines at all, or there is no wine with the given ID, loadVino returns errNotFound. -func loadVino(ctx context.Context, tx *sqlx.Tx, id int) (vino.Vino, error) { - var v vino.Vino - - err := tx.GetContext(ctx, &v, ` - SELECT id() as id, name, rating, country, picture IS NOT NULL AS has_picture - FROM wines - WHERE id() = ?1`, id) - if err != nil { - return v, err - } - - err = loadComments(ctx, &v, tx) - if err != nil { - return v, err - } - - return v, nil -} - -func (b Backend) LoadPictureData(ctx context.Context, id int) ([]byte, error) { - var data []byte - err := b.DB.GetContext(ctx, &data, `SELECT picture FROM wines WHERE id() = ?1`, id) - if errors.Is(err, sql.ErrNoRows) { - return nil, errNotFound - } - - if err != nil { - return nil, err - } - - if len(data) == 0 { - level.Error(log.GetContext(ctx)). - Log("msg", "zero length image data") - return nil, errNotFound - } - - return data, nil -} - // AddPicture loads picture data (PNG or JPEG) from fh and persists a scaled down version in q. // 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 @@ -171,8 +75,8 @@ func AddPicture(ctx context.Context, q *query.Queries, wineID int, fh io.Reader, //go:embed migrations/*.sql var migrationFS embed.FS -func Open(ctx context.Context, path string, logger kitlog.Logger) (*sqlx.DB, error) { - db, err := sqlx.Open("sqlite", path) +func Open(ctx context.Context, path string, logger kitlog.Logger) (*sql.DB, error) { + db, err := sql.Open("sqlite", path) if err != nil { return nil, err } diff --git a/templates/details.tpl b/templates/details.tpl index d9ebcb53eb6407f7e43ea33099c7fb414457c2c5..9beb897cfac8a13001b68e1bc6dcd2f2ad9f5932 100644 --- a/templates/details.tpl +++ b/templates/details.tpl @@ -38,7 +38,7 @@ Country (<a href="https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes#Current_ISO_3166_country_codes" target="_blank">ISO 3166 Alpha-2</a>) </label> {{ if .User.CanWrite }} - <input id="form-country" type="text" name="country" value="{{ .Wine.Country.String }}" placeholder="XX" pattern="[A-Z]{2}"> + <input id="form-country" type="text" name="country" value="{{ .Wine.Country.String }}" placeholder="XX" pattern="[A-Za-z]{2}"> {{ else }} <span id="form-country">{{ .Wine.Country.String }}</span> {{ end }} diff --git a/templates/index.tpl b/templates/index.tpl index 029c8d8b1a8d86542e08cee21e9950883194bc90..2d7f6898f5ecad95ba952a10001a9793a5faff7b 100644 --- a/templates/index.tpl +++ b/templates/index.tpl @@ -23,12 +23,11 @@ <tbody> {{ range .Wines }} <tr> - <td><a href="/details?id={{ .ID }}">{{ .Name }}</a></td> - <td>{{ .Rating }}</td> - <td>{{ if eq .Country.String "XX" }} - {{ .Country }} - {{ else }} - <img src="/static/flags/{{ .Country }}.png"> ({{ .Country }}) + <td><a href="/details?id={{ .WineID }}">{{ .Name }}</a></td> + <td>{{ .Rating.Int32 }}</td> + <td>{{ if .Country.Valid }} + <img src="/static/flags/{{ .Country.String }}.png"> + ({{ .Country.String }}) {{ end }}</td> </tr> {{ end }} diff --git a/vino/vino.go b/vino/vino.go index 9e07f3b673bbd55d1b103f5143ac70d5c9558844..89ad7b37e63323251cd25519c39b143f2f4f32e5 100644 --- a/vino/vino.go +++ b/vino/vino.go @@ -4,8 +4,7 @@ import ( "database/sql/driver" "errors" "fmt" - "image" - "io" + "strings" // Imported for side effects to register format handlers _ "image/jpeg" @@ -30,6 +29,8 @@ func ISO2CountryCodeFromString(s string) (ISO2CountryCode, error) { return UnknownCountry, errors.New("invalid length") } + s = strings.ToUpper(s) + return ISO2CountryCode{s[0], s[1]}, nil } @@ -41,6 +42,10 @@ func (i ISO2CountryCode) String() string { return fmt.Sprintf("%c%c", i[0], i[1]) } +func (i ISO2CountryCode) Valid() bool { + return i.String() != "XX" +} + func (i *ISO2CountryCode) UnmarshalBinary(d []byte) error { if len(d) == 0 { *i = UnknownCountry @@ -82,38 +87,3 @@ func (i *ISO2CountryCode) Scan(data interface{}) error { return nil } - -type Vino struct { - ID int `db:"id"` - Name string `db:"name"` - Rating int `db:"rating"` - Country ISO2CountryCode `db:"country"` - Picture image.Image `db:"-"` - HasPicture bool `db:"has_picture"` // Set to true if there's picture data for this vino - Comments []Comment `db:"-"` -} - -// 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. -func (v *Vino) AddPicture(fh io.Reader, contentType string) error { - switch contentType { - case "", "image/jpeg", "image/png": - default: - return fmt.Errorf("unexpected content type for image: %q", contentType) - } - - img, _, err := image.Decode(fh) - if err != nil { - return err - } - - v.Picture = img - - return nil -} - -func (v Vino) String() string { - return fmt.Sprintf("{Name: %q, Rating: %d}", v.Name, v.Rating) -}