Add tags for contacts too
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.
This commit is contained in:
parent
6b73acafe6
commit
4131602fa3
|
@ -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})"}');
|
||||
|
|
|
@ -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;
|
|
@ -5,8 +5,6 @@
|
|||
-- requires: email
|
||||
-- requires: extension_pg_libphonenumber
|
||||
-- requires: extension_uri
|
||||
-- requires: currency_code
|
||||
-- requires: currency
|
||||
-- requires: country_code
|
||||
-- requires: country
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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))
|
||||
}
|
||||
|
|
45
pkg/form.go
45
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:contact_tag from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop table if exists numerus.contact_tag;
|
||||
|
||||
commit;
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:tag_contact from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists numerus.tag_contact(integer, integer, numerus.tag_name[]);
|
||||
|
||||
commit;
|
|
@ -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 <jordi@tandem.blog> # Add the relation for tax classes
|
||||
tax_rate [schema_numerus] 2023-01-28T11:33:39Z jordi fita mas <jordi@tandem.blog> # Add domain for tax rates
|
||||
tax [schema_numerus company tax_rate tax_class] 2023-01-28T11:45:47Z jordi fita mas <jordi@tandem.blog> # Add relation for taxes
|
||||
contact [schema_numerus company extension_vat email extension_pg_libphonenumber extension_uri currency_code currency country_code country] 2023-01-29T12:59:18Z jordi fita mas <jordi@tandem.blog> # Add the relation for contacts
|
||||
contact [schema_numerus company extension_vat email extension_pg_libphonenumber extension_uri country_code country] 2023-01-29T12:59:18Z jordi fita mas <jordi@tandem.blog> # Add the relation for contacts
|
||||
product [schema_numerus company tax] 2023-02-04T09:17:24Z jordi fita mas <jordi@tandem.blog> # Add relation for products
|
||||
parse_price [schema_public] 2023-02-05T11:04:54Z jordi fita mas <jordi@tandem.blog> # Add function to convert from price to cents
|
||||
to_price [schema_numerus] 2023-02-05T11:46:31Z jordi fita mas <jordi@tandem.blog> # Add function to format cents to prices
|
||||
|
@ -71,3 +71,7 @@ new_invoice_amount [schema_numerus] 2023-02-23T12:08:25Z jordi fita mas <jordi@t
|
|||
compute_new_invoice_amount [schema_numerus company currency tax new_invoice_product new_invoice_amount] 2023-02-23T12:20:13Z jordi fita mas <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add function to edit invoices
|
||||
contact_tag [schema_numerus tag contact] 2023-03-24T22:20:51Z jordi fita mas <jordi@tandem.blog> # Add relation for contact tag
|
||||
tag_contact [schema_numerus tag_name tag_relation contact_tag] 2023-03-25T22:16:42Z jordi fita mas <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add function to edit contacts
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 }}
|
||||
|
||||
<fieldset>
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
<th>{{( pgettext "Customer" "title" )}}</th>
|
||||
<th>{{( pgettext "Email" "title" )}}</th>
|
||||
<th>{{( pgettext "Phone" "title" )}}</th>
|
||||
<th>{{( pgettext "Tags" "title" )}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-hx-push-url="false" data-hx-swap="beforeend">
|
||||
|
@ -36,11 +37,17 @@
|
|||
<td><a href="{{ companyURI "/contacts/"}}{{ .Slug }}" data-hx-boost="true">{{ .Name }}</a></td>
|
||||
<td><a href="mailto:{{ .Email }}">{{ .Email }}</a></td>
|
||||
<td><a href="tel:{{ .Phone }}">{{ .Phone }}</a></td>
|
||||
<td>
|
||||
{{- range $index, $tag := .Tags }}
|
||||
{{- if gt $index 0 }}, {{ end -}}
|
||||
<a href="?tag={{ . }}">{{ . }}</a>
|
||||
{{- end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
{{ else }}
|
||||
<tr>
|
||||
<td colspan="4">{{( gettext "No contacts added yet." )}}</td>
|
||||
<td colspan="5">{{( gettext "No contacts added yet." )}}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
{{ template "input-field" .Province }}
|
||||
{{ template "input-field" .PostalCode }}
|
||||
{{ template "select-field" .Country | addSelectAttr `class="width-fixed"` }}
|
||||
{{ template "tags-field" .Tags }}
|
||||
|
||||
<fieldset>
|
||||
<button class="primary" type="submit">{{( pgettext "New contact" "action" )}}</button>
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
<div class="input {{ if .Errors }}has-errors{{ end }}" is="numerus-tags">
|
||||
<input type="text" name="{{ .Name }}" id="{{ .Name }}-field"
|
||||
{{- range $attribute := .Attributes }} {{$attribute}} {{ end }}
|
||||
{{ if .Required }}required="required"{{ end }} value="{{ .Val }}" placeholder="{{ .Label }}">
|
||||
{{ if .Required }}required="required"{{ end }} value="{{ .String }}" placeholder="{{ .Label }}">
|
||||
<label for="{{ .Name }}-field">{{ .Label }}</label>
|
||||
{{- if .Errors }}
|
||||
<ul>
|
||||
|
|
Loading…
Reference in New Issue