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 }}