Compare commits

..

No commits in common. "ae1949024b7215ff97448178d595b90506403a84" and "e9cc331ee06cb80ac1610cdb95ede11e445ab058" have entirely different histories.

20 changed files with 90 additions and 341 deletions

View File

@ -1,47 +0,0 @@
-- Deploy numerus:parse_price to pg
-- requires: schema_public
begin;
set search_path to numerus, public;
create or replace function parse_price(price text, decimal_digits integer) returns integer as
$$
declare
parts text[];
result int;
frac_part text;
begin
parts := string_to_array(price, '.');
if array_length(parts, 1) > 2 then
raise invalid_parameter_value using message = price || ' is not a valid price representation.';
end if;
result := parts[1]::integer;
for d in 1..decimal_digits loop
result := result * 10;
end loop;
if array_length(parts, 1) = 2 then
frac_part := rtrim(parts[2], '0');
if length(frac_part) > decimal_digits then
raise invalid_parameter_value using message = price || ' has too many digits in the fraction part.';
end if;
frac_part := rpad(frac_part, decimal_digits, '0');
result := result + frac_part::integer;
end if;
return result;
end;
$$
language plpgsql
immutable;
comment on function parse_price(text, integer) is
'Converts the string representation of a price in decimal form to cents, according to the number of decimal digits passed.';
revoke execute on function parse_price(text, integer) from public;
grant execute on function parse_price(text, integer) to invoicer;
grant execute on function parse_price(text, integer) to admin;
commit;

View File

@ -1,28 +0,0 @@
-- Deploy numerus:to_price to pg
-- requires: schema_numerus
begin;
set search_path to numerus, public;
create or replace function to_price(cents integer, decimal_digits integer) returns text as
$$
declare
result text;
scale integer := 10^decimal_digits;
begin
result = cents::text;
return (cents / scale)::text || '.' || to_char(mod(cents, scale), rpad('FM', decimal_digits + 2, '0'));
end;
$$
language plpgsql
immutable;
comment on function to_price(integer, integer) is
'Converts the cents to a price representation, without currency and any other separater than decimal.';
revoke execute on function to_price(integer, integer) from public;
grant execute on function to_price(integer, integer) to invoicer;
grant execute on function to_price(integer, integer) to admin;
commit;

View File

@ -16,8 +16,6 @@ const (
type Company struct {
Id int
CurrencySymbol string
DecimalDigits int
Slug string
}
@ -27,7 +25,7 @@ func CompanyHandler(next http.Handler) httprouter.Handle {
Slug: params[0].Value,
}
conn := getConn(r)
err := conn.QueryRow(r.Context(), "select company_id, currency_symbol, decimal_digits from company join currency using (currency_code) where slug = $1", company.Slug).Scan(&company.Id, &company.CurrencySymbol, &company.DecimalDigits)
err := conn.QueryRow(r.Context(), "select company_id from company where slug = $1", company.Slug).Scan(&company.Id)
if err != nil {
http.NotFound(w, r)
return
@ -43,15 +41,6 @@ func CompanyHandler(next http.Handler) httprouter.Handle {
}
}
func (c Company) MinCents() float64 {
var r float64
r = 1
for i := 0; i < c.DecimalDigits; i++ {
r /= 10.0
}
return r
}
func getCompany(r *http.Request) *Company {
company := r.Context().Value(ContextCompanyKey)
if company == nil {
@ -88,7 +77,6 @@ func newTaxDetailsForm(ctx context.Context, conn *Conn, locale *Locale) *taxDeta
Name: "currency",
Label: pgettext("input", "Currency", locale),
Options: MustGetOptions(ctx, conn, "select currency_code, currency_symbol from currency order by currency_code"),
Required: true,
Selected: "EUR",
},
}

View File

@ -224,7 +224,6 @@ func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactFor
Name: "country",
Label: pgettext("input", "Tax", locale),
Options: mustGetCountryOptions(ctx, conn, locale),
Required: true,
Selected: "ES",
Attributes: []template.HTMLAttr{
`autocomplete="country"`,

View File

@ -61,8 +61,6 @@ type SelectField struct {
Selected string
Options []*SelectOption
Attributes []template.HTMLAttr
Required bool
EmptyLabel string
Errors []error
}
@ -177,11 +175,6 @@ func (v *FormValidator) CheckValidInteger(field *InputField, min int, max int, m
return v.checkInput(field, err == nil && value >= min && value <= max, message)
}
func (v *FormValidator) CheckValidDecimal(field *InputField, min float64, max float64, message string) bool {
value, err := strconv.ParseFloat(field.Val, 64)
return v.checkInput(field, err == nil && value >= min && value <= max, message)
}
func (v *FormValidator) checkInput(field *InputField, ok bool, message string) bool {
if !ok {
field.Errors = append(field.Errors, errors.New(message))

View File

@ -2,7 +2,6 @@ package pkg
import (
"context"
"fmt"
"github.com/jackc/pgx/v4"
"github.com/julienschmidt/httprouter"
"html/template"
@ -13,7 +12,7 @@ import (
type ProductEntry struct {
Slug string
Name string
Price string
Price int
}
type productsIndexPage struct {
@ -36,12 +35,11 @@ func GetProductForm(w http.ResponseWriter, r *http.Request, params httprouter.Pa
form := newProductForm(r.Context(), conn, locale, company)
slug := params[0].Value
if slug == "new" {
form.Tax.EmptyLabel = gettext("Select a tax for this product.", locale)
w.WriteHeader(http.StatusOK)
mustRenderNewProductForm(w, r, form)
return
}
err := conn.QueryRow(r.Context(), "select product.name, product.description, to_price(price, decimal_digits), tax_id from product join company using (company_id) join currency using (currency_code) where product.slug = $1", slug).Scan(form.Name, form.Description, form.Price, form.Tax)
err := conn.QueryRow(r.Context(), "select name, description, price, tax_id from product where slug = $1", slug).Scan(form.Name, form.Description, form.Price, form.Tax)
if err != nil {
if err == pgx.ErrNoRows {
http.NotFound(w, r)
@ -79,7 +77,7 @@ func HandleAddProduct(w http.ResponseWriter, r *http.Request, _ httprouter.Param
mustRenderNewProductForm(w, r, form)
return
}
conn.MustExec(r.Context(), "insert into product (company_id, name, description, price, tax_id) select company_id, $2, $3, parse_price($4, decimal_digits), $5 from company join currency using (currency_code) where company_id = $1", company.Id, form.Name, form.Description, form.Price, form.Tax)
conn.MustExec(r.Context(), "insert into product (company_id, name, description, price, tax_id) values ($1, $2, $3, $4, $5)", company.Id, form.Name, form.Description, form.Price, form.Tax)
http.Redirect(w, r, companyURI(company, "/products"), http.StatusSeeOther)
}
@ -100,7 +98,7 @@ func HandleUpdateProduct(w http.ResponseWriter, r *http.Request, params httprout
mustRenderEditProductForm(w, r, form)
return
}
slug := conn.MustGetText(r.Context(), "", "update product set name = $1, description = $2, price = parse_price($3, decimal_digits), tax_id = $4 from company join currency using (currency_code) where product.company_id = company.company_id and product.slug = $5 returning product.slug", form.Name, form.Description, form.Price, form.Tax, params[0].Value)
slug := conn.MustGetText(r.Context(), "", "update product set name = $1, description = $2, price = $3, tax_id = $4 where slug = $5 returning slug", form.Name, form.Description, form.Price, form.Tax, params[0].Value)
if slug == "" {
http.NotFound(w, r)
}
@ -108,7 +106,7 @@ func HandleUpdateProduct(w http.ResponseWriter, r *http.Request, params httprout
}
func mustGetProductEntries(ctx context.Context, conn *Conn, company *Company) []*ProductEntry {
rows, err := conn.Query(ctx, "select product.slug, product.name, to_price(price, decimal_digits) from product join company using (company_id) join currency using (currency_code) where company_id = $1 order by name", company.Id)
rows, err := conn.Query(ctx, "select slug, name, price from product where company_id = $1 order by name", company.Id)
if err != nil {
panic(err)
}
@ -132,7 +130,6 @@ func mustGetProductEntries(ctx context.Context, conn *Conn, company *Company) []
type productForm struct {
locale *Locale
company *Company
Name *InputField
Description *InputField
Price *InputField
@ -142,7 +139,6 @@ type productForm struct {
func newProductForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *productForm {
return &productForm{
locale: locale,
company: company,
Name: &InputField{
Name: "name",
Label: pgettext("input", "Name", locale),
@ -161,13 +157,11 @@ func newProductForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
Required: true,
Attributes: []template.HTMLAttr{
`min="0"`,
template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())),
},
},
Tax: &SelectField{
Name: "tax",
Label: pgettext("input", "Tax", locale),
Required: true,
Options: MustGetOptions(ctx, conn, "select tax_id::text, name from tax where company_id = $1 order by name", company.Id),
},
}
@ -188,7 +182,7 @@ 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)) {
validator.CheckValidDecimal(form.Price, form.company.MinCents(), math.MaxFloat64, gettext("Price must be a number greater than zero.", form.locale))
validator.CheckValidInteger(form.Price, 0, math.MaxInt, gettext("Price must be a number greater than zero.", form.locale))
}
validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale))
return validator.AllOK()

View File

@ -64,7 +64,6 @@ func newProfileForm(ctx context.Context, conn *Conn, locale *Locale) *profileFor
Name: "language",
Label: pgettext("input", "Language", locale),
Options: languages,
Required: true,
Attributes: []template.HTMLAttr{
`autocomplete="language"`,
},

View File

@ -2,13 +2,9 @@ package pkg
import (
"fmt"
"golang.org/x/text/message"
"golang.org/x/text/number"
"html/template"
"io"
"math"
"net/http"
"strconv"
)
const overrideMethodName = "_method"
@ -31,14 +27,6 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
"companyURI": func(uri string) string {
return companyURI(company, uri)
},
"formatPrice": func(price string) string {
p := message.NewPrinter(locale.Language)
f, err := strconv.ParseFloat(price, 64)
if err != nil {
f = math.NaN()
}
return p.Sprintf("%.*f", company.DecimalDigits, number.Decimal(f))
},
"csrfToken": func() template.HTML {
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`, csrfTokenField, user.CsrfToken))
},

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-02-05 14:04+0100\n"
"POT-Creation-Date: 2023-02-04 11:24+0100\n"
"PO-Revision-Date: 2023-01-18 17:08+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n"
@ -238,11 +238,11 @@ msgctxt "input"
msgid "Password"
msgstr "Contrasenya"
#: pkg/login.go:69 pkg/profile.go:89 pkg/contacts.go:263
#: pkg/login.go:69 pkg/profile.go:88 pkg/contacts.go:262
msgid "Email can not be empty."
msgstr "No podeu deixar el correu-e en blanc."
#: pkg/login.go:70 pkg/profile.go:90 pkg/contacts.go:264
#: pkg/login.go:70 pkg/profile.go:89 pkg/contacts.go:263
msgid "This value is not a valid email. It should be like name@domain.com."
msgstr "Aquest valor no és un correu-e vàlid. Hauria de ser similar a nom@domini.cat."
@ -254,74 +254,70 @@ msgstr "No podeu deixar la contrasenya en blanc."
msgid "Invalid user or password."
msgstr "Nom dusuari o contrasenya incorrectes."
#: pkg/products.go:39
msgid "Select a tax for this product."
msgstr "Escolliu un impost per aquest producte."
#: pkg/products.go:148
#: pkg/products.go:144
msgctxt "input"
msgid "Name"
msgstr "Nom"
#: pkg/products.go:154
#: pkg/products.go:150
msgctxt "input"
msgid "Description"
msgstr "Descripció"
#: pkg/products.go:159
#: pkg/products.go:155
msgctxt "input"
msgid "Price"
msgstr "Preu"
#: pkg/products.go:169 pkg/contacts.go:225
#: pkg/products.go:164 pkg/contacts.go:225
msgctxt "input"
msgid "Tax"
msgstr "Impost"
#: pkg/products.go:189 pkg/profile.go:92
#: pkg/products.go:183 pkg/profile.go:91
msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc."
#: pkg/products.go:190
#: pkg/products.go:184
msgid "Price can not be empty."
msgstr "No podeu deixar el preu en blanc."
#: pkg/products.go:191
#: pkg/products.go:185
msgid "Price must be a number greater than zero."
msgstr "El preu ha de ser un número major a zero."
#: pkg/products.go:193
#: pkg/products.go:187
msgid "Selected tax is not valid."
msgstr "Heu seleccionat un impost que no és vàlid."
#: pkg/company.go:89
#: pkg/company.go:78
msgctxt "input"
msgid "Currency"
msgstr "Moneda"
#: pkg/company.go:107
#: pkg/company.go:95
msgid "Selected currency is not valid."
msgstr "Heu seleccionat una moneda que no és vàlida."
#: pkg/company.go:229
#: pkg/company.go:217
msgctxt "input"
msgid "Tax name"
msgstr "Nom impost"
#: pkg/company.go:235
#: pkg/company.go:223
msgctxt "input"
msgid "Rate (%)"
msgstr "Percentatge"
#: pkg/company.go:257
#: pkg/company.go:245
msgid "Tax name can not be empty."
msgstr "No podeu deixar el nom de limpost en blanc."
#: pkg/company.go:258
#: pkg/company.go:246
msgid "Tax rate can not be empty."
msgstr "No podeu deixar percentatge en blanc."
#: pkg/company.go:259
#: pkg/company.go:247
msgid "Tax rate must be an integer between -99 and 99."
msgstr "El percentatge ha de ser entre -99 i 99."
@ -345,11 +341,11 @@ msgctxt "input"
msgid "Language"
msgstr "Idioma"
#: pkg/profile.go:93
#: pkg/profile.go:92
msgid "Confirmation does not match password."
msgstr "La confirmació no és igual a la contrasenya."
#: pkg/profile.go:94
#: pkg/profile.go:93
msgid "Selected language is not valid."
msgstr "Heu seleccionat un idioma que no és vàlid."
@ -398,51 +394,51 @@ msgctxt "input"
msgid "Postal code"
msgstr "Codi postal"
#: pkg/contacts.go:256
#: pkg/contacts.go:255
msgid "Business name can not be empty."
msgstr "No podeu deixar el nom i els cognoms en blanc."
#: pkg/contacts.go:257
#: pkg/contacts.go:256
msgid "VAT number can not be empty."
msgstr "No podeu deixar el DNI o NIF en blanc."
#: pkg/contacts.go:258
#: pkg/contacts.go:257
msgid "This value is not a valid VAT number."
msgstr "Aquest valor no és un DNI o NIF vàlid."
#: pkg/contacts.go:260
#: pkg/contacts.go:259
msgid "Phone can not be empty."
msgstr "No podeu deixar el telèfon en blanc."
#: pkg/contacts.go:261
#: pkg/contacts.go:260
msgid "This value is not a valid phone number."
msgstr "Aquest valor no és un telèfon vàlid."
#: pkg/contacts.go:267
#: pkg/contacts.go:266
msgid "This value is not a valid web address. It should be like https://domain.com/."
msgstr "Aquest valor no és una adreça web vàlida. Hauria de ser similar a https://domini.cat/."
#: pkg/contacts.go:269
#: pkg/contacts.go:268
msgid "Address can not be empty."
msgstr "No podeu deixar ladreça en blanc."
#: pkg/contacts.go:270
#: pkg/contacts.go:269
msgid "City can not be empty."
msgstr "No podeu deixar la població en blanc."
#: pkg/contacts.go:271
#: pkg/contacts.go:270
msgid "Province can not be empty."
msgstr "No podeu deixar la província en blanc."
#: pkg/contacts.go:272
#: pkg/contacts.go:271
msgid "Postal code can not be empty."
msgstr "No podeu deixar el codi postal en blanc."
#: pkg/contacts.go:273
#: pkg/contacts.go:272
msgid "This value is not a valid postal code."
msgstr "Aquest valor no és un codi postal vàlid."
#: pkg/contacts.go:275
#: pkg/contacts.go:274
msgid "Selected country is not valid."
msgstr "Heu seleccionat un país que no és vàlid."

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-02-05 14:04+0100\n"
"POT-Creation-Date: 2023-02-04 11:24+0100\n"
"PO-Revision-Date: 2023-01-18 17:45+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n"
@ -238,11 +238,11 @@ msgctxt "input"
msgid "Password"
msgstr "Contraseña"
#: pkg/login.go:69 pkg/profile.go:89 pkg/contacts.go:263
#: pkg/login.go:69 pkg/profile.go:88 pkg/contacts.go:262
msgid "Email can not be empty."
msgstr "No podéis dejar el correo-e en blanco."
#: pkg/login.go:70 pkg/profile.go:90 pkg/contacts.go:264
#: pkg/login.go:70 pkg/profile.go:89 pkg/contacts.go:263
msgid "This value is not a valid email. It should be like name@domain.com."
msgstr "Este valor no es un correo-e válido. Tiene que ser parecido a nombre@dominio.es."
@ -254,74 +254,70 @@ msgstr "No podéis dejar la contraseña en blanco."
msgid "Invalid user or password."
msgstr "Nombre de usuario o contraseña inválido."
#: pkg/products.go:39
msgid "Select a tax for this product."
msgstr "Escoged un impuesto para este producto."
#: pkg/products.go:148
#: pkg/products.go:144
msgctxt "input"
msgid "Name"
msgstr "Nombre"
#: pkg/products.go:154
#: pkg/products.go:150
msgctxt "input"
msgid "Description"
msgstr "Descripción"
#: pkg/products.go:159
#: pkg/products.go:155
msgctxt "input"
msgid "Price"
msgstr "Precio"
#: pkg/products.go:169 pkg/contacts.go:225
#: pkg/products.go:164 pkg/contacts.go:225
msgctxt "input"
msgid "Tax"
msgstr "Impuesto"
#: pkg/products.go:189 pkg/profile.go:92
#: pkg/products.go:183 pkg/profile.go:91
msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco."
#: pkg/products.go:190
#: pkg/products.go:184
msgid "Price can not be empty."
msgstr "No podéis dejar el precio en blanco."
#: pkg/products.go:191
#: pkg/products.go:185
msgid "Price must be a number greater than zero."
msgstr "El precio tiene que ser un número mayor a cero."
#: pkg/products.go:193
#: pkg/products.go:187
msgid "Selected tax is not valid."
msgstr "Habéis escogido un impuesto que no es válido."
#: pkg/company.go:89
#: pkg/company.go:78
msgctxt "input"
msgid "Currency"
msgstr "Moneda"
#: pkg/company.go:107
#: pkg/company.go:95
msgid "Selected currency is not valid."
msgstr "Habéis escogido una moneda que no es válida."
#: pkg/company.go:229
#: pkg/company.go:217
msgctxt "input"
msgid "Tax name"
msgstr "Nombre impuesto"
#: pkg/company.go:235
#: pkg/company.go:223
msgctxt "input"
msgid "Rate (%)"
msgstr "Porcentaje"
#: pkg/company.go:257
#: pkg/company.go:245
msgid "Tax name can not be empty."
msgstr "No podéis dejar el nombre del impuesto en blanco."
#: pkg/company.go:258
#: pkg/company.go:246
msgid "Tax rate can not be empty."
msgstr "No podéis dejar el porcentaje en blanco."
#: pkg/company.go:259
#: pkg/company.go:247
msgid "Tax rate must be an integer between -99 and 99."
msgstr "El porcentaje tiene que estar entre -99 y 99."
@ -345,11 +341,11 @@ msgctxt "input"
msgid "Language"
msgstr "Idioma"
#: pkg/profile.go:93
#: pkg/profile.go:92
msgid "Confirmation does not match password."
msgstr "La confirmación no corresponde con la contraseña."
#: pkg/profile.go:94
#: pkg/profile.go:93
msgid "Selected language is not valid."
msgstr "Habéis escogido un idioma que no es válido."
@ -398,51 +394,51 @@ msgctxt "input"
msgid "Postal code"
msgstr "Código postal"
#: pkg/contacts.go:256
#: pkg/contacts.go:255
msgid "Business name can not be empty."
msgstr "No podéis dejar el nombre y los apellidos en blanco."
#: pkg/contacts.go:257
#: pkg/contacts.go:256
msgid "VAT number can not be empty."
msgstr "No podéis dejar el DNI o NIF en blanco."
#: pkg/contacts.go:258
#: pkg/contacts.go:257
msgid "This value is not a valid VAT number."
msgstr "Este valor no es un DNI o NIF válido."
#: pkg/contacts.go:260
#: pkg/contacts.go:259
msgid "Phone can not be empty."
msgstr "No podéis dejar el teléfono en blanco."
#: pkg/contacts.go:261
#: pkg/contacts.go:260
msgid "This value is not a valid phone number."
msgstr "Este valor no es un teléfono válido."
#: pkg/contacts.go:267
#: pkg/contacts.go:266
msgid "This value is not a valid web address. It should be like https://domain.com/."
msgstr "Este valor no es una dirección web válida. Tiene que ser parecida a https://dominio.es/."
#: pkg/contacts.go:269
#: pkg/contacts.go:268
msgid "Address can not be empty."
msgstr "No podéis dejar la dirección en blanco."
#: pkg/contacts.go:270
#: pkg/contacts.go:269
msgid "City can not be empty."
msgstr "No podéis dejar la población en blanco."
#: pkg/contacts.go:271
#: pkg/contacts.go:270
msgid "Province can not be empty."
msgstr "No podéis dejar la provincia en blanco."
#: pkg/contacts.go:272
#: pkg/contacts.go:271
msgid "Postal code can not be empty."
msgstr "No podéis dejar el código postal en blanco."
#: pkg/contacts.go:273
#: pkg/contacts.go:272
msgid "This value is not a valid postal code."
msgstr "Este valor no es un código postal válido válido."
#: pkg/contacts.go:275
#: pkg/contacts.go:274
msgid "Selected country is not valid."
msgstr "Habéis escogido un país que no es válido."

View File

@ -1,7 +0,0 @@
-- Revert numerus:parse_price from pg
begin;
drop function if exists numerus.parse_price(text, integer);
commit;

View File

@ -1,7 +0,0 @@
-- Revert numerus:to_price from pg
begin;
drop function if exists numerus.to_price(integer, integer);
commit;

View File

@ -40,5 +40,3 @@ tax_rate [schema_numerus] 2023-01-28T11:33:39Z jordi fita mas <jordi@tandem.blog
tax [schema_numerus company tax_rate] 2023-01-28T11:45:47Z jordi fita mas <jordi@tandem.blog> # Add relation for taxes
contact [schema_numerus company extension_vat email extension_pg_libphonenumber extension_uri currency_code currency country_code country] 2023-01-29T12:59:18Z jordi fita mas <jordi@tandem.blog> # Add the relation for contacts
product [schema_numerus company] 2023-02-04T09:17:24Z jordi fita mas <jordi@tandem.blog> # Add relation for products
parse_price [schema_public] 2023-02-05T11:04:54Z jordi fita mas <jordi@tandem.blog> # Add function to convert from price to cents
to_price [schema_numerus] 2023-02-05T11:46:31Z jordi fita mas <jordi@tandem.blog> # Add function to format cents to prices

View File

@ -1,53 +0,0 @@
-- Test parse_price
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(36);
set search_path to auth, numerus, public;
select has_function('numerus', 'parse_price', array ['text', 'integer']);
select function_lang_is('numerus', 'parse_price', array ['text', 'integer'], 'plpgsql');
select function_returns('numerus', 'parse_price', array ['text', 'integer'], 'integer');
select isnt_definer('numerus', 'parse_price', array ['text', 'integer']);
select volatility_is('numerus', 'parse_price', array ['text', 'integer'], 'immutable');
select function_privs_are('numerus', 'parse_price', array ['text', 'integer'], 'guest', array []::text[]);
select function_privs_are('numerus', 'parse_price', array ['text', 'integer'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'parse_price', array ['text', 'integer'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'parse_price', array ['text', 'integer'], 'authenticator', array []::text[]);
select is( parse_price('1.1', 2), 110 );
select is( parse_price('1.1', 3), 1100 );
select is( parse_price('0', 2), 0 );
select is( parse_price('0', 3), 0 );
select is( parse_price('0.01', 2), 1 );
select is( parse_price('0.001', 3), 1 );
select is( parse_price('0.1', 2), 10 );
select is( parse_price('0.01', 3), 10 );
select is( parse_price('1', 2), 100 );
select is( parse_price('0.1', 3), 100 );
select is( parse_price('10', 2), 1000 );
select is( parse_price('1', 3), 1000 );
select is( parse_price('23.23', 2), 2323 );
select is( parse_price('23.23', 3), 23230 );
select throws_ok( $$ select parse_price('234.234', 2) $$ );
select is( parse_price('234.234', 3), 234234 );
select throws_ok( $$ select parse_price('2345.2345', 2) $$ );
select throws_ok( $$ select parse_price('2345.2345', 3) $$ );
select is( parse_price('00000000000000001.100000000000000000000', 2), 110 );
select is( parse_price('00000000000000001.100000000000000000000', 3), 1100 );
select is( parse_price('00000000000000000.100000000000000000000', 2), 10 );
select is( parse_price('00000000000000000.100000000000000000000', 3), 100 );
select is( parse_price('00000000000123456.780000000000000000000', 2), 12345678 );
select is( parse_price('00000000000123456.789000000000000000000', 3), 123456789 );
select throws_ok( $$ select parse_price('1,1', 2) $$ );
select throws_ok( $$ select parse_price('1.1.1', 2) $$ );
select throws_ok( $$ select parse_price('a.b', 2) $$ );
select *
from finish();
rollback;

View File

@ -1,40 +0,0 @@
-- Test to_price
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(23);
set search_path to numerus, public;
select has_function('numerus', 'to_price', array ['integer', 'integer']);
select function_lang_is('numerus', 'to_price', array ['integer', 'integer'], 'plpgsql');
select function_returns('numerus', 'to_price', array ['integer', 'integer'], 'text');
select isnt_definer('numerus', 'to_price', array ['integer', 'integer']);
select volatility_is('numerus', 'to_price', array ['integer', 'integer'], 'immutable');
select function_privs_are('numerus', 'to_price', array ['integer', 'integer'], 'guest', array []::text[]);
select function_privs_are('numerus', 'to_price', array ['integer', 'integer'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'to_price', array ['integer', 'integer'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'to_price', array ['integer', 'integer'], 'authenticator', array []::text[]);
select is( to_price(0, 2), '0.00' );
select is( to_price(0, 3), '0.000' );
select is( to_price(1, 2), '0.01' );
select is( to_price(1, 3), '0.001' );
select is( to_price(10, 2), '0.10' );
select is( to_price(10, 3), '0.010' );
select is( to_price(100, 2), '1.00' );
select is( to_price(100, 3), '0.100' );
select is( to_price(110, 2), '1.10' );
select is( to_price(1100, 3), '1.100' );
select is( to_price(12345678, 2), '123456.78' );
select is( to_price(12345678, 3), '12345.678' );
select is( to_price(12345678, 4), '1234.5678' );
select is( to_price(12345678, 5), '123.45678' );
select *
from finish();
rollback;

View File

@ -1,7 +0,0 @@
-- Verify numerus:parse_price on pg
begin;
select has_function_privilege('numerus.parse_price(text, integer)', 'execute');
rollback;

View File

@ -1,7 +0,0 @@
-- Verify numerus:to_price on pg
begin;
select has_function_privilege('numerus.to_price(integer, integer)', 'execute');
rollback;

View File

@ -342,13 +342,11 @@ input.width-2x {
color: var(--numerus--color--red);
}
[lang="en"] input:not([required]) + label::after,
[lang="en"] select:not([required]) + label::after {
[lang="en"] input:not([required]) + label::after {
content: " (optional)"
}
[lang="ca"] input:not([required]) + label::after, [lang="es"] input:not([required]) + label::after,
[lang="ca"] select:not([required]) + label::after, [lang="es"] select:not([required]) + label::after {
[lang="ca"] input:not([required]) + label::after, [lang="es"] input:not([required]) + label::after {
content: " (opcional)"
}

View File

@ -18,11 +18,7 @@
<div class="input {{ if .Errors }}has-errors{{ end }}">
<select id="{{ .Name }}-field" name="{{ .Name }}"
{{- range $attribute := .Attributes }} {{$attribute}} {{ end -}}
{{ if .Required }}required="required"{{ end }}
>
{{- with .EmptyLabel }}
<option value="">{{ . }}</option>
{{- end}}
{{- range $option := .Options }}
<option value="{{ .Value }}"
{{- if eq .Value $.Selected }} selected="selected"{{ end }}>{{ .Label }}</option>

View File

@ -29,7 +29,7 @@
<tr>
<td></td>
<td><a href="{{ companyURI "/products/"}}{{ .Slug }}">{{ .Name }}</a></td>
<td>{{ .Price | formatPrice }}</td>
<td>{{ .Price }}</td>
</tr>
{{- end }}
{{ else }}