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-04-23 01:20:01 +00:00
|
|
|
"strings"
|
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
|
2023-04-23 01:20:01 +00:00
|
|
|
Filters *productFilterForm
|
2023-02-04 10:32:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func IndexProducts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
|
|
|
conn := getConn(r)
|
|
|
|
company := mustGetCompany(r)
|
2023-04-23 01:20:01 +00:00
|
|
|
locale := getLocale(r)
|
|
|
|
filters := newProductFilterForm(locale)
|
|
|
|
if err := filters.Parse(r); err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
2023-02-04 10:32:39 +00:00
|
|
|
page := &productsIndexPage{
|
2023-04-23 01:20:01 +00:00
|
|
|
Products: mustCollectProductEntries(r.Context(), conn, company, filters),
|
|
|
|
Filters: filters,
|
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-04-25 13:28:55 +00:00
|
|
|
mustRenderMainTemplate(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,
|
|
|
|
}
|
2023-04-25 13:28:55 +00:00
|
|
|
mustRenderMainTemplate(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-04-25 13:28:55 +00:00
|
|
|
htmxRedirect(w, r, companyURI(company, "/products"))
|
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-05-05 08:57:48 +00:00
|
|
|
if !IsHTMxRequest(r) {
|
|
|
|
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-05-05 08:54:40 +00:00
|
|
|
return
|
2023-02-04 10:32:39 +00:00
|
|
|
}
|
2023-04-25 13:28:55 +00:00
|
|
|
htmxRedirect(w, r, companyURI(company, "/products"))
|
2023-02-04 10:32:39 +00:00
|
|
|
}
|
|
|
|
|
2023-04-23 01:20:01 +00:00
|
|
|
type productFilterForm struct {
|
|
|
|
Name *InputField
|
|
|
|
Tags *TagsField
|
|
|
|
TagsCondition *ToggleField
|
|
|
|
}
|
|
|
|
|
|
|
|
func newProductFilterForm(locale *Locale) *productFilterForm {
|
|
|
|
return &productFilterForm{
|
|
|
|
Name: &InputField{
|
|
|
|
Name: "number",
|
2023-04-25 13:21:34 +00:00
|
|
|
Label: pgettext("input", "Name", locale),
|
2023-04-23 01:20:01 +00:00
|
|
|
Type: "search",
|
|
|
|
},
|
|
|
|
Tags: &TagsField{
|
|
|
|
Name: "tags",
|
|
|
|
Label: pgettext("input", "Tags", locale),
|
|
|
|
},
|
|
|
|
TagsCondition: &ToggleField{
|
|
|
|
Name: "tags_condition",
|
|
|
|
Label: pgettext("input", "Tags Condition", locale),
|
|
|
|
Selected: "and",
|
|
|
|
FirstOption: &ToggleOption{
|
|
|
|
Value: "and",
|
|
|
|
Label: pgettext("tag condition", "All", locale),
|
|
|
|
Description: gettext("Invoices must have all the specified labels.", locale),
|
|
|
|
},
|
|
|
|
SecondOption: &ToggleOption{
|
|
|
|
Value: "or",
|
|
|
|
Label: pgettext("tag condition", "Any", locale),
|
|
|
|
Description: gettext("Invoices must have at least one of the specified labels.", locale),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (form *productFilterForm) Parse(r *http.Request) error {
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
form.Name.FillValue(r)
|
|
|
|
form.Tags.FillValue(r)
|
|
|
|
form.TagsCondition.FillValue(r)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func mustCollectProductEntries(ctx context.Context, conn *Conn, company *Company, filters *productFilterForm) []*ProductEntry {
|
|
|
|
args := []interface{}{company.Id}
|
|
|
|
where := []string{"product.company_id = $1"}
|
|
|
|
appendWhere := func(expression string, value interface{}) {
|
|
|
|
args = append(args, value)
|
|
|
|
where = append(where, fmt.Sprintf(expression, len(args)))
|
|
|
|
}
|
|
|
|
if filters != nil {
|
|
|
|
name := strings.TrimSpace(filters.Name.String())
|
|
|
|
if name != "" {
|
|
|
|
appendWhere("product.name ilike $%d", "%"+name+"%")
|
|
|
|
}
|
|
|
|
if len(filters.Tags.Tags) > 0 {
|
|
|
|
if filters.TagsCondition.Selected == "and" {
|
|
|
|
appendWhere("product.tags @> $%d", filters.Tags)
|
|
|
|
} else {
|
|
|
|
appendWhere("product.tags && $%d", filters.Tags)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
rows := conn.MustQuery(ctx, fmt.Sprintf(`
|
2023-03-26 11:51:57 +00:00
|
|
|
select product.slug
|
|
|
|
, product.name
|
|
|
|
, to_price(price, decimal_digits)
|
Replace tag relations with array attributes
It all started when i wanted to try to filter invoices by multiple tags
using an “AND”, instead of “OR” as it was doing until now. But
something felt off and seemed to me that i was doing thing much more
complex than needed, all to be able to list the tags as a suggestion
in the input field—which i am not doing yet.
I found this article series[0] exploring different approaches for
tagging, which includes the one i was using, and comparing their
performance. I have not actually tested it, but it seems that i have
chosen the worst option, in both query time and storage.
I attempted to try using an array attribute to each table, which is more
or less the same they did in the articles but without using a separate
relation for tags, and i found out that all the queries were way easier
to write, and needed two joins less, so it was a no-brainer.
[0]: http://www.databasesoup.com/2015/01/tag-all-things.html
2023-04-07 19:31:35 +00:00
|
|
|
, tags
|
2023-03-26 11:51:57 +00:00
|
|
|
from product
|
|
|
|
join company using (company_id)
|
|
|
|
join currency using (currency_code)
|
2023-04-23 01:20:01 +00:00
|
|
|
where (%s)
|
2023-03-26 11:51:57 +00:00
|
|
|
order by name
|
2023-04-23 01:20:01 +00:00
|
|
|
`, strings.Join(where, ") AND (")), args...)
|
2023-02-04 10:32:39 +00:00
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
var entries []*ProductEntry
|
|
|
|
for rows.Next() {
|
|
|
|
entry := &ProductEntry{}
|
2023-04-23 01:20:01 +00:00
|
|
|
if err := rows.Scan(&entry.Slug, &entry.Name, &entry.Price, &entry.Tags); err != nil {
|
2023-02-04 10:32:39 +00:00
|
|
|
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)
|
2023-05-27 19:36:10 +00:00
|
|
|
, tags
|
2023-03-26 11:51:57 +00:00
|
|
|
from product
|
|
|
|
left join product_tax using (product_id)
|
|
|
|
join company using (company_id)
|
|
|
|
join currency using (currency_code)
|
|
|
|
where product.slug = $1
|
|
|
|
group by product_id
|
|
|
|
, product.name
|
|
|
|
, product.description
|
|
|
|
, price
|
Replace tag relations with array attributes
It all started when i wanted to try to filter invoices by multiple tags
using an “AND”, instead of “OR” as it was doing until now. But
something felt off and seemed to me that i was doing thing much more
complex than needed, all to be able to list the tags as a suggestion
in the input field—which i am not doing yet.
I found this article series[0] exploring different approaches for
tagging, which includes the one i was using, and comparing their
performance. I have not actually tested it, but it seems that i have
chosen the worst option, in both query time and storage.
I attempted to try using an array attribute to each table, which is more
or less the same they did in the articles but without using a separate
relation for tags, and i found out that all the queries were way easier
to write, and needed two joins less, so it was a no-brainer.
[0]: http://www.databasesoup.com/2015/01/tag-all-things.html
2023-04-07 19:31:35 +00:00
|
|
|
, tags
|
2023-03-26 11:51:57 +00:00
|
|
|
, decimal_digits
|
|
|
|
`, slug).Scan(
|
|
|
|
form.Name,
|
|
|
|
form.Description,
|
|
|
|
form.Price,
|
|
|
|
form.Tax,
|
|
|
|
form.Tags))
|
|
|
|
}
|
2023-04-23 01:20:01 +00:00
|
|
|
|
|
|
|
func HandleProductSearch(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
2023-04-24 00:00:38 +00:00
|
|
|
filters := newProductFilterForm(getLocale(r))
|
2023-04-23 01:20:01 +00:00
|
|
|
query := r.URL.Query()
|
2023-04-24 00:00:38 +00:00
|
|
|
index := query.Get("index")
|
|
|
|
filters.Name.Val = strings.TrimSpace(query.Get("product.name." + index))
|
|
|
|
var products []*ProductEntry
|
|
|
|
if len(filters.Name.Val) > 0 {
|
|
|
|
products = mustCollectProductEntries(r.Context(), getConn(r), mustGetCompany(r), filters)
|
2023-04-23 01:20:01 +00:00
|
|
|
}
|
2023-04-24 00:00:38 +00:00
|
|
|
mustRenderStandaloneTemplate(w, r, "products/search.gohtml", products)
|
2023-04-23 01:20:01 +00:00
|
|
|
}
|
2023-05-09 10:18:31 +00:00
|
|
|
|
|
|
|
func ServeEditProductTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
|
|
|
conn := getConn(r)
|
|
|
|
locale := getLocale(r)
|
|
|
|
company := getCompany(r)
|
|
|
|
slug := params[0].Value
|
|
|
|
form := newTagsForm(companyURI(company, "/products/"+slug+"/tags"), slug, locale)
|
2023-05-27 19:36:10 +00:00
|
|
|
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), `select tags from product where slug = $1`, form.Slug).Scan(form.Tags)) {
|
2023-05-09 10:18:31 +00:00
|
|
|
http.NotFound(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
mustRenderStandaloneTemplate(w, r, "tags/edit.gohtml", form)
|
|
|
|
}
|
|
|
|
|
|
|
|
func HandleUpdateProductTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
|
|
|
locale := getLocale(r)
|
|
|
|
conn := getConn(r)
|
|
|
|
company := getCompany(r)
|
|
|
|
slug := params[0].Value
|
|
|
|
form := newTagsForm(companyURI(company, "/products/"+slug+"/tags/edit"), slug, locale)
|
|
|
|
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 conn.MustGetText(r.Context(), "", "update product set tags = $1 where slug = $2 returning slug", form.Tags, form.Slug) == "" {
|
|
|
|
http.NotFound(w, r)
|
|
|
|
}
|
|
|
|
mustRenderStandaloneTemplate(w, r, "tags/view.gohtml", form)
|
|
|
|
}
|