Add tags for products too
With Oriol we agreed that products should have tags, too, and that the “tag pool”, as it were, should be shared with the one for invoices and contacts. Had to add the `company_id` attribute in the `using` clause for `tag` in `MustFillFromDatabase`, even though it’s not strictly necessary, because then PostgreSQL does not know which `company_id` attribute use for the join with `company`—the one from `product` or the one from `tag`.
This commit is contained in:
parent
4131602fa3
commit
a1f70ff654
|
@ -51,13 +51,13 @@ select add_contact (1, 'Bou', '41414142C', '', '333 333 333', 'bou@pesebre.cat',
|
|||
select add_contact (1, 'Rabadà', '41414143K', '', '444 444 444', 'rabada@pesebre.cat', '', 'C/ De les Ovelles, 6', 'Fornells de la Selva', 'Girona', '17458', 'ES', array['pesebre', 'persona']);
|
||||
|
||||
alter sequence product_product_id_seq restart;
|
||||
select add_product(1, 'Or', 'Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a l’aigua règia.', '55.92', array[2]);
|
||||
select add_product(1, 'Encens', 'Goma resina fragrant que desprèn una olor característica quan es crema.', '2.15', array[2]);
|
||||
select add_product(1, 'Mirra', 'Goma resinosa aromàtica de color gris groguenc i gust amargant.', '6.90', array[2]);
|
||||
select add_product(1, 'Paper higiènic (pack de 32 U)', 'Paper que s’usa per mantenir la higiene personal després de defecar o orinar.', '7.99', array[4]);
|
||||
select add_product(1, 'Cavall Fort', 'Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.', '3.64', array[2]);
|
||||
select add_product(1, 'Palla', 'Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.', '25.00', array[3]);
|
||||
select add_product(1, 'Teia', 'Fusta resinosa de pi i d’altres arbres, provinent sobretot del cor de l’arbre, que crema amb molta facilitat.', '7.00', array[2]);
|
||||
select add_product(1, 'Or', 'Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a l’aigua règia.', '55.92', array[2], array['metall']);
|
||||
select add_product(1, 'Encens', 'Goma resina fragrant que desprèn una olor característica quan es crema.', '2.15', array[2], array['resina']);
|
||||
select add_product(1, 'Mirra', 'Goma resinosa aromàtica de color gris groguenc i gust amargant.', '6.90', array[2], array['resina']);
|
||||
select add_product(1, 'Paper higiènic (pack de 32 U)', 'Paper que s’usa per mantenir la higiene personal després de defecar o orinar.', '7.99', array[4], array['necessitat']);
|
||||
select add_product(1, 'Cavall Fort', 'Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.', '3.64', array[2], array['revista']);
|
||||
select add_product(1, 'Palla', 'Tija seca dels cereals després que el gra o llavor ha estat separat mitjançant la trilla.', '25.00', array[3], array['necessitat']);
|
||||
select add_product(1, 'Teia', 'Fusta resinosa de pi i d’altres arbres, provinent sobretot del cor de l’arbre, que crema amb molta facilitat.', '7.00', array[2], array['obsolet']);
|
||||
|
||||
alter sequence invoice_invoice_id_seq restart;
|
||||
alter sequence invoice_product_invoice_product_id_seq restart;
|
||||
|
|
|
@ -5,12 +5,14 @@
|
|||
-- requires: parse_price
|
||||
-- requires: company
|
||||
-- requires: currency
|
||||
-- requires: tag_name
|
||||
-- requires: tag_product
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function add_product(company_id integer, name text, description text, price text, taxes integer[]) returns uuid
|
||||
create or replace function add_product(company_id integer, name text, description text, price text, taxes integer[], tags tag_name[]) returns uuid
|
||||
as $$
|
||||
declare
|
||||
pid integer;
|
||||
|
@ -28,12 +30,14 @@ begin
|
|||
select pid, tax_id
|
||||
from unnest(taxes) as tax(tax_id);
|
||||
|
||||
perform tag_product(company_id, pid, tags);
|
||||
|
||||
return pslug;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
revoke execute on function add_product(integer, text, text, text, integer[]) from public;
|
||||
grant execute on function add_product(integer, text, text, text, integer[]) to invoicer;
|
||||
grant execute on function add_product(integer, text, text, text, integer[]) to admin;
|
||||
revoke execute on function add_product(integer, text, text, text, integer[], tag_name[]) from public;
|
||||
grant execute on function add_product(integer, text, text, text, integer[], tag_name[]) to invoicer;
|
||||
grant execute on function add_product(integer, text, text, text, integer[], tag_name[]) to admin;
|
||||
|
||||
commit;
|
||||
|
|
|
@ -5,15 +5,18 @@
|
|||
-- requires: parse_price
|
||||
-- requires: company
|
||||
-- requires: currency
|
||||
-- requires: tag_name
|
||||
-- requires: tag_product
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function edit_product(slug uuid, name text, description text, price text, taxes integer[]) returns boolean
|
||||
create or replace function edit_product(slug uuid, name text, description text, price text, taxes integer[], tags tag_name[]) returns boolean
|
||||
as $$
|
||||
declare
|
||||
pid integer;
|
||||
company integer;
|
||||
begin
|
||||
update product
|
||||
set name = edit_product.name
|
||||
|
@ -23,8 +26,8 @@ begin
|
|||
join currency using (currency_code)
|
||||
where product.company_id = company.company_id
|
||||
and product.slug = edit_product.slug
|
||||
returning product_id
|
||||
into pid;
|
||||
returning product_id, product.company_id
|
||||
into pid, company;
|
||||
|
||||
if pid is null then
|
||||
return false;
|
||||
|
@ -36,12 +39,14 @@ begin
|
|||
select pid, tax_id
|
||||
from unnest(taxes) as tax(tax_id);
|
||||
|
||||
perform tag_product(company, pid, tags);
|
||||
|
||||
return true;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
revoke execute on function edit_product(uuid, text, text, text, integer[]) from public;
|
||||
grant execute on function edit_product(uuid, text, text, text, integer[]) to invoicer;
|
||||
grant execute on function edit_product(uuid, text, text, text, integer[]) to admin;
|
||||
revoke execute on function edit_product(uuid, text, text, text, integer[], tag_name[]) from public;
|
||||
grant execute on function edit_product(uuid, text, text, text, integer[], tag_name[]) to invoicer;
|
||||
grant execute on function edit_product(uuid, text, text, text, integer[], tag_name[]) to admin;
|
||||
|
||||
commit;
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
-- Deploy numerus:product_tag to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: tag
|
||||
-- requires: product
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table product_tag (
|
||||
product_id integer not null references product,
|
||||
tag_id integer not null references tag,
|
||||
primary key (product_id, tag_id)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table product_tag to invoicer;
|
||||
grant select, insert, update, delete on table product_tag to admin;
|
||||
|
||||
alter table product_tag enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on product_tag
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from product
|
||||
where product.product_id = product_tag.product_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,22 @@
|
|||
-- Deploy numerus:tag_product to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: tag_name
|
||||
-- requires: tag_relation
|
||||
-- requires: product_tag
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace function tag_product (company_id integer, product_id integer, tags tag_name[]) returns void as
|
||||
$$
|
||||
select tag_relation('product_tag', 'product_id', company_id, product_id, tags);
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function tag_product(integer, integer, tag_name[]) from public;
|
||||
grant execute on function tag_product(integer, integer, tag_name[]) to invoicer;
|
||||
grant execute on function tag_product(integer, integer, tag_name[]) to admin;
|
||||
|
||||
commit;
|
|
@ -14,6 +14,7 @@ type ProductEntry struct {
|
|||
Slug string
|
||||
Name string
|
||||
Price string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type productsIndexPage struct {
|
||||
|
@ -23,8 +24,9 @@ type productsIndexPage struct {
|
|||
func IndexProducts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
conn := getConn(r)
|
||||
company := mustGetCompany(r)
|
||||
tag := r.URL.Query().Get("tag")
|
||||
page := &productsIndexPage{
|
||||
Products: mustGetProductEntries(r.Context(), conn, company),
|
||||
Products: mustCollectProductEntries(r.Context(), conn, company, tag),
|
||||
}
|
||||
mustRenderMainTemplate(w, r, "products/index.gohtml", page)
|
||||
}
|
||||
|
@ -40,7 +42,7 @@ func GetProductForm(w http.ResponseWriter, r *http.Request, params httprouter.Pa
|
|||
mustRenderNewProductForm(w, r, form)
|
||||
return
|
||||
}
|
||||
if notFoundErrorOrPanic(conn.QueryRow(r.Context(), "select product.name, product.description, to_price(price, decimal_digits), array_agg(tax_id) 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, decimal_digits", slug).Scan(form.Name, form.Description, form.Price, form.Tax)) {
|
||||
if !form.MustFillFromDatabase(r.Context(), conn, slug) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
@ -74,12 +76,12 @@ func HandleAddProduct(w http.ResponseWriter, r *http.Request, _ httprouter.Param
|
|||
return
|
||||
}
|
||||
taxes := mustSliceAtoi(form.Tax.Selected)
|
||||
conn.MustExec(r.Context(), "select add_product($1, $2, $3, $4, $5)", company.Id, form.Name, form.Description, form.Price, taxes)
|
||||
conn.MustExec(r.Context(), "select add_product($1, $2, $3, $4, $5, $6)", company.Id, form.Name, form.Description, form.Price, taxes, form.Tags)
|
||||
http.Redirect(w, r, companyURI(company, "/products"), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func sliceAtoi(s []string) ([]int, error) {
|
||||
i := []int{}
|
||||
var i []int
|
||||
for _, vs := range s {
|
||||
vi, err := strconv.Atoi(vs)
|
||||
if err != nil {
|
||||
|
@ -118,14 +120,29 @@ func HandleUpdateProduct(w http.ResponseWriter, r *http.Request, params httprout
|
|||
}
|
||||
slug := params[0].Value
|
||||
taxes := mustSliceAtoi(form.Tax.Selected)
|
||||
if ok := conn.MustGetBool(r.Context(), "select edit_product($1, $2, $3, $4, $5)", slug, form.Name, form.Description, form.Price, taxes); !ok {
|
||||
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 {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
http.Redirect(w, r, companyURI(company, "/products/"+slug), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -134,7 +151,7 @@ func mustGetProductEntries(ctx context.Context, conn *Conn, company *Company) []
|
|||
var entries []*ProductEntry
|
||||
for rows.Next() {
|
||||
entry := &ProductEntry{}
|
||||
err = rows.Scan(&entry.Slug, &entry.Name, &entry.Price)
|
||||
err = rows.Scan(&entry.Slug, &entry.Name, &entry.Price, &entry.Tags)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -154,6 +171,7 @@ type productForm struct {
|
|||
Description *InputField
|
||||
Price *InputField
|
||||
Tax *SelectField
|
||||
Tags *TagsField
|
||||
}
|
||||
|
||||
func newProductForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *productForm {
|
||||
|
@ -187,6 +205,10 @@ func newProductForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
|
|||
Multiple: true,
|
||||
Options: mustGetTaxOptions(ctx, conn, company),
|
||||
},
|
||||
Tags: &TagsField{
|
||||
Name: "tags",
|
||||
Label: pgettext("input", "Tags", locale),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,6 +220,7 @@ func (form *productForm) Parse(r *http.Request) error {
|
|||
form.Description.FillValue(r)
|
||||
form.Price.FillValue(r)
|
||||
form.Tax.FillValue(r)
|
||||
form.Tags.FillValue(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -211,3 +234,30 @@ func (form *productForm) Validate() bool {
|
|||
validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale))
|
||||
return validator.AllOK()
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
begin;
|
||||
|
||||
drop function if exists numerus.add_product(integer, text, text, text, integer[]);
|
||||
drop function if exists numerus.add_product(integer, text, text, text, integer[], numerus.tag_name[]);
|
||||
|
||||
commit;
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
begin;
|
||||
|
||||
drop function if exists numerus.edit_product(uuid, text, text, text, integer[]);
|
||||
drop function if exists numerus.edit_product(uuid, text, text, text, integer[], numerus.tag_name[]);
|
||||
|
||||
commit;
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:product_tag from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop table if exists numerus.product_tag;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:tag_product from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists numerus.tag_product(integer, integer, numerus.tag_name[]);
|
||||
|
||||
commit;
|
12
sqitch.plan
12
sqitch.plan
|
@ -52,16 +52,18 @@ product_tax [schema_numerus product tax] 2023-02-08T11:36:49Z jordi fita mas <jo
|
|||
invoice [schema_numerus company contact invoice_status payment_method currency] 2023-02-09T09:52:21Z jordi fita mas <jordi@tandem.blog> # Add relation for invoice
|
||||
discount_rate [schema_numerus] 2023-02-10T17:22:40Z jordi fita mas <jordi@tandem.blog> # Add domain for discount rates
|
||||
invoice_product [schema_numerus invoice discount_rate] 2023-02-10T17:07:08Z jordi fita mas <jordi@tandem.blog> # Add relation for invoice product
|
||||
add_product [schema_numerus product product_tax parse_price company currency] 2023-02-14T10:32:18Z jordi fita mas <jordi@tandem.blog> # Add function to add new products
|
||||
edit_product [schema_numerus product product_tax parse_price company currency] 2023-02-14T11:06:03Z jordi fita mas <jordi@tandem.blog> # Add function to edit products
|
||||
tag_name [schema_numerus] 2023-03-10T11:06:11Z jordi fita mas <jordi@tandem.blog> # Add domain for tag names
|
||||
tag [schema_numerus tag_name] 2023-03-10T11:04:24Z jordi fita mas <jordi@tandem.blog> # Add relation for tags
|
||||
tag_relation [schema_numerus tag tag_name] 2023-03-25T17:40:52Z jordi fita mas <jordi@tandem.blog> # Add function to tag “relations”
|
||||
product_tag [schema_numerus tag product] 2023-03-26T10:54:23Z jordi fita mas <jordi@tandem.blog> # Add relation for product tag
|
||||
tag_product [schema_numerus tag_name tag_relation product_tag] 2023-03-26T11:01:17Z jordi fita mas <jordi@tandem.blog> # Add function to tag products
|
||||
add_product [schema_numerus product product_tax parse_price company currency tag_name tag_product] 2023-02-14T10:32:18Z jordi fita mas <jordi@tandem.blog> # Add function to add new products
|
||||
edit_product [schema_numerus product product_tax parse_price company currency tag_name tag_product] 2023-02-14T11:06:03Z jordi fita mas <jordi@tandem.blog> # Add function to edit products
|
||||
invoice_product_tax [schema_numerus invoice_product tax tax_rate] 2023-02-15T13:20:30Z jordi fita mas <jordi@tandem.blog> # Add relation for taxes in invoice products
|
||||
new_invoice_product [schema_numerus discount_rate] 2023-02-16T21:06:01Z jordi fita mas <jordi@tandem.blog> # Add type for passing products to new invoices
|
||||
invoice_number_counter [schema_numerus company] 2023-02-17T13:04:48Z jordi fita mas <jordi@tandem.blog> # Add relation to count invoice numbers
|
||||
next_invoice_number [schema_numerus invoice_number_counter] 2023-02-17T13:21:48Z jordi fita mas <jordi@tandem.blog> # Add function to retrieve the next invoice number
|
||||
tag_name [schema_numerus] 2023-03-10T11:06:11Z jordi fita mas <jordi@tandem.blog> # Add domain for tag names
|
||||
tag [schema_numerus tag_name] 2023-03-10T11:04:24Z jordi fita mas <jordi@tandem.blog> # Add relation for tags
|
||||
invoice_tag [schema_numerus tag invoice] 2023-03-10T11:37:43Z jordi fita mas <jordi@tandem.blog> # Add relation for invoice tag
|
||||
tag_relation [schema_numerus tag tag_name] 2023-03-25T17:40:52Z jordi fita mas <jordi@tandem.blog> # Add function to tag “relations”
|
||||
tag_invoice [schema_numerus tag_name tag_relation invoice_tag] 2023-03-25T18:04:02Z jordi fita mas <jordi@tandem.blog> # Add function to tag invoices
|
||||
add_invoice [schema_numerus invoice company currency parse_price new_invoice_product tax invoice_product invoice_product_tax next_invoice_number tag_name tag_invoice] 2023-02-16T21:12:46Z jordi fita mas <jordi@tandem.blog> # Add function to create new invoices
|
||||
invoice_tax_amount [schema_numerus invoice_product invoice_product_tax] 2023-02-22T12:08:35Z jordi fita mas <jordi@tandem.blog> # Add view for invoice tax amount
|
||||
|
|
|
@ -90,6 +90,8 @@ select bag_eq(
|
|||
$$,
|
||||
'Should have assigned the tags to contacts'
|
||||
);
|
||||
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
|
|
|
@ -5,22 +5,24 @@ reset client_min_messages;
|
|||
|
||||
begin;
|
||||
|
||||
select plan(14);
|
||||
select plan(16);
|
||||
|
||||
set search_path to auth, numerus, public;
|
||||
|
||||
select has_function('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]']);
|
||||
select function_lang_is('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]'], 'plpgsql');
|
||||
select function_returns('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]'], 'uuid');
|
||||
select isnt_definer('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]']);
|
||||
select volatility_is('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]'], 'volatile');
|
||||
select function_privs_are('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]'], 'guest', array []::text[]);
|
||||
select function_privs_are('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]'], 'invoicer', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]'], 'admin', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]'], 'authenticator', array []::text[]);
|
||||
select has_function('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]', 'tag_name[]']);
|
||||
select function_lang_is('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]', 'tag_name[]'], 'plpgsql');
|
||||
select function_returns('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]', 'tag_name[]'], 'uuid');
|
||||
select isnt_definer('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]', 'tag_name[]']);
|
||||
select volatility_is('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]', 'tag_name[]'], 'volatile');
|
||||
select function_privs_are('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]', 'tag_name[]'], 'guest', array []::text[]);
|
||||
select function_privs_are('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]', 'tag_name[]'], 'invoicer', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]', 'tag_name[]'], 'admin', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'add_product', array ['integer', 'text', 'text', 'text', 'integer[]', 'tag_name[]'], 'authenticator', array []::text[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate product_tag cascade;
|
||||
truncate tag cascade;
|
||||
truncate product_tax cascade;
|
||||
truncate product cascade;
|
||||
truncate tax cascade;
|
||||
|
@ -57,17 +59,17 @@ values (3, 1, 11, 'IRPF -15 %', -0.15)
|
|||
|
||||
|
||||
select lives_ok(
|
||||
$$ select add_product(1, 'Product 1', 'Description 1', '12.12', array[3, 4]) $$,
|
||||
$$ select add_product(1, 'Product 1', 'Description 1', '12.12', array[3, 4], array['tag1']) $$,
|
||||
'Should be able to add product to first company'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select add_product(2, 'Product 2', 'Description 2', '24.24', array[6]) $$,
|
||||
$$ select add_product(2, 'Product 2', 'Description 2', '24.24', array[6], array['tag1', 'tag2']) $$,
|
||||
'Should be able to add product to second company'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select add_product(2, 'Product 3', 'Description 3', '36.36', array[]::integer[]) $$,
|
||||
$$ select add_product(2, 'Product 3', 'Description 3', '36.36', array[]::integer[], array[]::tag_name[]) $$,
|
||||
'Should be able to add product without taxes'
|
||||
);
|
||||
|
||||
|
@ -89,6 +91,25 @@ select bag_eq(
|
|||
'Should have added the taxes for the products we told to'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select company_id, name from tag $$,
|
||||
$$ values (1, 'tag1')
|
||||
, (2, 'tag1')
|
||||
, (2, 'tag2')
|
||||
$$,
|
||||
'Should have added all new tags once'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select product.name as product_name, tag.name as tag_name from product_tag join product using (product_id) join tag using (tag_id) $$,
|
||||
$$ values ('Product 1', 'tag1')
|
||||
, ('Product 2', 'tag1')
|
||||
, ('Product 2', 'tag2')
|
||||
$$,
|
||||
'Should have assigned the tags to products'
|
||||
);
|
||||
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
|
|
|
@ -5,22 +5,24 @@ reset client_min_messages;
|
|||
|
||||
begin;
|
||||
|
||||
select plan(15);
|
||||
select plan(17);
|
||||
|
||||
set search_path to auth, numerus, public;
|
||||
|
||||
select has_function('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]']);
|
||||
select function_lang_is('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]'], 'plpgsql');
|
||||
select function_returns('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]'], 'boolean');
|
||||
select isnt_definer('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]']);
|
||||
select volatility_is('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]'], 'volatile');
|
||||
select function_privs_are('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]'], 'guest', array []::text[]);
|
||||
select function_privs_are('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]'], 'invoicer', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]'], 'admin', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]'], 'authenticator', array []::text[]);
|
||||
select has_function('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]', 'tag_name[]']);
|
||||
select function_lang_is('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]', 'tag_name[]'], 'plpgsql');
|
||||
select function_returns('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]', 'tag_name[]'], 'boolean');
|
||||
select isnt_definer('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]', 'tag_name[]']);
|
||||
select volatility_is('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]', 'tag_name[]'], 'volatile');
|
||||
select function_privs_are('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]', 'tag_name[]'], 'guest', array []::text[]);
|
||||
select function_privs_are('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]', 'tag_name[]'], 'invoicer', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]', 'tag_name[]'], 'admin', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'edit_product', array ['uuid', 'text', 'text', 'text', 'integer[]', 'tag_name[]'], 'authenticator', array []::text[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate product_tag cascade;
|
||||
truncate tag cascade;
|
||||
truncate product_tax cascade;
|
||||
truncate product cascade;
|
||||
truncate tax cascade;
|
||||
|
@ -68,26 +70,40 @@ values (7, 3)
|
|||
, (9, 6)
|
||||
;
|
||||
|
||||
insert into tag (tag_id, company_id, name)
|
||||
values (10, 1, 'tag1')
|
||||
, (11, 2, 'tag2')
|
||||
;
|
||||
-- edit_product uses the sequence and sometimes it would confict
|
||||
alter sequence tag_tag_id_seq restart with 15;
|
||||
|
||||
insert into product_tag (product_id, tag_id)
|
||||
values (7, 10)
|
||||
, (8, 11)
|
||||
, (9, 11)
|
||||
;
|
||||
|
||||
|
||||
select is(
|
||||
edit_product('d2300404-bd23-48b3-8e2b-2bbf86dc7bd6', 'Product 1', 'Description 1', '12.12', array[3, 4]),
|
||||
edit_product('d2300404-bd23-48b3-8e2b-2bbf86dc7bd6', 'Product 1', 'Description 1', '12.12', array[3, 4], array['tag1']),
|
||||
true,
|
||||
'Should be able to edit product from first company'
|
||||
);
|
||||
|
||||
select is(
|
||||
edit_product('2f085b8b-da90-41fe-b8cf-6ba8d94cfa38', 'Product 2', 'Description 2', '24.24', array[6]),
|
||||
edit_product('2f085b8b-da90-41fe-b8cf-6ba8d94cfa38', 'Product 2', 'Description 2', '24.24', array[6], array['tag1', 'tag3']),
|
||||
true,
|
||||
'Should be able to edit product from second company'
|
||||
);
|
||||
|
||||
select is(
|
||||
edit_product('84044d0b-af33-442a-95a6-21efc77260d5', 'Product 3', 'Description 3', '36.36', array[]::integer[]),
|
||||
edit_product('84044d0b-af33-442a-95a6-21efc77260d5', 'Product 3', 'Description 3', '36.36', array[]::integer[], array[]::tag_name[]),
|
||||
true,
|
||||
'Should be able to edit a product a remove all taxes'
|
||||
);
|
||||
|
||||
select is(
|
||||
edit_product('87e158d1-a0f5-48a7-854b-b86d7b4bb21c', 'Product 4', 'Description 4', '48.48', array[]::integer[]),
|
||||
edit_product('87e158d1-a0f5-48a7-854b-b86d7b4bb21c', 'Product 4', 'Description 4', '48.48', array[]::integer[], array[]::tag_name[]),
|
||||
false,
|
||||
'Should return false when the product does not exist'
|
||||
);
|
||||
|
@ -110,6 +126,25 @@ select bag_eq(
|
|||
'Should have updated the taxes for the products we told to'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select company_id, name from tag $$,
|
||||
$$ values (1, 'tag1')
|
||||
, (2, 'tag1')
|
||||
, (2, 'tag2')
|
||||
, (2, 'tag3')
|
||||
$$,
|
||||
'Should have added all new tags'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select product.name as product_name, tag.name as tag_name from product_tag join product using (product_id) join tag using (tag_id) $$,
|
||||
$$ values ('Product 1', 'tag1')
|
||||
, ('Product 2', 'tag1')
|
||||
, ('Product 2', 'tag3')
|
||||
$$,
|
||||
'Should have assigned the tags to products'
|
||||
);
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
-- Test product_tag
|
||||
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, auth, public;
|
||||
|
||||
select has_table('product_tag');
|
||||
select has_pk('product_tag');
|
||||
select col_is_pk('product_tag', array['product_id', 'tag_id']);
|
||||
select table_privs_are('product_tag', 'guest', array []::text[]);
|
||||
select table_privs_are('product_tag', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
|
||||
select table_privs_are('product_tag', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
|
||||
select table_privs_are('product_tag', 'authenticator', array []::text[]);
|
||||
|
||||
select has_column('product_tag', 'product_id');
|
||||
select col_is_fk('product_tag', 'product_id');
|
||||
select fk_ok('product_tag', 'product_id', 'product', 'product_id');
|
||||
select col_type_is('product_tag', 'product_id', 'integer');
|
||||
select col_not_null('product_tag', 'product_id');
|
||||
select col_hasnt_default('product_tag', 'product_id');
|
||||
|
||||
select has_column('product_tag', 'tag_id');
|
||||
select col_is_fk('product_tag', 'tag_id');
|
||||
select fk_ok('product_tag', 'tag_id', 'tag', 'tag_id');
|
||||
select col_type_is('product_tag', 'tag_id', 'integer');
|
||||
select col_not_null('product_tag', 'tag_id');
|
||||
select col_hasnt_default('product_tag', 'tag_id');
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate product_tag cascade;
|
||||
truncate product cascade;
|
||||
truncate tag cascade;
|
||||
truncate company_user cascade;
|
||||
truncate company cascade;
|
||||
truncate payment_method cascade;
|
||||
truncate auth."user" cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
|
||||
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
|
||||
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
|
||||
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222)
|
||||
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (444, 4, 'cash', 'cash')
|
||||
, (222, 2, 'cash', 'cash')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" immediate;
|
||||
|
||||
insert into company_user (company_id, user_id)
|
||||
values (2, 1)
|
||||
, (4, 5)
|
||||
;
|
||||
|
||||
insert into product (product_id, company_id, name, description, price)
|
||||
values (6, 2, 'Product 1', 'Description 1', 1200)
|
||||
, (8, 4, 'Product 2', 'Description 2', 2400)
|
||||
;
|
||||
|
||||
insert into tag (tag_id, company_id, name)
|
||||
values (14, 2, 'web')
|
||||
, (15, 2, 'design')
|
||||
, (16, 4, 'product')
|
||||
, (17, 4, 'development')
|
||||
, (18, 4, 'something-else')
|
||||
, (19, 4, 'design')
|
||||
;
|
||||
|
||||
insert into product_tag (product_id, tag_id)
|
||||
values (6, 14)
|
||||
, (6, 15)
|
||||
, (8, 18)
|
||||
;
|
||||
|
||||
prepare product_tag_data as
|
||||
select product_id, tag_id
|
||||
from product_tag
|
||||
;
|
||||
|
||||
set role invoicer;
|
||||
select is_empty('product_tag_data', 'Should show no data when cookie is not set yet');
|
||||
reset role;
|
||||
|
||||
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
|
||||
select bag_eq(
|
||||
'product_tag_data',
|
||||
$$ values ( 6, 14 )
|
||||
, ( 6, 15 )
|
||||
$$,
|
||||
'Should only list product tags of the companies where demo@tandem.blog is user of'
|
||||
);
|
||||
reset role;
|
||||
|
||||
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
|
||||
select bag_eq(
|
||||
'product_tag_data',
|
||||
$$ values ( 8, 18 )
|
||||
$$,
|
||||
'Should only list product tags of the companies where admin@tandem.blog is user of'
|
||||
);
|
||||
reset role;
|
||||
|
||||
select set_cookie('not-a-cookie');
|
||||
select throws_ok(
|
||||
'product_tag_data',
|
||||
'42501', 'permission denied for table product_tag',
|
||||
'Should not allow select to guest users'
|
||||
);
|
||||
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
-- Test tag_product
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(17);
|
||||
|
||||
set search_path to numerus, auth, public;
|
||||
|
||||
select has_function('numerus', 'tag_product', array ['integer', 'integer', 'tag_name[]']);
|
||||
select function_lang_is('numerus', 'tag_product', array ['integer', 'integer', 'tag_name[]'], 'sql');
|
||||
select function_returns('numerus', 'tag_product', array ['integer', 'integer', 'tag_name[]'], 'void');
|
||||
select isnt_definer('numerus', 'tag_product', array ['integer', 'integer', 'tag_name[]']);
|
||||
select volatility_is('numerus', 'tag_product', array ['integer', 'integer', 'tag_name[]'], 'volatile');
|
||||
select function_privs_are('numerus', 'tag_product', array ['integer', 'integer', 'tag_name[]'], 'guest', array []::text[]);
|
||||
select function_privs_are('numerus', 'tag_product', array ['integer', 'integer', 'tag_name[]'], 'invoicer', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'tag_product', array ['integer', 'integer', 'tag_name[]'], 'admin', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'tag_product', array ['integer', 'integer', 'tag_name[]'], 'authenticator', array []::text[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate product_tag cascade;
|
||||
truncate tag cascade;
|
||||
truncate product cascade;
|
||||
truncate payment_method cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" deferred;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_payment_method_id)
|
||||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111)
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (111, 1, 'cash', 'cash')
|
||||
, (112, 1, 'bank', 'send money to my bank account')
|
||||
;
|
||||
|
||||
set constraints "company_default_payment_method_id_fkey" immediate;
|
||||
|
||||
insert into tag (tag_id, company_id, name)
|
||||
values (10, 1, 'tag1')
|
||||
, (11, 1, 'tag2')
|
||||
;
|
||||
-- tag_product uses the sequence and sometimes it would confict
|
||||
alter sequence tag_tag_id_seq restart with 15;
|
||||
|
||||
|
||||
insert into product (product_id, company_id, name, description, price)
|
||||
values (12, 1, 'Product 1', 'Description 1', 1200)
|
||||
, (13, 1, 'Product 2', 'Description 2', 2400)
|
||||
;
|
||||
|
||||
insert into product_tag (product_id, tag_id)
|
||||
values (12, 10)
|
||||
, (13, 11)
|
||||
;
|
||||
|
||||
prepare current_tags as
|
||||
select product_id, tag.name
|
||||
from product
|
||||
join product_tag using (product_id)
|
||||
join tag using (tag_id);
|
||||
|
||||
select lives_ok(
|
||||
$$ select tag_product(1, 12, array['tag1']) $$,
|
||||
'Should be able to keep the same tags to the product'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'current_tags',
|
||||
$$ values (12, 'tag1')
|
||||
, (13, 'tag2')
|
||||
$$,
|
||||
'Should not have changed any product tag'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select tag_product(1, 12, array['tag1', 'tag2']) $$,
|
||||
'Should be able to add tag2 product'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'current_tags',
|
||||
$$ values (12, 'tag1')
|
||||
, (12, 'tag2')
|
||||
, (13, 'tag2')
|
||||
$$,
|
||||
'Should have added tag2 to product'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select tag_product(1, 13, array['tag3']) $$,
|
||||
'Should be able to replace all tags of an product with a new one'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'current_tags',
|
||||
$$ values (12, 'tag1')
|
||||
, (12, 'tag2')
|
||||
, (13, 'tag3')
|
||||
$$,
|
||||
'Should have set tag3 to product'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select tag_product(1, 12, array[]::tag_name[]) $$,
|
||||
'Should be able to remove all tags from an product'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'current_tags',
|
||||
$$ values (13, 'tag3')
|
||||
$$,
|
||||
'Should have remove all tags from product'
|
||||
);
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('numerus.add_product(integer, text, text, text, integer[])', 'execute');
|
||||
select has_function_privilege('numerus.add_product(integer, text, text, text, integer[], numerus.tag_name[])', 'execute');
|
||||
|
||||
rollback;
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('numerus.edit_product(uuid, text, text, text, integer[])', 'execute');
|
||||
select has_function_privilege('numerus.edit_product(uuid, text, text, text, integer[], numerus.tag_name[])', 'execute');
|
||||
|
||||
rollback;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
-- Verify numerus:product_tag on pg
|
||||
|
||||
begin;
|
||||
|
||||
select product_id
|
||||
, tag_id
|
||||
from numerus.product_tag
|
||||
where false;
|
||||
|
||||
select 1 / count(*) from pg_class where oid = 'numerus.product_tag'::regclass and relrowsecurity;
|
||||
select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.product_tag'::regclass;
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:tag_product on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('numerus.tag_product(integer, integer, numerus.tag_name[])', 'execute');
|
||||
|
||||
rollback;
|
|
@ -25,6 +25,7 @@
|
|||
{{ template "input-field" .Description }}
|
||||
{{ template "input-field" .Price }}
|
||||
{{ template "select-field" .Tax }}
|
||||
{{ template "tags-field" .Tags }}
|
||||
|
||||
<fieldset>
|
||||
<button class="primary" type="submit">{{( pgettext "Update product" "action" )}}</button>
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
<tr>
|
||||
<th>{{( pgettext "All" "product" )}}</th>
|
||||
<th>{{( pgettext "Name" "title" )}}</th>
|
||||
<th>{{( pgettext "Tags" "title" )}}</th>
|
||||
<th>{{( pgettext "Price" "title" )}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -32,6 +33,12 @@
|
|||
<tr>
|
||||
<td></td>
|
||||
<td><a href="{{ companyURI "/products/"}}{{ .Slug }}">{{ .Name }}</a></td>
|
||||
<td>
|
||||
{{- range $index, $tag := .Tags }}
|
||||
{{- if gt $index 0 }}, {{ end -}}
|
||||
<a href="?tag={{ . }}">{{ . }}</a>
|
||||
{{- end }}
|
||||
</td>
|
||||
<td class="numeric">{{ .Price | formatPrice }}</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
{{ template "input-field" .Description }}
|
||||
{{ template "input-field" .Price }}
|
||||
{{ template "select-field" .Tax }}
|
||||
{{ template "tags-field" .Tags }}
|
||||
|
||||
<fieldset>
|
||||
<button class="primary" type="submit">{{( pgettext "New product" "action" )}}</button>
|
||||
|
|
Loading…
Reference in New Issue