Add the company relation and read-only form to edit
I do not have more time to update the update to the company today, but i believe this is already a good amount of work for a commit. The company is going to be used for row level security, as users will only have access to the data from companies they are granted access, by virtue of being in the company_user relation. I did not know how add a row level security policy to the company_user because i needed the to select on the same relation and this is not allowed, because it would create an infinite loop. Had to add the vat, pg_libphonenumber, and uri extensions in order to validate VAT identification numbers, phone numbers, and URIs, repectively. These libraries are not in Debian, but i created packages for them all in https://dev.tandem.ws/tandem.
This commit is contained in:
parent
c037f671f8
commit
627841d4dd
|
@ -2,9 +2,17 @@ begin;
|
|||
|
||||
set search_path to auth, numerus, public;
|
||||
|
||||
insert into auth."user" (email, name, password, role)
|
||||
values ('demo@numerus', 'Demo User', 'demo', 'invoicer')
|
||||
, ('admin@numerus', 'Demo Admin', 'admin', 'admin')
|
||||
insert into auth."user" (user_id, email, name, password, role)
|
||||
values (1, 'demo@numerus', 'Demo User', 'demo', 'invoicer')
|
||||
, (2, 'admin@numerus', 'Demo Admin', 'admin', 'admin')
|
||||
;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country, currency_code)
|
||||
values (1, 'Juli Verd', 'ES40404040D', 'Pesebre', parse_packed_phone_number('972 50 60 70', 'ES'), 'info@numerus.cat', 'https://numerus.cat/', 'C/ de l’Hort', 'Castelló d’Empúries', 'Alt Empordà', '17486', 'Espanya', 'EUR');
|
||||
|
||||
insert into company_user (company_id, user_id)
|
||||
values (1, 1)
|
||||
, (1, 2)
|
||||
;
|
||||
|
||||
commit;
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
-- Deploy numerus:available_currencies to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: currency
|
||||
|
||||
begin;
|
||||
|
||||
insert into numerus.currency(currency_code, currency_symbol)
|
||||
values ('EUR', '€')
|
||||
, ('USD', '$')
|
||||
;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,36 @@
|
|||
-- Deploy numerus:company to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: extension_vat
|
||||
-- requires: email
|
||||
-- requires: extension_pg_libphonenumber
|
||||
-- requires: extension_uri
|
||||
-- requires: currency_code
|
||||
-- requires: currency
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus,public;
|
||||
|
||||
create table company (
|
||||
company_id serial primary key,
|
||||
slug uuid not null unique default gen_random_uuid(),
|
||||
business_name text not null,
|
||||
vatin vatin not null,
|
||||
trade_name text not null,
|
||||
phone packed_phone_number not null,
|
||||
email email not null,
|
||||
web uri not null,
|
||||
address text not null,
|
||||
city text not null,
|
||||
province text not null,
|
||||
postal_code text not null,
|
||||
country text not null,
|
||||
currency_code currency_code not null references currency,
|
||||
created_at timestamptz not null default current_timestamp
|
||||
);
|
||||
|
||||
grant select on table company to invoicer;
|
||||
grant select on table company to admin;
|
||||
|
||||
|
||||
commit;
|
|
@ -0,0 +1,38 @@
|
|||
-- Deploy numerus:company_user to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: user
|
||||
-- requires: company
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, auth, public;
|
||||
|
||||
create table company_user (
|
||||
company_id integer not null references company,
|
||||
user_id integer not null references "user",
|
||||
primary key (company_id, user_id)
|
||||
);
|
||||
|
||||
grant select on table company_user to invoicer;
|
||||
grant select on table company_user to admin;
|
||||
|
||||
|
||||
alter table company enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on company
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from company_user
|
||||
join user_profile using (user_id)
|
||||
where company_user.company_id = company.company_id
|
||||
)
|
||||
);
|
||||
|
||||
-- TODO:
|
||||
-- I think we can not do the same for company_user because it would be
|
||||
-- an infinite loop, but in this case i think it is fine because we can
|
||||
-- only see ids, nothing more.
|
||||
|
||||
commit;
|
|
@ -0,0 +1,18 @@
|
|||
-- Deploy numerus:currency to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: currency_code
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create table currency (
|
||||
currency_code currency_code not null primary key,
|
||||
currency_symbol text not null,
|
||||
decimal_digits integer not null default 2
|
||||
);
|
||||
|
||||
grant select on table currency to invoicer;
|
||||
grant select on table currency to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,14 @@
|
|||
-- Deploy numerus:currency_code to pg
|
||||
-- requires: schema_numerus
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create domain currency_code as text
|
||||
check (value ~ '^[A-Z]{3}$');
|
||||
|
||||
comment on domain currency_code is
|
||||
'A correctly formated, but not necessarily valid, ISO 4217 currency code';
|
||||
|
||||
commit;
|
|
@ -0,0 +1,8 @@
|
|||
-- Deploy numerus:extension_pg_libphonenumber to pg
|
||||
-- requires: schema_public
|
||||
|
||||
begin;
|
||||
|
||||
create extension if not exists pg_libphonenumber;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,8 @@
|
|||
-- Deploy numerus:extension_uri to pg
|
||||
-- requires: schema_public
|
||||
|
||||
begin;
|
||||
|
||||
create extension if not exists uri;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,8 @@
|
|||
-- Deploy numerus:extension_vat to pg
|
||||
-- requires: schema_public
|
||||
|
||||
begin;
|
||||
|
||||
create extension if not exists vat;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,105 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
ContextCompanyKey = "numerus-company"
|
||||
)
|
||||
|
||||
type Company struct {
|
||||
Id int
|
||||
Slug string
|
||||
}
|
||||
|
||||
func CompanyHandler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
slug := r.URL.Path
|
||||
if idx := strings.IndexByte(slug, '/'); idx >= 0 {
|
||||
slug = slug[:idx]
|
||||
}
|
||||
|
||||
conn := getConn(r)
|
||||
company := &Company{
|
||||
Slug: slug,
|
||||
}
|
||||
err := conn.QueryRow(r.Context(), "select company_id from company where slug = $1", slug).Scan(&company.Id)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), ContextCompanyKey, company)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
// Same as StripPrefix
|
||||
p := strings.TrimPrefix(r.URL.Path, slug)
|
||||
rp := strings.TrimPrefix(r.URL.RawPath, slug)
|
||||
if len(p) < len(r.URL.Path) && (r.URL.RawPath == "" || len(rp) < len(r.URL.RawPath)) {
|
||||
r2 := new(http.Request)
|
||||
*r2 = *r
|
||||
r2.URL = new(url.URL)
|
||||
*r2.URL = *r.URL
|
||||
if p == "" {
|
||||
r2.URL.Path = "/"
|
||||
} else {
|
||||
r2.URL.Path = p
|
||||
}
|
||||
r2.URL.RawPath = rp
|
||||
next.ServeHTTP(w, r2)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func getCompany(r *http.Request) *Company {
|
||||
company := r.Context().Value(ContextCompanyKey)
|
||||
if company == nil {
|
||||
return nil
|
||||
}
|
||||
return company.(*Company)
|
||||
}
|
||||
|
||||
type TaxDetailsPage struct {
|
||||
Title string
|
||||
BusinessName string
|
||||
VATIN string
|
||||
TradeName string
|
||||
Phone string
|
||||
Email string
|
||||
Web string
|
||||
Address string
|
||||
City string
|
||||
Province string
|
||||
PostalCode string
|
||||
Country string
|
||||
}
|
||||
|
||||
func CompanyTaxDetailsHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
locale := getLocale(r)
|
||||
page := &TaxDetailsPage{
|
||||
Title: pgettext("title", "Tax Details", locale),
|
||||
}
|
||||
company := mustGetCompany(r)
|
||||
conn := getConn(r)
|
||||
err := conn.QueryRow(r.Context(), "select business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country from company where company_id = $1", company.Id).Scan(&page.BusinessName, &page.VATIN, &page.TradeName, &page.Phone, &page.Email, &page.Web, &page.Address, &page.City, &page.Province, &page.PostalCode, &page.Country);
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
mustRenderAppTemplate(w, r, "tax-details.html", page)
|
||||
});
|
||||
}
|
||||
|
||||
func mustGetCompany(r *http.Request) *Company {
|
||||
company := getCompany(r)
|
||||
if company == nil {
|
||||
panic(errors.New("company: required but not found"))
|
||||
}
|
||||
return company;
|
||||
}
|
|
@ -5,15 +5,28 @@ import (
|
|||
)
|
||||
|
||||
func NewRouter(db *Db) http.Handler {
|
||||
companyRouter := http.NewServeMux()
|
||||
companyRouter.Handle("/tax-details", CompanyTaxDetailsHandler())
|
||||
companyRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
mustRenderAppTemplate(w, r, "dashboard.html", nil)
|
||||
})
|
||||
|
||||
router := http.NewServeMux()
|
||||
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
|
||||
router.Handle("/login", LoginHandler())
|
||||
router.Handle("/logout", Authenticated(LogoutHandler()))
|
||||
router.Handle("/profile", Authenticated(ProfileHandler()))
|
||||
router.Handle("/company/", Authenticated(http.StripPrefix("/company/", CompanyHandler(companyRouter))))
|
||||
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
user := getUser(r)
|
||||
if user.LoggedIn {
|
||||
mustRenderAppTemplate(w, r, "dashboard.html", nil)
|
||||
conn := getConn(r)
|
||||
var slug string
|
||||
err := conn.QueryRow(r.Context(), "select slug::text from company order by company_id limit 1").Scan(&slug)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
http.Redirect(w, r, "/company/"+slug, http.StatusFound)
|
||||
} else {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
}
|
||||
|
|
|
@ -12,10 +12,17 @@ func templateFile (name string) string {
|
|||
|
||||
func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename string, data interface{}) {
|
||||
locale := getLocale(r)
|
||||
company := getCompany(r)
|
||||
t := template.New(filename)
|
||||
t.Funcs(template.FuncMap{
|
||||
"gettext": locale.Get,
|
||||
"pgettext": locale.GetC,
|
||||
"companyURI": func(uri string) string {
|
||||
if company == nil {
|
||||
return uri;
|
||||
}
|
||||
return "/company/" + company.Slug + uri;
|
||||
},
|
||||
})
|
||||
if _, err := t.ParseFiles(templateFile(filename), templateFile(layout)); err != nil {
|
||||
panic(err)
|
||||
|
|
86
po/ca.po
86
po/ca.po
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: numerus\n"
|
||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||
"POT-Creation-Date: 2023-01-23 18:50+0100\n"
|
||||
"POT-Creation-Date: 2023-01-24 21:37+0100\n"
|
||||
"PO-Revision-Date: 2023-01-18 17:08+0100\n"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Catalan <ca@dodds.net>\n"
|
||||
|
@ -26,12 +26,13 @@ msgstr "Entrada"
|
|||
msgid "Invalid user or password"
|
||||
msgstr "Nom d’usuari o contrasenya incorrectes"
|
||||
|
||||
#: web/template/login.html:13 web/template/profile.html:14
|
||||
#: web/template/login.html:13 web/template/profile.html:15
|
||||
#: web/template/tax-details.html:23
|
||||
msgctxt "input"
|
||||
msgid "Email"
|
||||
msgstr "Correu-e"
|
||||
|
||||
#: web/template/login.html:18 web/template/profile.html:22
|
||||
#: web/template/login.html:18 web/template/profile.html:23
|
||||
msgctxt "input"
|
||||
msgid "Password"
|
||||
msgstr "Contrasenya"
|
||||
|
@ -41,51 +42,112 @@ msgctxt "action"
|
|||
msgid "Login"
|
||||
msgstr "Entra"
|
||||
|
||||
#: web/template/profile.html:2 pkg/profile.go:33
|
||||
#: web/template/profile.html:3 pkg/profile.go:29
|
||||
msgctxt "title"
|
||||
msgid "User Settings"
|
||||
msgstr "Configuració usuari"
|
||||
|
||||
#: web/template/profile.html:5
|
||||
#: web/template/profile.html:6
|
||||
msgctxt "title"
|
||||
msgid "User Access Data"
|
||||
msgstr "Dades accés usuari"
|
||||
|
||||
#: web/template/profile.html:9
|
||||
#: web/template/profile.html:10
|
||||
msgctxt "input"
|
||||
msgid "User name"
|
||||
msgstr "Nom d’usuari"
|
||||
|
||||
#: web/template/profile.html:18
|
||||
#: web/template/profile.html:19
|
||||
msgctxt "title"
|
||||
msgid "Password Change"
|
||||
msgstr "Canvi contrasenya"
|
||||
|
||||
#: web/template/profile.html:27
|
||||
#: web/template/profile.html:28
|
||||
msgctxt "input"
|
||||
msgid "Password Confirmation"
|
||||
msgstr "Confirmació contrasenya"
|
||||
|
||||
#: web/template/profile.html:31
|
||||
#: web/template/profile.html:33
|
||||
msgctxt "input"
|
||||
msgid "Language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: web/template/profile.html:33
|
||||
#: web/template/profile.html:36
|
||||
msgctxt "language option"
|
||||
msgid "Automatic"
|
||||
msgstr "Automàtic"
|
||||
|
||||
#: web/template/profile.html:38
|
||||
#: web/template/profile.html:42
|
||||
msgctxt "action"
|
||||
msgid "Save changes"
|
||||
msgstr "Desa canvis"
|
||||
|
||||
#: web/template/tax-details.html:3 pkg/company.go:87
|
||||
msgctxt "title"
|
||||
msgid "Tax Details"
|
||||
msgstr "Configuració fiscal"
|
||||
|
||||
#: web/template/tax-details.html:7
|
||||
msgctxt "input"
|
||||
msgid "Business name"
|
||||
msgstr "Nom i cognom"
|
||||
|
||||
#: web/template/tax-details.html:11
|
||||
msgctxt "input"
|
||||
msgid "VAT number"
|
||||
msgstr "DNI / NIF"
|
||||
|
||||
#: web/template/tax-details.html:15
|
||||
msgctxt "input"
|
||||
msgid "Trade name"
|
||||
msgstr "Nom comercial"
|
||||
|
||||
#: web/template/tax-details.html:19
|
||||
msgctxt "input"
|
||||
msgid "Phone"
|
||||
msgstr "Telèfon"
|
||||
|
||||
#: web/template/tax-details.html:27
|
||||
msgctxt "input"
|
||||
msgid "Web"
|
||||
msgstr "Web"
|
||||
|
||||
#: web/template/tax-details.html:31
|
||||
msgctxt "input"
|
||||
msgid "Address"
|
||||
msgstr "Adreça"
|
||||
|
||||
#: web/template/tax-details.html:35
|
||||
msgctxt "input"
|
||||
msgid "City"
|
||||
msgstr "Població"
|
||||
|
||||
#: web/template/tax-details.html:39
|
||||
msgctxt "input"
|
||||
msgid "Province"
|
||||
msgstr "Província"
|
||||
|
||||
#: web/template/tax-details.html:43
|
||||
msgctxt "input"
|
||||
msgid "Postal code"
|
||||
msgstr "Codi postal"
|
||||
|
||||
#: web/template/tax-details.html:47
|
||||
msgctxt "input"
|
||||
msgid "Country"
|
||||
msgstr "País"
|
||||
|
||||
#: web/template/app.html:20
|
||||
msgctxt "menu"
|
||||
msgid "Account"
|
||||
msgstr "Compte"
|
||||
|
||||
#: web/template/app.html:27
|
||||
#: web/template/app.html:26
|
||||
msgctxt "menu"
|
||||
msgid "Tax Details"
|
||||
msgstr "Configuració fiscal"
|
||||
|
||||
#: web/template/app.html:33
|
||||
msgctxt "action"
|
||||
msgid "Logout"
|
||||
msgstr "Surt"
|
||||
|
|
86
po/es.po
86
po/es.po
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: numerus\n"
|
||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||
"POT-Creation-Date: 2023-01-23 18:50+0100\n"
|
||||
"POT-Creation-Date: 2023-01-24 21:37+0100\n"
|
||||
"PO-Revision-Date: 2023-01-18 17:45+0100\n"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Spanish <es@tp.org.es>\n"
|
||||
|
@ -26,12 +26,13 @@ msgstr "Entrada"
|
|||
msgid "Invalid user or password"
|
||||
msgstr "Nombre de usuario o contraseña inválido"
|
||||
|
||||
#: web/template/login.html:13 web/template/profile.html:14
|
||||
#: web/template/login.html:13 web/template/profile.html:15
|
||||
#: web/template/tax-details.html:23
|
||||
msgctxt "input"
|
||||
msgid "Email"
|
||||
msgstr "Correo-e"
|
||||
|
||||
#: web/template/login.html:18 web/template/profile.html:22
|
||||
#: web/template/login.html:18 web/template/profile.html:23
|
||||
msgctxt "input"
|
||||
msgid "Password"
|
||||
msgstr "Contraseña"
|
||||
|
@ -41,51 +42,112 @@ msgctxt "action"
|
|||
msgid "Login"
|
||||
msgstr "Entrar"
|
||||
|
||||
#: web/template/profile.html:2 pkg/profile.go:33
|
||||
#: web/template/profile.html:3 pkg/profile.go:29
|
||||
msgctxt "title"
|
||||
msgid "User Settings"
|
||||
msgstr "Configuración usuario"
|
||||
|
||||
#: web/template/profile.html:5
|
||||
#: web/template/profile.html:6
|
||||
msgctxt "title"
|
||||
msgid "User Access Data"
|
||||
msgstr "Datos acceso usuario"
|
||||
|
||||
#: web/template/profile.html:9
|
||||
#: web/template/profile.html:10
|
||||
msgctxt "input"
|
||||
msgid "User name"
|
||||
msgstr "Nombre de usuario"
|
||||
|
||||
#: web/template/profile.html:18
|
||||
#: web/template/profile.html:19
|
||||
msgctxt "title"
|
||||
msgid "Password Change"
|
||||
msgstr "Cambio de contraseña"
|
||||
|
||||
#: web/template/profile.html:27
|
||||
#: web/template/profile.html:28
|
||||
msgctxt "input"
|
||||
msgid "Password Confirmation"
|
||||
msgstr "Confirmación contrasenya"
|
||||
|
||||
#: web/template/profile.html:31
|
||||
#: web/template/profile.html:33
|
||||
msgctxt "input"
|
||||
msgid "Language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: web/template/profile.html:33
|
||||
#: web/template/profile.html:36
|
||||
msgctxt "language option"
|
||||
msgid "Automatic"
|
||||
msgstr "Automático"
|
||||
|
||||
#: web/template/profile.html:38
|
||||
#: web/template/profile.html:42
|
||||
msgctxt "action"
|
||||
msgid "Save changes"
|
||||
msgstr "Guardar cambios"
|
||||
|
||||
#: web/template/tax-details.html:3 pkg/company.go:87
|
||||
msgctxt "title"
|
||||
msgid "Tax Details"
|
||||
msgstr "Configuración fiscal"
|
||||
|
||||
#: web/template/tax-details.html:7
|
||||
msgctxt "input"
|
||||
msgid "Business name"
|
||||
msgstr "Nombre y apellidos"
|
||||
|
||||
#: web/template/tax-details.html:11
|
||||
msgctxt "input"
|
||||
msgid "VAT number"
|
||||
msgstr "DNI / NIF"
|
||||
|
||||
#: web/template/tax-details.html:15
|
||||
msgctxt "input"
|
||||
msgid "Trade name"
|
||||
msgstr "Nombre comercial"
|
||||
|
||||
#: web/template/tax-details.html:19
|
||||
msgctxt "input"
|
||||
msgid "Phone"
|
||||
msgstr "Teléfono"
|
||||
|
||||
#: web/template/tax-details.html:27
|
||||
msgctxt "input"
|
||||
msgid "Web"
|
||||
msgstr "Web"
|
||||
|
||||
#: web/template/tax-details.html:31
|
||||
msgctxt "input"
|
||||
msgid "Address"
|
||||
msgstr "Dirección"
|
||||
|
||||
#: web/template/tax-details.html:35
|
||||
msgctxt "input"
|
||||
msgid "City"
|
||||
msgstr "Población"
|
||||
|
||||
#: web/template/tax-details.html:39
|
||||
msgctxt "input"
|
||||
msgid "Province"
|
||||
msgstr "Provincia"
|
||||
|
||||
#: web/template/tax-details.html:43
|
||||
msgctxt "input"
|
||||
msgid "Postal code"
|
||||
msgstr "Código postal"
|
||||
|
||||
#: web/template/tax-details.html:47
|
||||
msgctxt "input"
|
||||
msgid "Country"
|
||||
msgstr "País"
|
||||
|
||||
#: web/template/app.html:20
|
||||
msgctxt "menu"
|
||||
msgid "Account"
|
||||
msgstr "Cuenta"
|
||||
|
||||
#: web/template/app.html:27
|
||||
#: web/template/app.html:26
|
||||
msgctxt "menu"
|
||||
msgid "Tax Details"
|
||||
msgstr "Configuración fiscal"
|
||||
|
||||
#: web/template/app.html:33
|
||||
msgctxt "action"
|
||||
msgid "Logout"
|
||||
msgstr "Salir"
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:available_currencies from pg
|
||||
|
||||
begin;
|
||||
|
||||
delete from numerus.currency;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:company from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop table numerus.company;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,9 @@
|
|||
-- Revert numerus:company_user from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop policy if exists company_policy on numerus.company;
|
||||
drop policy if exists company_policy on numerus.company_user;
|
||||
drop table if exists numerus.company_user;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:currency from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop table numerus.currency;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:currency_code from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop domain if exists numerus.currency_code;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:extension_pg_libphonenumber from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop extension if exists pg_libphonenumber;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:extension_uri from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop extension if exists uri;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:extension_vat from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop extension if exists vat;
|
||||
|
||||
commit;
|
12
sqitch.plan
12
sqitch.plan
|
@ -9,11 +9,11 @@ schema_numerus [roles] 2023-01-12T22:57:22Z jordi fita mas <jordi@tandem.blog> #
|
|||
extension_citext [schema_public] 2023-01-12T23:03:33Z jordi fita mas <jordi@tandem.blog> # Add citext extension
|
||||
email [schema_numerus extension_citext] 2023-01-12T23:09:59Z jordi fita mas <jordi@tandem.blog> # Add email domain
|
||||
language [schema_numerus] 2023-01-21T20:55:49Z jordi fita mas <jordi@tandem.blog> # Add relation of available languages
|
||||
user [roles schema_auth email language] 2023-01-12T23:44:03Z jordi fita mas <jordi@tandem.blog> # Create user table
|
||||
user [roles schema_auth email language] 2023-01-12T23:44:03Z jordi fita mas <jordi@tandem.blog> # Create user relation
|
||||
ensure_role_exists [schema_auth user] 2023-01-12T23:57:59Z jordi fita mas <jordi@tandem.blog> # Add trigger to ensure the user’s role exists
|
||||
extension_pgcrypto [schema_auth] 2023-01-13T00:11:50Z jordi fita mas <jordi@tandem.blog> # Add pgcrypto extension
|
||||
encrypt_password [schema_auth user extension_pgcrypto] 2023-01-13T00:14:30Z jordi fita mas <jordi@tandem.blog> # Add trigger to encrypt user’s password
|
||||
login_attempt [schema_auth] 2023-01-17T14:05:49Z jordi fita mas <jordi@tandem.blog> # Add table to log login attempts
|
||||
login_attempt [schema_auth] 2023-01-17T14:05:49Z jordi fita mas <jordi@tandem.blog> # Add relation to log login attempts
|
||||
login [roles schema_numerus schema_auth extension_pgcrypto email user login_attempt] 2023-01-13T00:32:32Z jordi fita mas <jordi@tandem.blog> # Add function to login
|
||||
current_user_cookie [schema_numerus] 2023-01-21T20:16:28Z jordi fita mas <jordi@tandem.blog> # Add function to get the cookie of the current Numerus’ user
|
||||
current_user_email [schema_numerus] 2023-01-23T19:11:53Z jordi fita mas <jordi@tandem.blog> # Add function to get the email of the current Numerus’ user
|
||||
|
@ -24,3 +24,11 @@ set_cookie [schema_public check_cookie] 2023-01-19T11:00:22Z jordi fita mas <jor
|
|||
available_languages [schema_numerus language] 2023-01-21T21:11:08Z jordi fita mas <jordi@tandem.blog> # Add the initial available languages
|
||||
user_profile [schema_numerus user current_user_email current_user_cookie] 2023-01-21T23:18:20Z jordi fita mas <jordi@tandem.blog> # Add view for user profile
|
||||
change_password [schema_numerus user] 2023-01-23T20:22:45Z jordi fita mas <jordi@tandem.blog> # Add function to change the current user’s password
|
||||
extension_vat [schema_public] 2023-01-24T10:28:17Z jordi fita mas <jordi@tandem.blog> # Add vat extension
|
||||
extension_pg_libphonenumber [schema_public] 2023-01-24T13:50:14Z jordi fita mas <jordi@tandem.blog> # Add extension for phone numbers
|
||||
extension_uri [schema_public] 2023-01-24T14:29:29Z jordi fita mas <jordi@tandem.blog> # Add extension for URIs
|
||||
currency_code [schema_numerus] 2023-01-24T14:36:04Z jordi fita mas <jordi@tandem.blog> # Add the domain for currency code in ISO 4217
|
||||
currency [schema_numerus currency_code] 2023-01-24T14:45:26Z jordi fita mas <jordi@tandem.blog> # Add the relation for currencies
|
||||
available_currencies [schema_numerus currency] 2023-01-24T14:54:18Z jordi fita mas <jordi@tandem.blog> # Add the initial list of available currencies
|
||||
company [schema_numerus extension_vat email extension_pg_libphonenumber extension_uri currency_code currency] 2023-01-24T15:03:15Z jordi fita mas <jordi@tandem.blog> # Add the relation for companies
|
||||
company_user [schema_numerus user company] 2023-01-24T17:50:06Z jordi fita mas <jordi@tandem.blog> # Add the relation of companies and their users
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
-- Test company
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(77);
|
||||
|
||||
set search_path to numerus, auth, public;
|
||||
|
||||
select has_table('company');
|
||||
select has_pk('company');
|
||||
select table_privs_are('company', 'guest', array []::text[]);
|
||||
select table_privs_are('company', 'invoicer', array ['SELECT']);
|
||||
select table_privs_are('company', 'admin', array ['SELECT']);
|
||||
select table_privs_are('company', 'authenticator', array []::text[]);
|
||||
|
||||
select has_column('company', 'company_id');
|
||||
select col_is_pk('company', 'company_id');
|
||||
select col_type_is('company', 'company_id', 'integer');
|
||||
select col_not_null('company', 'company_id');
|
||||
select col_has_default('company', 'company_id');
|
||||
select col_default_is('company', 'company_id', 'nextval(''company_company_id_seq''::regclass)');
|
||||
|
||||
select has_column('company', 'slug');
|
||||
select col_is_unique('company', 'slug');
|
||||
select col_type_is('company', 'slug', 'uuid');
|
||||
select col_not_null('company', 'slug');
|
||||
select col_has_default('company', 'slug');
|
||||
select col_default_is('company', 'slug', 'gen_random_uuid()');
|
||||
|
||||
select has_column('company', 'business_name');
|
||||
select col_type_is('company', 'business_name', 'text');
|
||||
select col_not_null('company', 'business_name');
|
||||
select col_hasnt_default('company', 'business_name');
|
||||
|
||||
select has_column('company', 'vatin');
|
||||
select col_type_is('company', 'vatin', 'vatin');
|
||||
select col_not_null('company', 'vatin');
|
||||
select col_hasnt_default('company', 'vatin');
|
||||
|
||||
select has_column('company', 'trade_name');
|
||||
select col_type_is('company', 'trade_name', 'text');
|
||||
select col_not_null('company', 'trade_name');
|
||||
select col_hasnt_default('company', 'trade_name');
|
||||
|
||||
select has_column('company', 'phone');
|
||||
select col_type_is('company', 'phone', 'packed_phone_number');
|
||||
select col_not_null('company', 'phone');
|
||||
select col_hasnt_default('company', 'phone');
|
||||
|
||||
select has_column('company', 'email');
|
||||
select col_type_is('company', 'email', 'email');
|
||||
select col_not_null('company', 'email');
|
||||
select col_hasnt_default('company', 'email');
|
||||
|
||||
select has_column('company', 'web');
|
||||
select col_type_is('company', 'web', 'uri');
|
||||
select col_not_null('company', 'web');
|
||||
select col_hasnt_default('company', 'web');
|
||||
|
||||
select has_column('company', 'address');
|
||||
select col_type_is('company', 'address', 'text');
|
||||
select col_not_null('company', 'address');
|
||||
select col_hasnt_default('company', 'address');
|
||||
|
||||
select has_column('company', 'city');
|
||||
select col_type_is('company', 'city', 'text');
|
||||
select col_not_null('company', 'city');
|
||||
select col_hasnt_default('company', 'city');
|
||||
|
||||
select has_column('company', 'province');
|
||||
select col_type_is('company', 'province', 'text');
|
||||
select col_not_null('company', 'province');
|
||||
select col_hasnt_default('company', 'province');
|
||||
|
||||
select has_column('company', 'postal_code');
|
||||
select col_type_is('company', 'postal_code', 'text');
|
||||
select col_not_null('company', 'postal_code');
|
||||
select col_hasnt_default('company', 'postal_code');
|
||||
|
||||
select has_column('company', 'country');
|
||||
select col_type_is('company', 'country', 'text');
|
||||
select col_not_null('company', 'country');
|
||||
select col_hasnt_default('company', 'country');
|
||||
|
||||
select has_column('company', 'currency_code');
|
||||
select col_is_fk('company', 'currency_code');
|
||||
select fk_ok('company', 'currency_code', 'currency', 'currency_code');
|
||||
select col_type_is('company', 'currency_code', 'currency_code');
|
||||
select col_not_null('company', 'currency_code');
|
||||
select col_hasnt_default('company', 'currency_code');
|
||||
|
||||
select has_column('company', 'created_at');
|
||||
select col_type_is('company', 'created_at', 'timestamp with time zone');
|
||||
select col_not_null('company', 'created_at');
|
||||
select col_has_default('company', 'created_at');
|
||||
select col_default_is('company', 'created_at', current_timestamp);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate company_user cascade;
|
||||
truncate company 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')
|
||||
;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country, currency_code)
|
||||
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 'EUR')
|
||||
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 'USD')
|
||||
, (6, 'Company 6', 'XX345', '', '777-777-777', 'c@c', '', '', '', '', '', '', 'USD')
|
||||
;
|
||||
|
||||
insert into company_user (company_id, user_id)
|
||||
values (2, 1)
|
||||
, (2, 5)
|
||||
, (4, 1)
|
||||
, (6, 5)
|
||||
;
|
||||
|
||||
prepare company_data as
|
||||
select company_id, business_name
|
||||
from company
|
||||
order by company_id;
|
||||
|
||||
set role invoicer;
|
||||
select is_empty('company_data', 'Should show no data when cookie is not set yet');
|
||||
reset role;
|
||||
|
||||
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
|
||||
select results_eq(
|
||||
'company_data',
|
||||
$$ values ( 2, 'Company 2' )
|
||||
, ( 4, 'Company 4' )
|
||||
$$,
|
||||
'Should only list companies where demo@tandem.blog is user of'
|
||||
);
|
||||
reset role;
|
||||
|
||||
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
|
||||
select results_eq(
|
||||
'company_data',
|
||||
$$ values ( 2, 'Company 2' )
|
||||
, ( 6, 'Company 6' )
|
||||
$$,
|
||||
'Should only list companies where admin@tandem.blog is user of'
|
||||
);
|
||||
reset role;
|
||||
|
||||
select set_cookie('not-a-cookie');
|
||||
select throws_ok(
|
||||
'company_data',
|
||||
'42501', 'permission denied for table company',
|
||||
'Should not allow select to guest users'
|
||||
);
|
||||
reset role;
|
||||
|
||||
select finish();
|
||||
rollback;
|
|
@ -0,0 +1,39 @@
|
|||
-- Test company_user
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(19);
|
||||
|
||||
set search_path to numerus, auth, public;
|
||||
|
||||
select has_table('company_user');
|
||||
select has_pk('company_user' );
|
||||
select col_is_pk('company_user', array['company_id', 'user_id']);
|
||||
select table_privs_are('company_user', 'guest', array []::text[]);
|
||||
select table_privs_are('company_user', 'invoicer', array ['SELECT']);
|
||||
select table_privs_are('company_user', 'admin', array ['SELECT']);
|
||||
select table_privs_are('company_user', 'authenticator', array []::text[]);
|
||||
|
||||
select has_column('company_user', 'company_id');
|
||||
select col_is_fk('company_user', 'company_id');
|
||||
select fk_ok('company_user', 'company_id', 'company', 'company_id');
|
||||
select col_type_is('company_user', 'company_id', 'integer');
|
||||
select col_not_null('company_user', 'company_id');
|
||||
select col_hasnt_default('company_user', 'company_id');
|
||||
|
||||
select has_column('company_user', 'user_id');
|
||||
select col_is_fk('company_user', 'user_id');
|
||||
select fk_ok('company_user', 'user_id', 'user', 'user_id');
|
||||
select col_type_is('company_user', 'user_id', 'integer');
|
||||
select col_not_null('company_user', 'user_id');
|
||||
select col_hasnt_default('company_user', 'user_id');
|
||||
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
-- Test currency
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(20);
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
select has_table('currency');
|
||||
select has_pk('currency');
|
||||
select table_privs_are('currency', 'guest', array []::text[]);
|
||||
select table_privs_are('currency', 'invoicer', array ['SELECT']);
|
||||
select table_privs_are('currency', 'admin', array ['SELECT']);
|
||||
select table_privs_are('currency', 'authenticator', array []::text[]);
|
||||
|
||||
select has_column('currency', 'currency_code');
|
||||
select col_is_pk('currency', 'currency_code');
|
||||
select col_type_is('currency', 'currency_code', 'currency_code');
|
||||
select col_not_null('currency', 'currency_code');
|
||||
select col_hasnt_default('currency', 'currency_code');
|
||||
|
||||
select has_column('currency', 'currency_symbol');
|
||||
select col_type_is('currency', 'currency_symbol', 'text');
|
||||
select col_not_null('currency', 'currency_symbol');
|
||||
select col_hasnt_default('currency', 'currency_symbol');
|
||||
|
||||
select has_column('currency', 'decimal_digits');
|
||||
select col_type_is('currency', 'decimal_digits', 'integer');
|
||||
select col_not_null('currency', 'decimal_digits');
|
||||
select col_has_default('currency', 'decimal_digits');
|
||||
select col_default_is('currency', 'decimal_digits', 2);
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate currency cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
|
||||
select finish();
|
||||
rollback;
|
|
@ -0,0 +1,38 @@
|
|||
-- Test currency_code
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(6);
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
select has_domain('currency_code');
|
||||
select domain_type_is('currency_code', 'text');
|
||||
|
||||
select lives_ok($$ select 'EUR'::currency_code $$, 'Should be able to cast valid text to currency code');
|
||||
|
||||
select throws_ok(
|
||||
$$ SELECT '123'::currency_code $$,
|
||||
23514, null,
|
||||
'Should reject numeric text'
|
||||
);
|
||||
|
||||
select throws_ok(
|
||||
$$ SELECT 'eur'::currency_code $$,
|
||||
23514, null,
|
||||
'Should reject lowecase text'
|
||||
);
|
||||
|
||||
select throws_ok(
|
||||
$$ SELECT 'EURO'::currency_code $$,
|
||||
23514, null,
|
||||
'Should reject text longer than three letters'
|
||||
);
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -9,9 +9,12 @@ select plan(1);
|
|||
|
||||
select extensions_are(array[
|
||||
'citext'
|
||||
, 'pg_libphonenumber'
|
||||
, 'pgtap'
|
||||
, 'pgcrypto'
|
||||
, 'plpgsql'
|
||||
, 'uri'
|
||||
, 'vat'
|
||||
]);
|
||||
|
||||
select *
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
-- Verify numerus:available_currencies on pg
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus;
|
||||
|
||||
select 1 / count(*)
|
||||
from currency
|
||||
where currency_code = 'EUR'
|
||||
and currency_symbol = '€'
|
||||
and decimal_digits = 2
|
||||
;
|
||||
|
||||
select 1 / count(*)
|
||||
from currency
|
||||
where currency_code = 'USD'
|
||||
and currency_symbol = '$'
|
||||
and decimal_digits = 2
|
||||
;
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,23 @@
|
|||
-- Verify numerus:company on pg
|
||||
|
||||
begin;
|
||||
|
||||
select company_id
|
||||
, slug
|
||||
, business_name
|
||||
, vatin
|
||||
, trade_name
|
||||
, phone
|
||||
, email
|
||||
, web
|
||||
, address
|
||||
, city
|
||||
, province
|
||||
, postal_code
|
||||
, country
|
||||
, currency_code
|
||||
, created_at
|
||||
from numerus.company
|
||||
where false;
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,10 @@
|
|||
-- Verify numerus:company_user on pg
|
||||
|
||||
begin;
|
||||
|
||||
select company_id
|
||||
, user_id
|
||||
from numerus.company_user
|
||||
where false;
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,11 @@
|
|||
-- Verify numerus:currency on pg
|
||||
|
||||
begin;
|
||||
|
||||
select currency_code
|
||||
, currency_symbol
|
||||
, decimal_digits
|
||||
from numerus.currency
|
||||
where false;
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:currency_code on pg
|
||||
|
||||
begin;
|
||||
|
||||
select pg_catalog.has_type_privilege('numerus.currency_code', 'usage');
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:extension_pg_libphonenumber on pg
|
||||
|
||||
begin;
|
||||
|
||||
select 1/count(*) from pg_extension where extname = 'pg_libphonenumber';
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:extension_uri on pg
|
||||
|
||||
begin;
|
||||
|
||||
select 1/count(*) from pg_extension where extname = 'uri';
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:extension_vat on pg
|
||||
|
||||
begin;
|
||||
|
||||
select 1/count(*) from pg_extension where extname = 'vat';
|
||||
|
||||
rollback;
|
|
@ -245,9 +245,10 @@ main {
|
|||
.input {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="password"], input[type="email"], select {
|
||||
input[type="text"], input[type="password"], input[type="email"], input[type="tel"], input[type="url"], select {
|
||||
background-color: var(--numerus--background-color);
|
||||
border: 1px solid var(--numerus--color--black);
|
||||
border-radius: 0;
|
||||
|
@ -255,6 +256,10 @@ input[type="text"], input[type="password"], input[type="email"], select {
|
|||
min-width: 30rem;
|
||||
}
|
||||
|
||||
input.width-2x {
|
||||
min-width: 60.95rem;
|
||||
}
|
||||
|
||||
.input input::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
@ -309,7 +314,7 @@ fieldset {
|
|||
}
|
||||
|
||||
.full-width legend {
|
||||
margin-bottom: initial;
|
||||
margin-bottom: -1rem;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -17,7 +17,13 @@
|
|||
<li role="presentation">
|
||||
<a role="menuitem" href="/profile">
|
||||
<i class="ri-account-circle-line"></i>
|
||||
{{( gettext "Account" )}}
|
||||
{{( pgettext "Account" "menu" )}}
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a role="menuitem" href="{{ companyURI "/tax-details" }}">
|
||||
<i class="ri-vip-diamond-line"></i>
|
||||
{{( pgettext "Tax Details" "menu" )}}
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
{{ define "content" }}
|
||||
<section class="dialog-content">
|
||||
<h2>{{(pgettext "Tax Details" "title")}}</h2>
|
||||
<form method="POST">
|
||||
<div class="input">
|
||||
<input type="text" name="business_name" id="business_name" required="required" value="{{ .BusinessName }}" placeholder="{{( pgettext "Business name" "input" )}}">
|
||||
<label for="business_name">{{( pgettext "Business name" "input" )}}</label>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input type="text" name="vatin" id="vatin" required="required" value="{{ .VATIN }}" placeholder="{{( pgettext "VAT number" "input" )}}">
|
||||
<label for="vatin">{{( pgettext "VAT number" "input" )}}</label>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input type="text" name="trade_name" id="trade_name" value="{{ .TradeName }}" placeholder="{{( pgettext "Trade name" "input" )}}">
|
||||
<label for="trade_name">{{( pgettext "Trade name" "input" )}}</label>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input type="tel" name="phone" id="phone" required="required" value="{{ .Phone }}" placeholder="{{( pgettext "Phone" "input" )}}">
|
||||
<label for="phone">{{( pgettext "Phone" "input" )}}</label>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input type="email" name="email" id="email" required="required" value="{{ .Email }}" placeholder="{{( pgettext "Email" "input" )}}">
|
||||
<label for="email">{{( pgettext "Email" "input" )}}</label>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input type="url" name="web" id="web" value="{{ .Web }}" placeholder="{{( pgettext "Web" "input" )}}">
|
||||
<label for="web">{{( pgettext "Web" "input" )}}</label>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input type="text" name="address" id="address" class="width-2x" required="required" value="{{ .Address }}" placeholder="{{( pgettext "Address" "input" )}}">
|
||||
<label for="address">{{( pgettext "Address" "input" )}}</label>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input type="text" name="city" id="city" required="required" value="{{ .City }}" placeholder="{{( pgettext "City" "input" )}}">
|
||||
<label for="city">{{( pgettext "City" "input" )}}</label>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input type="text" name="province" id="province" required="required" value="{{ .City }}" placeholder="{{( pgettext "Province" "input" )}}">
|
||||
<label for="province">{{( pgettext "Province" "input" )}}</label>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input type="text" name="postal_code" id="postal_code" required="required" value="{{ .PostalCode }}" placeholder="{{( pgettext "Postal code" "input" )}}">
|
||||
<label for="postal_code">{{( pgettext "Postal code" "input" )}}</label>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input type="text" name="country" id="country" required="required" value="{{ .Country }}" placeholder="{{( pgettext "Country" "input" )}}">
|
||||
<label for="country">{{( pgettext "Country" "input" )}}</label>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{{- end }}
|
Loading…
Reference in New Issue