2023-02-04 10:32:39 +00:00
|
|
|
package pkg
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
Convert from cents to “price” and back
I do not want to use floats in the Go lang application, because it is
not supposed to do anything with these values other than to print and
retrieve them from the user; all computations will be performed by
PostgreSQL in cents.
That means i have to “convert” from the price format that users expect
to see (e.g., 1.234,56) to cents (e.g., 123456) and back when passing
data between Go and PostgreSQL, and that conversion depends on the
currency’s decimal places.
At first i did everything in Go, but saw that i would need to do it in
a loop when retrieving the list of products, and immediately knew it was
a mistake—i needed a PL/pgSQL function for that.
I still need to convert from string to float, however, when printing the
value to the user. Because the string representation is in C, but i
need to format it according to the locale with golang/x/text. That
package has the information of how to correctly format numbers, but it
is in an internal package that i can not use, and numbers.Digit only
accepts numeric types, not a string.
2023-02-05 12:55:12 +00:00
|
|
|
"fmt"
|
2023-02-04 10:32:39 +00:00
|
|
|
"github.com/julienschmidt/httprouter"
|
|
|
|
"html/template"
|
|
|
|
"math"
|
|
|
|
"net/http"
|
2023-02-14 11:39:54 +00:00
|
|
|
"strconv"
|
2023-02-04 10:32:39 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type ProductEntry struct {
|
|
|
|
Slug string
|
|
|
|
Name string
|
Convert from cents to “price” and back
I do not want to use floats in the Go lang application, because it is
not supposed to do anything with these values other than to print and
retrieve them from the user; all computations will be performed by
PostgreSQL in cents.
That means i have to “convert” from the price format that users expect
to see (e.g., 1.234,56) to cents (e.g., 123456) and back when passing
data between Go and PostgreSQL, and that conversion depends on the
currency’s decimal places.
At first i did everything in Go, but saw that i would need to do it in
a loop when retrieving the list of products, and immediately knew it was
a mistake—i needed a PL/pgSQL function for that.
I still need to convert from string to float, however, when printing the
value to the user. Because the string representation is in C, but i
need to format it according to the locale with golang/x/text. That
package has the information of how to correctly format numbers, but it
is in an internal package that i can not use, and numbers.Digit only
accepts numeric types, not a string.
2023-02-05 12:55:12 +00:00
|
|
|
Price string
|
2023-03-26 11:51:57 +00:00
|
|
|
Tags []string
|
2023-02-04 10:32:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type productsIndexPage struct {
|
|
|
|
Products []*ProductEntry
|
|
|
|
}
|
|
|
|
|
|
|
|
func IndexProducts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
|
|
|
conn := getConn(r)
|
|
|
|
company := mustGetCompany(r)
|
2023-03-26 11:51:57 +00:00
|
|
|
tag := r.URL.Query().Get("tag")
|
2023-02-04 10:32:39 +00:00
|
|
|
page := &productsIndexPage{
|
2023-03-26 11:51:57 +00:00
|
|
|
Products: mustCollectProductEntries(r.Context(), conn, company, tag),
|
2023-02-04 10:32:39 +00:00
|
|
|
}
|
2023-03-23 09:55:02 +00:00
|
|
|
mustRenderMainTemplate(w, r, "products/index.gohtml", page)
|
2023-02-04 10:32:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func GetProductForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
|
|
|
locale := getLocale(r)
|
|
|
|
conn := getConn(r)
|
|
|
|
company := mustGetCompany(r)
|
|
|
|
form := newProductForm(r.Context(), conn, locale, company)
|
|
|
|
slug := params[0].Value
|
|
|
|
if slug == "new" {
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
mustRenderNewProductForm(w, r, form)
|
|
|
|
return
|
|
|
|
}
|
2023-03-26 11:51:57 +00:00
|
|
|
if !form.MustFillFromDatabase(r.Context(), conn, slug) {
|
2023-02-14 11:46:11 +00:00
|
|
|
http.NotFound(w, r)
|
|
|
|
return
|
2023-02-04 10:32:39 +00:00
|
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
2023-03-27 07:44:04 +00:00
|
|
|
mustRenderEditProductForm(w, r, slug, form)
|
2023-02-04 10:32:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func mustRenderNewProductForm(w http.ResponseWriter, r *http.Request, form *productForm) {
|
2023-03-27 07:44:04 +00:00
|
|
|
mustRenderModalTemplate(w, r, "products/new.gohtml", form)
|
2023-02-04 10:32:39 +00:00
|
|
|
}
|
|
|
|
|
2023-03-27 07:44:04 +00:00
|
|
|
type editProductPage struct {
|
|
|
|
Slug string
|
|
|
|
ProductName string
|
|
|
|
Form *productForm
|
|
|
|
}
|
|
|
|
|
|
|
|
func mustRenderEditProductForm(w http.ResponseWriter, r *http.Request, slug string, form *productForm) {
|
|
|
|
page := &editProductPage{
|
|
|
|
Slug: slug,
|
|
|
|
ProductName: form.Name.Val,
|
|
|
|
Form: form,
|
|
|
|
}
|
|
|
|
mustRenderModalTemplate(w, r, "products/edit.gohtml", page)
|
2023-02-04 10:32:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func HandleAddProduct(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
|
|
|
conn := getConn(r)
|
|
|
|
locale := getLocale(r)
|
|
|
|
company := mustGetCompany(r)
|
|
|
|
form := newProductForm(r.Context(), conn, locale, company)
|
|
|
|
if err := form.Parse(r); err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err := verifyCsrfTokenValid(r); err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !form.Validate() {
|
2023-03-27 07:44:04 +00:00
|
|
|
if !IsHTMxRequest(r) {
|
|
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
|
|
}
|
2023-02-04 10:32:39 +00:00
|
|
|
mustRenderNewProductForm(w, r, form)
|
|
|
|
return
|
|
|
|
}
|
2023-02-14 11:39:54 +00:00
|
|
|
taxes := mustSliceAtoi(form.Tax.Selected)
|
2023-03-26 11:51:57 +00:00
|
|
|
conn.MustExec(r.Context(), "select add_product($1, $2, $3, $4, $5, $6)", company.Id, form.Name, form.Description, form.Price, taxes, form.Tags)
|
2023-03-27 07:44:04 +00:00
|
|
|
if IsHTMxRequest(r) {
|
|
|
|
w.Header().Set("HX-Trigger", "closeModal")
|
|
|
|
w.Header().Set("HX-Refresh", "true")
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
} else {
|
|
|
|
http.Redirect(w, r, companyURI(company, "/products"), http.StatusSeeOther)
|
|
|
|
}
|
2023-02-14 11:39:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func sliceAtoi(s []string) ([]int, error) {
|
2023-03-26 11:51:57 +00:00
|
|
|
var i []int
|
2023-02-14 11:39:54 +00:00
|
|
|
for _, vs := range s {
|
|
|
|
vi, err := strconv.Atoi(vs)
|
|
|
|
if err != nil {
|
|
|
|
return i, err
|
2023-02-08 12:47:36 +00:00
|
|
|
}
|
2023-02-14 11:39:54 +00:00
|
|
|
i = append(i, vi)
|
2023-02-08 12:47:36 +00:00
|
|
|
}
|
2023-02-14 11:39:54 +00:00
|
|
|
return i, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func mustSliceAtoi(s []string) []int {
|
|
|
|
i, err := sliceAtoi(s)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return i
|
2023-02-04 10:32:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func HandleUpdateProduct(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
|
|
|
conn := getConn(r)
|
|
|
|
locale := getLocale(r)
|
|
|
|
company := mustGetCompany(r)
|
|
|
|
form := newProductForm(r.Context(), conn, locale, company)
|
|
|
|
if err := form.Parse(r); err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err := verifyCsrfTokenValid(r); err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
|
|
return
|
|
|
|
}
|
2023-03-27 07:44:04 +00:00
|
|
|
slug := params[0].Value
|
2023-02-04 10:32:39 +00:00
|
|
|
if !form.Validate() {
|
2023-02-12 20:03:46 +00:00
|
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
2023-03-27 07:44:04 +00:00
|
|
|
mustRenderEditProductForm(w, r, slug, form)
|
2023-02-04 10:32:39 +00:00
|
|
|
return
|
|
|
|
}
|
2023-02-14 11:39:54 +00:00
|
|
|
taxes := mustSliceAtoi(form.Tax.Selected)
|
2023-03-26 11:51:57 +00:00
|
|
|
if ok := conn.MustGetBool(r.Context(), "select edit_product($1, $2, $3, $4, $5, $6)", slug, form.Name, form.Description, form.Price, taxes, form.Tags); !ok {
|
2023-02-04 10:32:39 +00:00
|
|
|
http.NotFound(w, r)
|
|
|
|
}
|
2023-03-27 07:44:04 +00:00
|
|
|
if IsHTMxRequest(r) {
|
|
|
|
w.Header().Set("HX-Trigger", "closeModal")
|
|
|
|
w.Header().Set("HX-Refresh", "true")
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
} else {
|
|
|
|
http.Redirect(w, r, companyURI(company, "/products/"+slug), http.StatusSeeOther)
|
|
|
|
}
|
2023-02-04 10:32:39 +00:00
|
|
|
}
|
|
|
|
|
2023-03-26 11:51:57 +00:00
|
|
|
func mustCollectProductEntries(ctx context.Context, conn *Conn, company *Company, tag string) []*ProductEntry {
|
|
|
|
rows, err := conn.Query(ctx, `
|
|
|
|
select product.slug
|
|
|
|
, product.name
|
|
|
|
, to_price(price, decimal_digits)
|
|
|
|
, array_agg(coalesce(tag.name::text, ''))
|
|
|
|
from product
|
|
|
|
join company using (company_id)
|
|
|
|
join currency using (currency_code)
|
|
|
|
left join product_tag using (product_id)
|
|
|
|
left join tag using(tag_id)
|
|
|
|
where product.company_id = $1 and (($2 = '') or (tag.name = $2))
|
|
|
|
group by product.slug
|
|
|
|
, product.name
|
|
|
|
, to_price(price, decimal_digits)
|
|
|
|
order by name
|
|
|
|
`, company.Id, tag)
|
2023-02-04 10:32:39 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
var entries []*ProductEntry
|
|
|
|
for rows.Next() {
|
|
|
|
entry := &ProductEntry{}
|
2023-03-26 11:51:57 +00:00
|
|
|
err = rows.Scan(&entry.Slug, &entry.Name, &entry.Price, &entry.Tags)
|
2023-02-04 10:32:39 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
entries = append(entries, entry)
|
|
|
|
}
|
|
|
|
if rows.Err() != nil {
|
|
|
|
panic(rows.Err())
|
|
|
|
}
|
|
|
|
|
|
|
|
return entries
|
|
|
|
}
|
|
|
|
|
|
|
|
type productForm struct {
|
|
|
|
locale *Locale
|
Convert from cents to “price” and back
I do not want to use floats in the Go lang application, because it is
not supposed to do anything with these values other than to print and
retrieve them from the user; all computations will be performed by
PostgreSQL in cents.
That means i have to “convert” from the price format that users expect
to see (e.g., 1.234,56) to cents (e.g., 123456) and back when passing
data between Go and PostgreSQL, and that conversion depends on the
currency’s decimal places.
At first i did everything in Go, but saw that i would need to do it in
a loop when retrieving the list of products, and immediately knew it was
a mistake—i needed a PL/pgSQL function for that.
I still need to convert from string to float, however, when printing the
value to the user. Because the string representation is in C, but i
need to format it according to the locale with golang/x/text. That
package has the information of how to correctly format numbers, but it
is in an internal package that i can not use, and numbers.Digit only
accepts numeric types, not a string.
2023-02-05 12:55:12 +00:00
|
|
|
company *Company
|
2023-02-04 10:32:39 +00:00
|
|
|
Name *InputField
|
|
|
|
Description *InputField
|
|
|
|
Price *InputField
|
|
|
|
Tax *SelectField
|
2023-03-26 11:51:57 +00:00
|
|
|
Tags *TagsField
|
2023-02-04 10:32:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func newProductForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *productForm {
|
|
|
|
return &productForm{
|
Convert from cents to “price” and back
I do not want to use floats in the Go lang application, because it is
not supposed to do anything with these values other than to print and
retrieve them from the user; all computations will be performed by
PostgreSQL in cents.
That means i have to “convert” from the price format that users expect
to see (e.g., 1.234,56) to cents (e.g., 123456) and back when passing
data between Go and PostgreSQL, and that conversion depends on the
currency’s decimal places.
At first i did everything in Go, but saw that i would need to do it in
a loop when retrieving the list of products, and immediately knew it was
a mistake—i needed a PL/pgSQL function for that.
I still need to convert from string to float, however, when printing the
value to the user. Because the string representation is in C, but i
need to format it according to the locale with golang/x/text. That
package has the information of how to correctly format numbers, but it
is in an internal package that i can not use, and numbers.Digit only
accepts numeric types, not a string.
2023-02-05 12:55:12 +00:00
|
|
|
locale: locale,
|
|
|
|
company: company,
|
2023-02-04 10:32:39 +00:00
|
|
|
Name: &InputField{
|
|
|
|
Name: "name",
|
|
|
|
Label: pgettext("input", "Name", locale),
|
|
|
|
Type: "text",
|
|
|
|
Required: true,
|
|
|
|
},
|
|
|
|
Description: &InputField{
|
|
|
|
Name: "description",
|
|
|
|
Label: pgettext("input", "Description", locale),
|
2023-02-07 14:28:22 +00:00
|
|
|
Type: "textarea",
|
2023-02-04 10:32:39 +00:00
|
|
|
},
|
|
|
|
Price: &InputField{
|
|
|
|
Name: "price",
|
|
|
|
Label: pgettext("input", "Price", locale),
|
|
|
|
Type: "number",
|
|
|
|
Required: true,
|
|
|
|
Attributes: []template.HTMLAttr{
|
|
|
|
`min="0"`,
|
Convert from cents to “price” and back
I do not want to use floats in the Go lang application, because it is
not supposed to do anything with these values other than to print and
retrieve them from the user; all computations will be performed by
PostgreSQL in cents.
That means i have to “convert” from the price format that users expect
to see (e.g., 1.234,56) to cents (e.g., 123456) and back when passing
data between Go and PostgreSQL, and that conversion depends on the
currency’s decimal places.
At first i did everything in Go, but saw that i would need to do it in
a loop when retrieving the list of products, and immediately knew it was
a mistake—i needed a PL/pgSQL function for that.
I still need to convert from string to float, however, when printing the
value to the user. Because the string representation is in C, but i
need to format it according to the locale with golang/x/text. That
package has the information of how to correctly format numbers, but it
is in an internal package that i can not use, and numbers.Digit only
accepts numeric types, not a string.
2023-02-05 12:55:12 +00:00
|
|
|
template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())),
|
2023-02-04 10:32:39 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
Tax: &SelectField{
|
2023-02-05 13:06:33 +00:00
|
|
|
Name: "tax",
|
2023-02-08 12:47:36 +00:00
|
|
|
Label: pgettext("input", "Taxes", locale),
|
|
|
|
Multiple: true,
|
2023-02-12 20:06:48 +00:00
|
|
|
Options: mustGetTaxOptions(ctx, conn, company),
|
2023-02-04 10:32:39 +00:00
|
|
|
},
|
2023-03-26 11:51:57 +00:00
|
|
|
Tags: &TagsField{
|
|
|
|
Name: "tags",
|
|
|
|
Label: pgettext("input", "Tags", locale),
|
|
|
|
},
|
2023-02-04 10:32:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (form *productForm) Parse(r *http.Request) error {
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
form.Name.FillValue(r)
|
|
|
|
form.Description.FillValue(r)
|
|
|
|
form.Price.FillValue(r)
|
|
|
|
form.Tax.FillValue(r)
|
2023-03-26 11:51:57 +00:00
|
|
|
form.Tags.FillValue(r)
|
2023-02-04 10:32:39 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (form *productForm) Validate() bool {
|
|
|
|
validator := newFormValidator()
|
|
|
|
validator.CheckRequiredInput(form.Name, gettext("Name can not be empty.", form.locale))
|
|
|
|
if validator.CheckRequiredInput(form.Price, gettext("Price can not be empty.", form.locale)) {
|
Convert from cents to “price” and back
I do not want to use floats in the Go lang application, because it is
not supposed to do anything with these values other than to print and
retrieve them from the user; all computations will be performed by
PostgreSQL in cents.
That means i have to “convert” from the price format that users expect
to see (e.g., 1.234,56) to cents (e.g., 123456) and back when passing
data between Go and PostgreSQL, and that conversion depends on the
currency’s decimal places.
At first i did everything in Go, but saw that i would need to do it in
a loop when retrieving the list of products, and immediately knew it was
a mistake—i needed a PL/pgSQL function for that.
I still need to convert from string to float, however, when printing the
value to the user. Because the string representation is in C, but i
need to format it according to the locale with golang/x/text. That
package has the information of how to correctly format numbers, but it
is in an internal package that i can not use, and numbers.Digit only
accepts numeric types, not a string.
2023-02-05 12:55:12 +00:00
|
|
|
validator.CheckValidDecimal(form.Price, form.company.MinCents(), math.MaxFloat64, gettext("Price must be a number greater than zero.", form.locale))
|
2023-02-04 10:32:39 +00:00
|
|
|
}
|
|
|
|
validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale))
|
2023-03-01 10:55:26 +00:00
|
|
|
validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale))
|
2023-02-04 10:32:39 +00:00
|
|
|
return validator.AllOK()
|
|
|
|
}
|
2023-03-26 11:51:57 +00:00
|
|
|
|
|
|
|
func (form *productForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
|
|
|
|
return !notFoundErrorOrPanic(conn.QueryRow(ctx, `
|
|
|
|
select product.name
|
|
|
|
, product.description
|
|
|
|
, to_price(price, decimal_digits)
|
|
|
|
, array_agg(tax_id)
|
|
|
|
, string_agg(tag.name, ',')
|
|
|
|
from product
|
|
|
|
left join product_tax using (product_id)
|
|
|
|
left join product_tag using (product_id)
|
|
|
|
left join tag using(company_id, tag_id)
|
|
|
|
join company using (company_id)
|
|
|
|
join currency using (currency_code)
|
|
|
|
where product.slug = $1
|
|
|
|
group by product_id
|
|
|
|
, product.name
|
|
|
|
, product.description
|
|
|
|
, price
|
|
|
|
, decimal_digits
|
|
|
|
`, slug).Scan(
|
|
|
|
form.Name,
|
|
|
|
form.Description,
|
|
|
|
form.Price,
|
|
|
|
form.Tax,
|
|
|
|
form.Tags))
|
|
|
|
}
|