From 4131602fa35322a8bf9a76e622c6a8e7569ab675 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Sun, 26 Mar 2023 01:32:53 +0100 Subject: [PATCH] Add tags for contacts too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With Oriol we agreed that contacts should have tags, too, and that the “tag pool”, as it were, should be shared with the one for invoices (and all future tags we might add). I added the contact_tag relation and tag_contact function, just like with invoices, and then realized that the SQL queries that Go had to execute were becoming “complex” enough: i had to get not only the slug, but the contact id to call tag_contact, and all inside a transaction. Therefore, i opted to create the add_contact and edit_contact functions, that mirror those for invoice and products, so now each “major” section has these functions. They also simplified a bit the handling of the VATIN and phone numbers, because it is now encapsuled inside the PL/pgSQL function and Go does not know how to assemble the parts. --- demo/demo.sql | 16 ++-- deploy/add_contact.sql | 38 +++++++++ deploy/contact.sql | 2 - deploy/contact_tag.sql | 31 +++++++ deploy/edit_contact.sql | 55 ++++++++++++ deploy/tag_contact.sql | 22 +++++ pkg/contacts.go | 81 ++++++++++++++++-- pkg/form.go | 45 ++++++++++ pkg/invoices.go | 19 +---- revert/add_contact.sql | 7 ++ revert/contact_tag.sql | 7 ++ revert/edit_contact.sql | 7 ++ revert/tag_contact.sql | 7 ++ sqitch.plan | 6 +- test/add_contact.sql | 96 +++++++++++++++++++++ test/contact_tag.sql | 130 +++++++++++++++++++++++++++++ test/edit_contact.sql | 101 ++++++++++++++++++++++ test/tag_contact.sql | 124 +++++++++++++++++++++++++++ verify/add_contact.sql | 7 ++ verify/contact_tag.sql | 13 +++ verify/edit_contact.sql | 7 ++ verify/tag_contact.sql | 7 ++ web/template/contacts/edit.gohtml | 1 + web/template/contacts/index.gohtml | 9 +- web/template/contacts/new.gohtml | 1 + web/template/form.gohtml | 2 +- 26 files changed, 805 insertions(+), 36 deletions(-) create mode 100644 deploy/add_contact.sql create mode 100644 deploy/contact_tag.sql create mode 100644 deploy/edit_contact.sql create mode 100644 deploy/tag_contact.sql create mode 100644 revert/add_contact.sql create mode 100644 revert/contact_tag.sql create mode 100644 revert/edit_contact.sql create mode 100644 revert/tag_contact.sql create mode 100644 test/add_contact.sql create mode 100644 test/contact_tag.sql create mode 100644 test/edit_contact.sql create mode 100644 test/tag_contact.sql create mode 100644 verify/add_contact.sql create mode 100644 verify/contact_tag.sql create mode 100644 verify/edit_contact.sql create mode 100644 verify/tag_contact.sql diff --git a/demo/demo.sql b/demo/demo.sql index 45c0a31..4e0b066 100644 --- a/demo/demo.sql +++ b/demo/demo.sql @@ -41,15 +41,14 @@ values (1, 1, 'Retenció 15 %', -0.15) , (1, 2, 'IVA 4 %', 0.04) ; +alter sequence tag_tag_id_seq restart; alter sequence contact_contact_id_seq restart; -insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) -values (1, 'Melcior', 'IR1', 'Rei Blanc', parse_packed_phone_number('0732621', 'IR'), 'melcio@reismags.cat', '', 'C/ Principal, 1', 'Shiraz', 'Fars', '1', 'IR') - , (1, 'Gaspar', 'IN2', 'Rei Ros', parse_packed_phone_number('111', 'IN'), 'gaspar@reismags.cat', '', 'C/ Principal, 2', 'Nova Delhi', 'Delhi', '2', 'IN') - , (1, 'Baltasar', 'YE3', 'Rei Negre', parse_packed_phone_number('1-111-111', 'YE'), 'baltasar@reismags.cat', '', 'C/ Principal, 3', 'Sanaa', 'Sanaa', '3', 'YE') - , (1, 'Caganera', 'ES41414141L', '', parse_packed_phone_number('222 222 222', 'ES'), 'caganera@pesebre.cat', '', 'C/ De l’Hort, 4', 'Olot', 'Girona', '17800', 'ES') - , (1, 'Bou', 'ES41414142C', '', parse_packed_phone_number('333 333 333', 'ES'), 'bou@pesebre.cat', '', 'C/ De la Palla, 5', 'Sant Climent Sescebes', 'Girona', '17751', 'ES') - , (1, 'Rabadà', 'ES41414143K', '', parse_packed_phone_number('444 444 444', 'ES'), 'rabada@pesebre.cat', '', 'C/ De les Ovelles, 6', 'Fornells de la Selva', 'Girona', '17458', 'ES') -; +select add_contact (1, 'Melcior', '1', 'Rei Blanc', '0732621', 'melcio@reismags.cat', '', 'C/ Principal, 1', 'Shiraz', 'Fars', '1', 'IR', array['pesebre', 'mag']); +select add_contact (1, 'Gaspar', '2', 'Rei Ros', '111', 'gaspar@reismags.cat', '', 'C/ Principal, 2', 'Nova Delhi', 'Delhi', '2', 'IN', array['pesebre', 'mag']); +select add_contact (1, 'Baltasar', '3', 'Rei Negre', '1-111-111', 'baltasar@reismags.cat', '', 'C/ Principal, 3', 'Sanaa', 'Sanaa', '3', 'YE', array['pesebre', 'mag']); +select add_contact (1, 'Caganera', '41414141L', '', '222 222 222', 'caganera@pesebre.cat', '', 'C/ De l’Hort, 4', 'Olot', 'Girona', '17800', 'ES', array['pesebre', 'persona']); +select add_contact (1, 'Bou', '41414142C', '', '333 333 333', 'bou@pesebre.cat', '', 'C/ De la Palla, 5', 'Sant Climent Sescebes', 'Girona', '17751', 'ES', array['pesebre', 'bestia']); +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]); @@ -60,7 +59,6 @@ select add_product(1, 'Cavall Fort', 'Revista quinzenal en llengua catalana i de 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]); -alter sequence tag_tag_id_seq restart; alter sequence invoice_invoice_id_seq restart; alter sequence invoice_product_invoice_product_id_seq restart; select add_invoice(1, '', (current_date - '28 days'::interval)::date, 6, 'Vol esmorzar!', 1, '{producte}','{"(1,Teia,\"Fusta resinosa de pi i d’altres arbres, provinent sobretot del cor de l’arbre, que crema amb molta facilitat.\",7.00,1,0.0,{2})","(5,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.64,1,0.0,{2})"}'); diff --git a/deploy/add_contact.sql b/deploy/add_contact.sql new file mode 100644 index 0000000..c991cba --- /dev/null +++ b/deploy/add_contact.sql @@ -0,0 +1,38 @@ +-- Deploy numerus:add_contact to pg +-- requires: schema_numerus +-- requires: extension_vat +-- requires: email +-- requires: extension_pg_libphonenumber +-- requires: extension_uri +-- requires: country_code +-- requires: contact +-- requires: tag_name + +begin; + +set search_path to numerus, public; + +create or replace function add_contact(company_id integer, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as +$$ +declare + cid integer; + cslug uuid; +begin + insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) + values (add_contact.company_id, add_contact.business_name, (add_contact.country_code || add_contact.vatin)::vatin, add_contact.trade_name, parse_packed_phone_number(add_contact.phone, add_contact.country_code), add_contact.email, add_contact.web, add_contact.address, add_contact.city, add_contact.province, add_contact.postal_code, add_contact.country_code) + returning contact_id, slug + into cid, cslug; + + perform tag_contact(company_id, cid, tags); + + return cslug; +end +$$ + language plpgsql +; + +revoke execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public; +grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer; +grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin; + +commit; diff --git a/deploy/contact.sql b/deploy/contact.sql index 912c8db..bca5c5b 100644 --- a/deploy/contact.sql +++ b/deploy/contact.sql @@ -5,8 +5,6 @@ -- requires: email -- requires: extension_pg_libphonenumber -- requires: extension_uri --- requires: currency_code --- requires: currency -- requires: country_code -- requires: country diff --git a/deploy/contact_tag.sql b/deploy/contact_tag.sql new file mode 100644 index 0000000..6d1010e --- /dev/null +++ b/deploy/contact_tag.sql @@ -0,0 +1,31 @@ +-- Deploy numerus:contact_tag to pg +-- requires: schema_numerus +-- requires: tag +-- requires: contact + +begin; + +set search_path to numerus, public; + +create table contact_tag ( + contact_id integer not null references contact, + tag_id integer not null references tag, + primary key (contact_id, tag_id) +); + +grant select, insert, update, delete on table contact_tag to invoicer; +grant select, insert, update, delete on table contact_tag to admin; + +alter table contact_tag enable row level security; + +create policy company_policy +on contact_tag +using ( + exists( + select 1 + from contact + where contact.contact_id = contact_tag.contact_id + ) +); + +commit; diff --git a/deploy/edit_contact.sql b/deploy/edit_contact.sql new file mode 100644 index 0000000..db3a563 --- /dev/null +++ b/deploy/edit_contact.sql @@ -0,0 +1,55 @@ +-- Deploy numerus:edit_contact to pg +-- requires: schema_numerus +-- requires: email +-- requires: extension_uri +-- requires: country_code +-- requires: tag_name +-- requires: contact +-- requires: tag_contact +-- requires: extension_vat +-- requires: extension_pg_libphonenumber + +begin; + +set search_path to numerus, public; + +create or replace function edit_contact(contact_slug uuid, business_name text, vatin text, trade_name text, phone text, email email, web uri, address text, city text, province text, postal_code text, country_code country_code, tags tag_name[]) returns uuid as +$$ +declare + cid integer; + company integer; +begin + update contact + set business_name = edit_contact.business_name + , vatin = (edit_contact.country_code || edit_contact.vatin)::vatin + , trade_name = edit_contact.trade_name + , phone = parse_packed_phone_number( edit_contact.phone, edit_contact.country_code) + , email = edit_contact.email + , web = edit_contact.web + , address = edit_contact.address + , city = edit_contact.city + , province = edit_contact.province + , postal_code = edit_contact.postal_code + , country_code = edit_contact.country_code + where slug = contact_slug + returning contact_id, company_id + into cid, company + ; + + if cid is null then + return null; + end if; + + perform tag_contact(company, cid, tags); + + return contact_slug; +end +$$ + language plpgsql +; + +revoke execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) from public; +grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer; +grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin; + +commit; diff --git a/deploy/tag_contact.sql b/deploy/tag_contact.sql new file mode 100644 index 0000000..02301a5 --- /dev/null +++ b/deploy/tag_contact.sql @@ -0,0 +1,22 @@ +-- Deploy numerus:tag_contact to pg +-- requires: schema_numerus +-- requires: tag_name +-- requires: tag_relation +-- requires: contact_tag + +begin; + +set search_path to numerus, public; + +create or replace function tag_contact (company_id integer, contact_id integer, tags tag_name[]) returns void as +$$ + select tag_relation('contact_tag', 'contact_id', company_id, contact_id, tags); +$$ + language sql +; + +revoke execute on function tag_contact(integer, integer, tag_name[]) from public; +grant execute on function tag_contact(integer, integer, tag_name[]) to invoicer; +grant execute on function tag_contact(integer, integer, tag_name[]) to admin; + +commit; diff --git a/pkg/contacts.go b/pkg/contacts.go index b298a80..0028446 100644 --- a/pkg/contacts.go +++ b/pkg/contacts.go @@ -12,6 +12,7 @@ type ContactEntry struct { Name string Email string Phone string + Tags []string } type ContactsIndexPage struct { @@ -21,8 +22,9 @@ type ContactsIndexPage struct { func IndexContacts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { conn := getConn(r) company := mustGetCompany(r) + tag := r.URL.Query().Get("tag") page := &ContactsIndexPage{ - Contacts: mustGetContactEntries(r.Context(), conn, company), + Contacts: mustCollectContactEntries(r.Context(), conn, company, tag), } mustRenderMainTemplate(w, r, "contacts/index.gohtml", page) } @@ -37,7 +39,7 @@ func GetContactForm(w http.ResponseWriter, r *http.Request, params httprouter.Pa mustRenderNewContactForm(w, r, form) return } - if notFoundErrorOrPanic(conn.QueryRow(r.Context(), "select business_name, substr(vatin::text, 3), trade_name, phone, email, web, address, city, province, postal_code, country_code from contact where slug = $1", slug).Scan(form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country)) { + if !form.MustFillFromDatabase(r.Context(), conn, slug) { http.NotFound(w, r) return } @@ -84,7 +86,7 @@ func HandleAddContact(w http.ResponseWriter, r *http.Request, _ httprouter.Param return } company := mustGetCompany(r) - conn.MustExec(r.Context(), "insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) values ($1, $2, ($12 || $3)::vatin, $4, parse_packed_phone_number($5, $12), $6, $7, $8, $9, $10, $11, $12)", company.Id, form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country) + conn.MustExec(r.Context(), "select add_contact($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", company.Id, form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Tags) if IsHTMxRequest(r) { w.Header().Set("HX-Trigger", "closeModal") w.Header().Set("HX-Refresh", "true") @@ -110,7 +112,7 @@ func HandleUpdateContact(w http.ResponseWriter, r *http.Request, params httprout mustRenderEditContactForm(w, r, params[0].Value, form) return } - slug := conn.MustGetText(r.Context(), "", "update contact set business_name = $1, vatin = ($11 || $2)::vatin, trade_name = $3, phone = parse_packed_phone_number($4, $11), email = $5, web = $6, address = $7, city = $8, province = $9, postal_code = $10, country_code = $11 where slug = $12 returning slug", form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, params[0].Value) + slug := conn.MustGetText(r.Context(), "", "select edit_contact($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", params[0].Value, form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Tags) if slug == "" { http.NotFound(w, r) } @@ -123,8 +125,23 @@ func HandleUpdateContact(w http.ResponseWriter, r *http.Request, params httprout } } -func mustGetContactEntries(ctx context.Context, conn *Conn, company *Company) []*ContactEntry { - rows, err := conn.Query(ctx, "select slug, business_name, email, phone from contact where company_id = $1 order by business_name", company.Id) +func mustCollectContactEntries(ctx context.Context, conn *Conn, company *Company, tag string) []*ContactEntry { + rows, err := conn.Query(ctx, ` + select slug + , business_name + , email + , phone + , array_agg(coalesce(tag.name::text, '')) + from contact + left join contact_tag using (contact_id) + left join tag using(tag_id) + where contact.company_id = $1 and (($2 = '') or (tag.name = $2)) + group by slug + , business_name + , email + , phone + order by business_name + `, company.Id, tag) if err != nil { panic(err) } @@ -133,7 +150,7 @@ func mustGetContactEntries(ctx context.Context, conn *Conn, company *Company) [] var entries []*ContactEntry for rows.Next() { entry := &ContactEntry{} - err = rows.Scan(&entry.Slug, &entry.Name, &entry.Email, &entry.Phone) + err = rows.Scan(&entry.Slug, &entry.Name, &entry.Email, &entry.Phone, &entry.Tags) if err != nil { panic(err) } @@ -159,6 +176,7 @@ type contactForm struct { Province *InputField PostalCode *InputField Country *SelectField + Tags *TagsField } func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactForm { @@ -250,6 +268,10 @@ func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactFor `autocomplete="country"`, }, }, + Tags: &TagsField{ + Name: "tags", + Label: pgettext("input", "Tags", locale), + }, } } @@ -268,6 +290,7 @@ func (form *contactForm) Parse(r *http.Request) error { form.Province.FillValue(r) form.PostalCode.FillValue(r) form.Country.FillValue(r) + form.Tags.FillValue(r) return nil } @@ -300,3 +323,47 @@ func (form *contactForm) Validate(ctx context.Context, conn *Conn) bool { } return validator.AllOK() } + +func (form *contactForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool { + return !notFoundErrorOrPanic(conn.QueryRow(ctx, ` + select business_name + , substr(vatin::text, 3) + , trade_name + , phone + , email + , web + , address + , city + , province + , postal_code + , country_code + , string_agg(tag.name, ',') + from contact + left join contact_tag using (contact_id) + left join tag using(tag_id) + where slug = $1 + group by business_name + , substr(vatin::text, 3) + , trade_name + , phone + , email + , web + , address + , city + , province + , postal_code + , country_code + `, slug).Scan( + form.BusinessName, + form.VATIN, + form.TradeName, + form.Phone, + form.Email, + form.Web, + form.Address, + form.City, + form.Province, + form.PostalCode, + form.Country, + form.Tags)) +} diff --git a/pkg/form.go b/pkg/form.go index bd96807..6f74a86 100644 --- a/pkg/form.go +++ b/pkg/form.go @@ -16,6 +16,8 @@ import ( "time" ) +var tagsRegex = regexp.MustCompile("[^a-z0-9-]+") + type Attribute struct { Key, Val string } @@ -209,6 +211,49 @@ func mustGetCountryOptions(ctx context.Context, conn *Conn, locale *Locale) []*S return MustGetOptions(ctx, conn, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", locale.Language) } +type TagsField struct { + Name string + Label string + Tags []string + Attributes []template.HTMLAttr + Required bool + Errors []error +} + +func (field *TagsField) Value() (driver.Value, error) { + if field.Tags == nil { + return []string{}, nil + } + return field.Tags, nil +} + +func (field *TagsField) Scan(value interface{}) error { + if value == nil { + return nil + } + if str, ok := value.(string); ok { + if array, err := pgtype.ParseUntypedTextArray(str); err == nil { + for _, element := range array.Elements { + field.Tags = append(field.Tags, element) + } + return nil + } + } + field.Tags = append(field.Tags, fmt.Sprintf("%v", value)) + return nil +} + +func (field *TagsField) FillValue(r *http.Request) { + field.Tags = strings.Split(tagsRegex.ReplaceAllString(r.FormValue(field.Name), ","), ",") + if len(field.Tags) == 1 && len(field.Tags[0]) == 0 { + field.Tags = []string{} + } +} + +func (field *TagsField) String() string { + return strings.Join(field.Tags, ",") +} + type FormValidator struct { Valid bool } diff --git a/pkg/invoices.go b/pkg/invoices.go index 5beac9e..61668a9 100644 --- a/pkg/invoices.go +++ b/pkg/invoices.go @@ -14,7 +14,6 @@ import ( "net/http" "os" "os/exec" - "regexp" "sort" "strconv" "strings" @@ -365,7 +364,7 @@ func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Param mustRenderNewInvoiceForm(w, r, form) return } - slug := conn.MustGetText(r.Context(), "", "select add_invoice($1, $2, $3, $4, $5, $6, $7, $8)", company.Id, form.Number, form.Date, form.Customer, form.Notes, form.PaymentMethod, form.SplitTags(), NewInvoiceProductArray(form.Products)) + slug := conn.MustGetText(r.Context(), "", "select add_invoice($1, $2, $3, $4, $5, $6, $7, $8)", company.Id, form.Number, form.Date, form.Customer, form.Notes, form.PaymentMethod, form.Tags, NewInvoiceProductArray(form.Products)) http.Redirect(w, r, companyURI(company, "/invoices/"+slug), http.StatusSeeOther) } @@ -438,7 +437,7 @@ type invoiceForm struct { Date *InputField Notes *InputField PaymentMethod *SelectField - Tags *InputField + Tags *TagsField Products []*invoiceProductForm } @@ -475,10 +474,9 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co Label: pgettext("input", "Notes", locale), Type: "textarea", }, - Tags: &InputField{ + Tags: &TagsField{ Name: "tags", Label: pgettext("input", "Tags", locale), - Type: "text", }, PaymentMethod: &SelectField{ Name: "payment_method", @@ -607,15 +605,6 @@ func mustGetTaxOptions(ctx context.Context, conn *Conn, company *Company) []*Sel return MustGetGroupedOptions(ctx, conn, "select tax_id::text, tax.name, tax_class.name from tax join tax_class using (tax_class_id) where tax.company_id = $1 order by tax_class.name, tax.name", company.Id) } -func (form *invoiceForm) SplitTags() []string { - reg := regexp.MustCompile("[^a-z0-9-]+") - tags := strings.Split(reg.ReplaceAllString(form.Tags.Val, ","), ",") - if len(tags) == 1 && len(tags[0]) == 0 { - return []string{} - } - return tags -} - type invoiceProductForm struct { locale *Locale company *Company @@ -759,7 +748,7 @@ func HandleUpdateInvoice(w http.ResponseWriter, r *http.Request, params httprout mustRenderEditInvoiceForm(w, r, slug, form) return } - slug = conn.MustGetText(r.Context(), "", "select edit_invoice($1, $2, $3, $4, $5, $6, $7)", slug, form.InvoiceStatus, form.Customer, form.Notes, form.PaymentMethod, form.SplitTags(), EditedInvoiceProductArray(form.Products)) + slug = conn.MustGetText(r.Context(), "", "select edit_invoice($1, $2, $3, $4, $5, $6, $7)", slug, form.InvoiceStatus, form.Customer, form.Notes, form.PaymentMethod, form.Tags, EditedInvoiceProductArray(form.Products)) if slug == "" { http.NotFound(w, r) return diff --git a/revert/add_contact.sql b/revert/add_contact.sql new file mode 100644 index 0000000..9437949 --- /dev/null +++ b/revert/add_contact.sql @@ -0,0 +1,7 @@ +-- Revert numerus:add_contact from pg + +begin; + +drop function if exists numerus.add_contact(integer, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[]); + +commit; diff --git a/revert/contact_tag.sql b/revert/contact_tag.sql new file mode 100644 index 0000000..df36364 --- /dev/null +++ b/revert/contact_tag.sql @@ -0,0 +1,7 @@ +-- Revert numerus:contact_tag from pg + +begin; + +drop table if exists numerus.contact_tag; + +commit; diff --git a/revert/edit_contact.sql b/revert/edit_contact.sql new file mode 100644 index 0000000..e17b325 --- /dev/null +++ b/revert/edit_contact.sql @@ -0,0 +1,7 @@ +-- Revert numerus:edit_contact from pg + +begin; + +drop function if exists numerus.edit_contact(uuid, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[]); + +commit; diff --git a/revert/tag_contact.sql b/revert/tag_contact.sql new file mode 100644 index 0000000..5ace225 --- /dev/null +++ b/revert/tag_contact.sql @@ -0,0 +1,7 @@ +-- Revert numerus:tag_contact from pg + +begin; + +drop function if exists numerus.tag_contact(integer, integer, numerus.tag_name[]); + +commit; diff --git a/sqitch.plan b/sqitch.plan index 6ba9367..aba6c61 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -41,7 +41,7 @@ company_default_payment_method [schema_numerus company payment_method] 2023-03-0 tax_class [schema_numerus company] 2023-02-28T10:13:14Z jordi fita mas # Add the relation for tax classes tax_rate [schema_numerus] 2023-01-28T11:33:39Z jordi fita mas # Add domain for tax rates tax [schema_numerus company tax_rate tax_class] 2023-01-28T11:45:47Z jordi fita mas # 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 # Add the relation for contacts +contact [schema_numerus company extension_vat email extension_pg_libphonenumber extension_uri country_code country] 2023-01-29T12:59:18Z jordi fita mas # Add the relation for contacts product [schema_numerus company tax] 2023-02-04T09:17:24Z jordi fita mas # Add relation for products parse_price [schema_public] 2023-02-05T11:04:54Z jordi fita mas # Add function to convert from price to cents to_price [schema_numerus] 2023-02-05T11:46:31Z jordi fita mas # Add function to format cents to prices @@ -71,3 +71,7 @@ new_invoice_amount [schema_numerus] 2023-02-23T12:08:25Z jordi fita mas # Add function to compute the subtotal, taxes, and total amounts for a new invoice edited_invoice_product [schema_numerus discount_rate] 2023-03-11T19:22:24Z jordi fita mas # Add typo for passing products to edited invoices edit_invoice [schema_numerus invoice currency parse_price edited_invoice_product tax invoice_product invoice_product_tax tag_name tag_invoice] 2023-03-11T18:30:50Z jordi fita mas # Add function to edit invoices +contact_tag [schema_numerus tag contact] 2023-03-24T22:20:51Z jordi fita mas # Add relation for contact tag +tag_contact [schema_numerus tag_name tag_relation contact_tag] 2023-03-25T22:16:42Z jordi fita mas # Add function to tag contacts +add_contact [schema_numerus extension_vat email extension_pg_libphonenumber extension_uri country_code tag_name contact] 2023-03-25T22:32:37Z jordi fita mas # Add function to create new contacts +edit_contact [schema_numerus email extension_uri country_code tag_name contact tag_contact extension_vat extension_pg_libphonenumber] 2023-03-25T23:20:27Z jordi fita mas # Add function to edit contacts diff --git a/test/add_contact.sql b/test/add_contact.sql new file mode 100644 index 0000000..b75b6e6 --- /dev/null +++ b/test/add_contact.sql @@ -0,0 +1,96 @@ +-- Test add_contact +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(16); + +set search_path to auth, numerus, public; + +select has_function('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]']); +select function_lang_is('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'plpgsql'); +select function_returns('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'uuid'); +select isnt_definer('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]']); +select volatility_is('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'volatile'); +select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'guest', array []::text[]); +select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate contact_tag cascade; +truncate tag cascade; +truncate contact 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) + , (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 222) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (111, 1, 'cash', 'cash') + , (222, 2, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +select lives_ok( + $$ select add_contact(1, 'Contact 2.1', '40404040D', 'Trade Contact 2.1', '777-777-777', 'c@c', 'https://c', 'Fake St., 123', 'City 2.1', 'Province 2.1', '17486', 'ES', '{tag1,tag2}') $$, + 'Should be able to insert a contact for the first company with two tags' +); + +select lives_ok( + $$ select add_contact(1, 'Contact 2.2', '41414141L', 'Trade Contact 2.2', '888-888-888', 'd@d', 'https://d', 'Another Fake St., 123', 'City 2.2', 'Province 2.2', '17487', 'ES', '{}') $$, + 'Should be able to insert a second contact for the first company with no tag' +); + +select lives_ok( + $$ select add_contact(2, 'Contact 4.1', '42424242Y', '', '999-999-999', 'e@e', '', 'Yet Another Fake St., 123', 'City 4.1', 'Province 4.1', '17488', 'ES', '{tag2}') $$, + 'Should be able to insert a contact for the second company with a tag' +); + +select lives_ok( + $$ select add_contact(1, 'Contact 2.3', '43434343Q', '', '000-000-000', 'f@f', '', 'The Last Fake St., 123', '', '', '', 'ES', '{tag2}') $$, + 'Should be able to insert another contact with a repeated tag' +); + +select bag_eq( + $$ select company_id, business_name, vatin::text, trade_name, phone::text, email::text, web::text, address, city, province, postal_code, country_code::text, created_at from contact $$, + $$ values (1, 'Contact 2.1', 'ES40404040D', 'Trade Contact 2.1', '+34 777 77 77 77', 'c@c', 'https://c', 'Fake St., 123', 'City 2.1', 'Province 2.1', '17486', 'ES', CURRENT_TIMESTAMP) + , (1, 'Contact 2.2', 'ES41414141L', 'Trade Contact 2.2', '+34 888 88 88 88', 'd@d', 'https://d', 'Another Fake St., 123', 'City 2.2', 'Province 2.2', '17487', 'ES', CURRENT_TIMESTAMP) + , (2, 'Contact 4.1', 'ES42424242Y', '', '+34 999 99 99 99', 'e@e', '', 'Yet Another Fake St., 123', 'City 4.1', 'Province 4.1', '17488', 'ES', CURRENT_TIMESTAMP) + , (1, 'Contact 2.3', 'ES43434343Q', '', '+34 000000000', 'f@f', '', 'The Last Fake St., 123', '', '', '', 'ES', CURRENT_TIMESTAMP) + $$, + 'Should have created all contacts' +); + +select bag_eq( + $$ select company_id, name from tag $$, + $$ values (1, 'tag1') + , (1, 'tag2') + , (2, 'tag2') + $$, + 'Should have added all new tags once' +); + +select bag_eq( + $$ select business_name, tag.name from contact_tag join contact using (contact_id) join tag using (tag_id) $$, + $$ values ('Contact 2.1', 'tag1') + , ('Contact 2.1', 'tag2') + , ('Contact 4.1', 'tag2') + , ('Contact 2.3', 'tag2') + $$, + 'Should have assigned the tags to contacts' +); +select * +from finish(); + +rollback; diff --git a/test/contact_tag.sql b/test/contact_tag.sql new file mode 100644 index 0000000..fc03584 --- /dev/null +++ b/test/contact_tag.sql @@ -0,0 +1,130 @@ +-- Test contact_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('contact_tag'); +select has_pk('contact_tag' ); +select col_is_pk('contact_tag', array['contact_id', 'tag_id']); +select table_privs_are('contact_tag', 'guest', array []::text[]); +select table_privs_are('contact_tag', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('contact_tag', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('contact_tag', 'authenticator', array []::text[]); + +select has_column('contact_tag', 'contact_id'); +select col_is_fk('contact_tag', 'contact_id'); +select fk_ok('contact_tag', 'contact_id', 'contact', 'contact_id'); +select col_type_is('contact_tag', 'contact_id', 'integer'); +select col_not_null('contact_tag', 'contact_id'); +select col_hasnt_default('contact_tag', 'contact_id'); + +select has_column('contact_tag', 'tag_id'); +select col_is_fk('contact_tag', 'tag_id'); +select fk_ok('contact_tag', 'tag_id', 'tag', 'tag_id'); +select col_type_is('contact_tag', 'tag_id', 'integer'); +select col_not_null('contact_tag', 'tag_id'); +select col_hasnt_default('contact_tag', 'tag_id'); + + +set client_min_messages to warning; +truncate contact_tag cascade; +truncate contact cascade; +truncate tag cascade; +truncate contact 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 contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) +values (6, 2, 'Contact 1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') + , (8, 4, 'Contact 2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES') +; + +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 contact_tag (contact_id, tag_id) +values (6, 14) + , (6, 15) + , (8, 18) +; + +prepare contact_tag_data as +select contact_id, tag_id +from contact_tag +; + +set role invoicer; +select is_empty('contact_tag_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'contact_tag_data', + $$ values ( 6, 14 ) + , ( 6, 15 ) + $$, + 'Should only list contact tags of the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'contact_tag_data', + $$ values ( 8, 18 ) + $$, + 'Should only list contact tags of the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'contact_tag_data', + '42501', 'permission denied for table contact_tag', + 'Should not allow select to guest users' +); + + +select * +from finish(); + +rollback; + diff --git a/test/edit_contact.sql b/test/edit_contact.sql new file mode 100644 index 0000000..c13156d --- /dev/null +++ b/test/edit_contact.sql @@ -0,0 +1,101 @@ +-- Test edit_contact +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(14); + +set search_path to auth, numerus, public; + +select has_function('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]']); +select function_lang_is('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'plpgsql'); +select function_returns('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'uuid'); +select isnt_definer('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]']); +select volatility_is('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'volatile'); +select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'guest', array []::text[]); +select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'email', 'uri', 'text', 'text', 'text', 'text', 'country_code', 'tag_name[]'], 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate contact_tag cascade; +truncate tag cascade; +truncate contact 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') +; +-- edit_contact uses the sequence and sometimes it would confict +alter sequence tag_tag_id_seq restart with 15; + +insert into contact (contact_id, company_id, slug, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) +values (12, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Contact 1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') + , (13, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'Contact 2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES') +; + +insert into contact_tag (contact_id, tag_id) +values (12, 10) + , (13, 11) +; + +select lives_ok( + $$ select edit_contact('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Contact 2.1', '40404040D', 'Trade Contact 2.1', '999-999-999', 'c1@c1', 'https://c', 'Fake St., 123', 'City 2.1', 'Province 2.1', '19486', 'ES', array['tag1']) $$, + 'Should be able to edit the first contact' +); + +select lives_ok( + $$ select edit_contact('b57b980b-247b-4be4-a0b7-03a7819c53ae', 'Contact 2.2', '41414141L', 'Trade Contact 2.2', '111-111-111', 'd2@d2', 'https://d', 'Another Fake St., 123', 'City 2.2', 'Province 2.2', '17417', 'ES', array['tag1', 'tag3']) $$, + 'Should be able to edit the second contact' +); + +select bag_eq( + $$ select company_id, business_name, vatin::text, trade_name, phone::text, email::text, web::text, address, city, province, postal_code, country_code::text, created_at from contact $$, + $$ values (1, 'Contact 2.1', 'ES40404040D', 'Trade Contact 2.1', '+34 999 99 99 99', 'c1@c1', 'https://c', 'Fake St., 123', 'City 2.1', 'Province 2.1', '19486', 'ES', CURRENT_TIMESTAMP) + , (1, 'Contact 2.2', 'ES41414141L', 'Trade Contact 2.2', '+34 111111111', 'd2@d2', 'https://d', 'Another Fake St., 123', 'City 2.2', 'Province 2.2', '17417', 'ES', CURRENT_TIMESTAMP) + $$, + 'Should have updated all contacts' +); + +select bag_eq( + $$ select company_id, name from tag $$, + $$ values (1, 'tag1') + , (1, 'tag2') + , (1, 'tag3') + $$, + 'Should have added all new tags' +); + +select bag_eq( + $$ select business_name, tag.name from contact_tag join contact using (contact_id) join tag using (tag_id) $$, + $$ values ('Contact 2.1', 'tag1') + , ('Contact 2.2', 'tag1') + , ('Contact 2.2', 'tag3') + $$, + 'Should have assigned the tags to contacts' +); + +select * +from finish(); + +rollback; diff --git a/test/tag_contact.sql b/test/tag_contact.sql new file mode 100644 index 0000000..e4f1761 --- /dev/null +++ b/test/tag_contact.sql @@ -0,0 +1,124 @@ +-- Test tag_contact +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_contact', array ['integer', 'integer', 'tag_name[]']); +select function_lang_is('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]'], 'sql'); +select function_returns('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]'], 'void'); +select isnt_definer('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]']); +select volatility_is('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]'], 'volatile'); +select function_privs_are('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]'], 'guest', array []::text[]); +select function_privs_are('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'tag_contact', array ['integer', 'integer', 'tag_name[]'], 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate contact_tag cascade; +truncate tag cascade; +truncate contact 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_contact uses the sequence and sometimes it would confict +alter sequence tag_tag_id_seq restart with 15; + +insert into contact (contact_id, company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code) +values (12, 1, 'Contact 2.1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES') + , (13, 1, 'Contact 2.2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES') +; + +insert into contact_tag (contact_id, tag_id) +values (12, 10) + , (13, 11) +; + +prepare current_tags as +select contact_id, tag.name +from contact +join contact_tag using (contact_id) +join tag using (tag_id); + +select lives_ok( + $$ select tag_contact(1, 12, array['tag1']) $$, + 'Should be able to keep the same tags to the contact' +); + +select bag_eq( + 'current_tags', + $$ values (12, 'tag1') + , (13, 'tag2') + $$, + 'Should not have changed any contact tag' +); + +select lives_ok( + $$ select tag_contact(1, 12, array['tag1', 'tag2']) $$, + 'Should be able to add tag2 contact' +); + +select bag_eq( + 'current_tags', + $$ values (12, 'tag1') + , (12, 'tag2') + , (13, 'tag2') + $$, + 'Should have added tag2 to contact' +); + +select lives_ok( + $$ select tag_contact(1, 13, array['tag3']) $$, + 'Should be able to replace all tags of an contact with a new one' +); + +select bag_eq( + 'current_tags', + $$ values (12, 'tag1') + , (12, 'tag2') + , (13, 'tag3') + $$, + 'Should have set tag3 to contact' +); + +select lives_ok( + $$ select tag_contact(1, 12, array[]::tag_name[]) $$, + 'Should be able to remove all tags from an contact' +); + +select bag_eq( + 'current_tags', + $$ values (13, 'tag3') + $$, + 'Should have remove all tags from contact' +); + + +select * +from finish(); + +rollback; diff --git a/verify/add_contact.sql b/verify/add_contact.sql new file mode 100644 index 0000000..2e71a67 --- /dev/null +++ b/verify/add_contact.sql @@ -0,0 +1,7 @@ +-- Verify numerus:add_contact on pg + +begin; + +select has_function_privilege('numerus.add_contact(integer, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[])', 'execute'); + +rollback; diff --git a/verify/contact_tag.sql b/verify/contact_tag.sql new file mode 100644 index 0000000..34883a1 --- /dev/null +++ b/verify/contact_tag.sql @@ -0,0 +1,13 @@ +-- Verify numerus:contact_tag on pg + +begin; + +select contact_id + , tag_id +from numerus.contact_tag +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.contact_tag'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.contact_tag'::regclass; + +rollback; diff --git a/verify/edit_contact.sql b/verify/edit_contact.sql new file mode 100644 index 0000000..e145ad3 --- /dev/null +++ b/verify/edit_contact.sql @@ -0,0 +1,7 @@ +-- Verify numerus:edit_contact on pg + +begin; + +select has_function_privilege('numerus.edit_contact(uuid, text, text, text, text, numerus.email, uri, text, text, text, text, numerus.country_code, numerus.tag_name[])', 'execute'); + +rollback; diff --git a/verify/tag_contact.sql b/verify/tag_contact.sql new file mode 100644 index 0000000..b3f8bdf --- /dev/null +++ b/verify/tag_contact.sql @@ -0,0 +1,7 @@ +-- Verify numerus:tag_contact on pg + +begin; + +select has_function_privilege('numerus.tag_contact(integer, integer, numerus.tag_name[])', 'execute'); + +rollback; diff --git a/web/template/contacts/edit.gohtml b/web/template/contacts/edit.gohtml index 3d24795..9d4b5fd 100644 --- a/web/template/contacts/edit.gohtml +++ b/web/template/contacts/edit.gohtml @@ -34,6 +34,7 @@ {{ template "input-field" .Province }} {{ template "input-field" .PostalCode }} {{ template "select-field" .Country | addSelectAttr `class="width-fixed"` }} + {{ template "tags-field" .Tags }} {{ end }}
diff --git a/web/template/contacts/index.gohtml b/web/template/contacts/index.gohtml index baee32c..b336ede 100644 --- a/web/template/contacts/index.gohtml +++ b/web/template/contacts/index.gohtml @@ -26,6 +26,7 @@ {{( pgettext "Customer" "title" )}} {{( pgettext "Email" "title" )}} {{( pgettext "Phone" "title" )}} + {{( pgettext "Tags" "title" )}} @@ -36,11 +37,17 @@ {{ .Name }} {{ .Email }} {{ .Phone }} + + {{- range $index, $tag := .Tags }} + {{- if gt $index 0 }}, {{ end -}} + {{ . }} + {{- end }} + {{- end }} {{ else }} - {{( gettext "No contacts added yet." )}} + {{( gettext "No contacts added yet." )}} {{ end }} diff --git a/web/template/contacts/new.gohtml b/web/template/contacts/new.gohtml index 4b974a6..76f1010 100644 --- a/web/template/contacts/new.gohtml +++ b/web/template/contacts/new.gohtml @@ -30,6 +30,7 @@ {{ template "input-field" .Province }} {{ template "input-field" .PostalCode }} {{ template "select-field" .Country | addSelectAttr `class="width-fixed"` }} + {{ template "tags-field" .Tags }}
diff --git a/web/template/form.gohtml b/web/template/form.gohtml index 6cb34f2..dc11a28 100644 --- a/web/template/form.gohtml +++ b/web/template/form.gohtml @@ -34,7 +34,7 @@
+ {{ if .Required }}required="required"{{ end }} value="{{ .String }}" placeholder="{{ .Label }}"> {{- if .Errors }}