Add cover media to campsite types
This is the image that is shown at the home page, and maybe other pages in the future. We can not use a static file because this image can be changed by the customer, not us; just like name and description. I decided to keep the actual media content in the database, but to copy this file out to the file system the first time it is accessed. This is because we are going to replicate the database to a public instance that must show exactly the same image, but the customer will update the image from the private instance, behind a firewall. We could also synchronize the folder where they upload the images, the same way we will replicate, but i thought that i would make the whole thing a little more brittle: this way if it can replicate the update of the media, it is impossible to not have its contents; dumping it to a file is to improve subsequent requests to the same media. I use the hex representation of the media’s hash as the URL to the resource, because PostgreSQL’s base64 is not URL save (i.e., it uses RFC2045’s charset that includes the forward slash[0]), and i did not feel necessary write a new function just to slightly reduce the URLs’ length. Before checking if the file exists, i make sure that the given hash is an hex string, like i do for UUID, otherwise any other check is going to fail for sure. I moved out hex.Valid function from UUID to check for valid hex values, but the actual hash check is inside app/media because i doubt it will be used outside that module. [0]: https://datatracker.ietf.org/doc/html/rfc2045#section-6.8
This commit is contained in:
parent
de0fac1368
commit
da127124a1
|
@ -19,6 +19,7 @@ import (
|
|||
)
|
||||
|
||||
var avatarsDir = "/var/lib/camper/avatars"
|
||||
var mediaDir = "/var/lib/camper/media"
|
||||
|
||||
func main() {
|
||||
db, err := database.New(context.Background(), os.Getenv("CAMPER_DATABASE_URL"))
|
||||
|
@ -27,7 +28,7 @@ func main() {
|
|||
}
|
||||
defer db.Close()
|
||||
|
||||
handler, err := app.New(db, avatarsDir)
|
||||
handler, err := app.New(db, avatarsDir, mediaDir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -8,16 +8,16 @@ begin;
|
|||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function add_campsite_type(company integer, name text, description text) returns uuid as
|
||||
create or replace function add_campsite_type(company integer, media_id integer, name text, description text) returns uuid as
|
||||
$$
|
||||
insert into campsite_type (company_id, name, description)
|
||||
values (company, name, xmlparse (content description))
|
||||
insert into campsite_type (company_id, media_id, name, description)
|
||||
values (company, media_id, name, xmlparse (content description))
|
||||
returning slug;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function add_campsite_type(integer, text, text) from public;
|
||||
grant execute on function add_campsite_type(integer, text, text) to admin;
|
||||
revoke execute on function add_campsite_type(integer, integer, text, text) from public;
|
||||
grant execute on function add_campsite_type(integer, integer, text, text) to admin;
|
||||
|
||||
commit;
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
-- requires: schema_camper
|
||||
-- requires: company
|
||||
-- requires: user_profile
|
||||
-- requires: media
|
||||
|
||||
begin;
|
||||
|
||||
|
@ -13,6 +14,7 @@ create table campsite_type (
|
|||
company_id integer not null references company,
|
||||
slug uuid not null unique default gen_random_uuid(),
|
||||
name text not null constraint name_not_empty check(length(trim(name)) > 0),
|
||||
media_id integer not null references media,
|
||||
description xml not null default ''::xml,
|
||||
active boolean not null default true
|
||||
);
|
||||
|
|
|
@ -8,11 +8,12 @@ begin;
|
|||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function edit_campsite_type(slug uuid, name text, description text, active boolean) returns uuid as
|
||||
create or replace function edit_campsite_type(slug uuid, media_id integer, name text, description text, active boolean) returns uuid as
|
||||
$$
|
||||
update campsite_type
|
||||
set name = edit_campsite_type.name
|
||||
, description = xmlparse(content edit_campsite_type.description)
|
||||
, media_id = coalesce(edit_campsite_type.media_id, campsite_type.media_id)
|
||||
, active = edit_campsite_type.active
|
||||
where slug = edit_campsite_type.slug
|
||||
returning slug;
|
||||
|
@ -20,7 +21,7 @@ $$
|
|||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function edit_campsite_type(uuid, text, text, boolean) from public;
|
||||
grant execute on function edit_campsite_type(uuid, text, text, boolean) to admin;
|
||||
revoke execute on function edit_campsite_type(uuid, integer, text, text, boolean) from public;
|
||||
grant execute on function edit_campsite_type(uuid, integer, text, text, boolean) to admin;
|
||||
|
||||
commit;
|
||||
|
|
|
@ -23,12 +23,13 @@ type App struct {
|
|||
profile *profileHandler
|
||||
admin *adminHandler
|
||||
public *publicHandler
|
||||
media *mediaHandler
|
||||
locales locale.Locales
|
||||
defaultLocale *locale.Locale
|
||||
languageMatcher language.Matcher
|
||||
}
|
||||
|
||||
func New(db *database.DB, avatarsDir string) (http.Handler, error) {
|
||||
func New(db *database.DB, avatarsDir string, mediaDir string) (http.Handler, error) {
|
||||
locales, err := locale.GetAll(context.Background(), db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -38,12 +39,17 @@ func New(db *database.DB, avatarsDir string) (http.Handler, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
media, err := newMediaHandler(mediaDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
app := &App{
|
||||
db: db,
|
||||
fileHandler: static,
|
||||
profile: profile,
|
||||
admin: newAdminHandler(),
|
||||
public: newPublicHandler(),
|
||||
media: media,
|
||||
locales: locales,
|
||||
defaultLocale: locales[language.Catalan],
|
||||
languageMatcher: language.NewMatcher(locales.Tags()),
|
||||
|
@ -104,6 +110,8 @@ func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
case "me":
|
||||
h.profile.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "media":
|
||||
h.media.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
default:
|
||||
langTag, err := language.Parse(head)
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
"dev.tandem.ws/tandem/camper/pkg/hex"
|
||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||
)
|
||||
|
||||
type mediaHandler struct {
|
||||
mediaDir string
|
||||
fileHandler http.Handler
|
||||
}
|
||||
|
||||
func newMediaHandler(mediaDir string) (*mediaHandler, error) {
|
||||
if err := os.MkdirAll(mediaDir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handler := &mediaHandler{
|
||||
mediaDir: mediaDir,
|
||||
fileHandler: http.FileServer(http.Dir(mediaDir)),
|
||||
}
|
||||
return handler, nil
|
||||
}
|
||||
|
||||
func (h *mediaHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var head string
|
||||
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||||
|
||||
if !mediaHashValid(head) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
h.serveMedia(w, r, company, conn, strings.ToLower(head))
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *mediaHandler) serveMedia(w http.ResponseWriter, r *http.Request, company *auth.Company, conn *database.Conn, hash string) {
|
||||
mediaPath := h.mediaPath(hash)
|
||||
var err error
|
||||
if _, err = os.Stat(mediaPath); err != nil {
|
||||
bytes, err := conn.GetBytes(r.Context(), "select content from media where company_id = $1 and hash = decode($2, 'hex')", company.ID, hash)
|
||||
if err != nil {
|
||||
if database.ErrorIsNotFound(err) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
if err = os.MkdirAll(path.Dir(mediaPath), 0755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err = os.WriteFile(mediaPath, bytes, 0644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
r.URL.Path, err = filepath.Rel(h.mediaDir, mediaPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
h.fileHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (h *mediaHandler) mediaPath(hash string) string {
|
||||
return filepath.Join(h.mediaDir, hash[:2], hash[2:])
|
||||
}
|
||||
|
||||
func mediaHashValid(s string) bool {
|
||||
if len(s) != 64 {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < 64; i += 2 {
|
||||
if !hex.Valid(s[i], s[i+1]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type test struct {
|
||||
in string
|
||||
isHash bool
|
||||
}
|
||||
|
||||
var tests = []test{
|
||||
{"6ccd4e641d6c52c11262b1a8140d656ff80c5c32e230ada565d5d46f11132ead", true},
|
||||
{"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", true},
|
||||
{"x6df42afe8fafbe77089309b732b5c20234d4b0ed429144ba5b8471b1b371310", false},
|
||||
{"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde99", false},
|
||||
{"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde", false},
|
||||
}
|
||||
|
||||
func testValid(t *testing.T, in string, isHash bool) {
|
||||
if ok := mediaHashValid(in); ok != isHash {
|
||||
t.Errorf("Valid(%s) got %v expected %v", in, ok, isHash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaHash(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
testValid(t, tt.in, tt.isHash)
|
||||
testValid(t, strings.ToUpper(tt.in), tt.isHash)
|
||||
}
|
||||
}
|
|
@ -6,12 +6,14 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/campsite"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||
"dev.tandem.ws/tandem/camper/pkg/template"
|
||||
)
|
||||
|
||||
|
@ -43,13 +45,46 @@ func (h *publicHandler) Handler(user *auth.User, company *auth.Company, conn *da
|
|||
|
||||
type homePage struct {
|
||||
*template.PublicPage
|
||||
CampsiteTypes []*campsiteType
|
||||
}
|
||||
|
||||
func newHomePage() *homePage {
|
||||
return &homePage{template.NewPublicPage()}
|
||||
return &homePage{template.NewPublicPage(), nil}
|
||||
}
|
||||
|
||||
func (p *homePage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||
p.Setup(r, user, company, conn)
|
||||
p.CampsiteTypes = mustCollectCampsiteTypes(r.Context(), company, conn, user.Locale)
|
||||
template.MustRenderPublic(w, r, user, company, "home.gohtml", p)
|
||||
}
|
||||
|
||||
type campsiteType struct {
|
||||
Label string
|
||||
HRef string
|
||||
Media string
|
||||
}
|
||||
|
||||
func mustCollectCampsiteTypes(ctx context.Context, company *auth.Company, conn *database.Conn, loc *locale.Locale) []*campsiteType {
|
||||
rows, err := conn.Query(ctx, "select name, '/campsites/types/' || slug, '/media/' || encode(hash, 'hex') from campsite_type join media using (media_id) where campsite_type.company_id = $1", company.ID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
localePath := "/" + loc.Language.String()
|
||||
var items []*campsiteType
|
||||
for rows.Next() {
|
||||
item := &campsiteType{}
|
||||
err = rows.Scan(&item.Label, &item.HRef, &item.Media)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
item.HRef = localePath + item.HRef
|
||||
items = append(items, item)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
panic(rows.Err())
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ package types
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
|
@ -115,48 +116,56 @@ func (page *typeIndex) MustRender(w http.ResponseWriter, r *http.Request, user *
|
|||
|
||||
func addType(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||
f := newTypeForm()
|
||||
if err := f.Parse(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := user.VerifyCSRFToken(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !f.Valid(user.Locale) {
|
||||
if !httplib.IsHTMxRequest(r) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
}
|
||||
f.MustRender(w, r, user, company)
|
||||
return
|
||||
}
|
||||
conn.MustExec(r.Context(), "select add_campsite_type($1, $2, $3)", company.ID, f.Name, f.Description)
|
||||
httplib.Redirect(w, r, "/admin/campsites/types", http.StatusSeeOther)
|
||||
processTypeForm(w, r, user, company, true, f, func(ctx context.Context) {
|
||||
bytes := f.MustReadAllMedia()
|
||||
tx := conn.MustBegin(ctx)
|
||||
defer tx.Rollback(ctx)
|
||||
mediaID := tx.MustGetInt(ctx, "select add_media($1, $2, $3, $4)", company.ID, f.Media.Filename(), f.Media.ContentType, bytes)
|
||||
tx.MustExec(ctx, "select add_campsite_type($1, $2, $3, $4)", company.ID, mediaID, f.Name, f.Description)
|
||||
tx.MustCommit(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
func editType(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *typeForm) {
|
||||
if err := f.Parse(r); err != nil {
|
||||
processTypeForm(w, r, user, company, false, f, func(ctx context.Context) {
|
||||
bytes := f.MustReadAllMedia()
|
||||
if bytes == nil {
|
||||
conn.MustExec(ctx, "select edit_campsite_type($1, $2, $3, $4, $5)", f.Slug, nil, f.Name, f.Description, f.Active)
|
||||
} else {
|
||||
tx := conn.MustBegin(ctx)
|
||||
defer tx.Rollback(ctx)
|
||||
mediaID := tx.MustGetInt(ctx, "select add_media($1, $2, $3, $4)", company.ID, f.Media.Filename(), f.Media.ContentType, bytes)
|
||||
tx.MustExec(ctx, "select edit_campsite_type($1, $2, $3, $4, $5)", f.Slug, mediaID, f.Name, f.Description, f.Active)
|
||||
tx.MustCommit(ctx)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func processTypeForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, mediaRequired bool, f *typeForm, act func(ctx context.Context)) {
|
||||
if err := f.Parse(w, r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
if err := user.VerifyCSRFToken(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !f.Valid(user.Locale) {
|
||||
if !f.Valid(user.Locale, mediaRequired) {
|
||||
if !httplib.IsHTMxRequest(r) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
}
|
||||
f.MustRender(w, r, user, company)
|
||||
return
|
||||
}
|
||||
conn.MustExec(r.Context(), "select edit_campsite_type($1, $2, $3, $4)", f.Slug, f.Name, f.Description, f.Active)
|
||||
act(r.Context())
|
||||
httplib.Redirect(w, r, "/admin/campsites/types", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
type typeForm struct {
|
||||
Slug string
|
||||
Active *form.Checkbox
|
||||
Media *form.File
|
||||
Name *form.Input
|
||||
Description *form.Input
|
||||
}
|
||||
|
@ -167,6 +176,10 @@ func newTypeForm() *typeForm {
|
|||
Name: "active",
|
||||
Checked: true,
|
||||
},
|
||||
Media: &form.File{
|
||||
Name: "media",
|
||||
MaxSize: 1 << 20,
|
||||
},
|
||||
Name: &form.Input{
|
||||
Name: "name",
|
||||
},
|
||||
|
@ -178,26 +191,63 @@ func newTypeForm() *typeForm {
|
|||
|
||||
func (f *typeForm) FillFromDatabase(ctx context.Context, conn *database.Conn, slug string) error {
|
||||
f.Slug = slug
|
||||
row := conn.QueryRow(ctx, "select name, description, active from campsite_type where slug = $1", slug)
|
||||
return row.Scan(&f.Name.Val, &f.Description.Val, &f.Active.Checked)
|
||||
row := conn.QueryRow(ctx, `
|
||||
select name
|
||||
, description
|
||||
, encode(hash, 'hex')
|
||||
, active
|
||||
from campsite_type
|
||||
join media using (media_id)
|
||||
where slug = $1
|
||||
`, slug)
|
||||
return row.Scan(&f.Name.Val, &f.Description.Val, &f.Media.Val, &f.Active.Checked)
|
||||
}
|
||||
|
||||
func (f *typeForm) Parse(r *http.Request) error {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
func (f *typeForm) Parse(w http.ResponseWriter, r *http.Request) error {
|
||||
maxSize := f.Media.MaxSize + 1024
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxSize)
|
||||
if err := r.ParseMultipartForm(maxSize); err != nil {
|
||||
return err
|
||||
}
|
||||
f.Active.FillValue(r)
|
||||
f.Name.FillValue(r)
|
||||
f.Description.FillValue(r)
|
||||
if err := f.Media.FillValue(r); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *typeForm) Valid(l *locale.Locale) bool {
|
||||
func (f *typeForm) Close() error {
|
||||
return f.Media.Close()
|
||||
}
|
||||
|
||||
func (f *typeForm) Valid(l *locale.Locale, mediaRequired bool) bool {
|
||||
v := form.NewValidator(l)
|
||||
v.CheckRequired(f.Name, l.GettextNoop("Name can not be empty."))
|
||||
if f.HasMediaFile() {
|
||||
v.CheckImageFile(f.Media, l.GettextNoop("File must be a valid PNG or JPEG image."))
|
||||
} else {
|
||||
v.Check(f.Media, !mediaRequired, l.GettextNoop("Cover image can not be empty."))
|
||||
}
|
||||
return v.AllOK
|
||||
}
|
||||
|
||||
func (f *typeForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||
template.MustRenderAdmin(w, r, user, company, "campsite/type/form.gohtml", f)
|
||||
}
|
||||
|
||||
func (f *typeForm) HasMediaFile() bool {
|
||||
return f.Media.HasData()
|
||||
}
|
||||
|
||||
func (f *typeForm) MustReadAllMedia() []byte {
|
||||
if !f.HasMediaFile() {
|
||||
return nil
|
||||
}
|
||||
bytes, err := io.ReadAll(f.Media)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
|
|
@ -63,6 +63,14 @@ type Conn struct {
|
|||
*pgxpool.Conn
|
||||
}
|
||||
|
||||
func (c *Conn) MustBegin(ctx context.Context) *Tx {
|
||||
tx, err := c.Begin(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &Tx{tx}
|
||||
}
|
||||
|
||||
func (c *Conn) MustExec(ctx context.Context, sql string, args ...interface{}) pgconn.CommandTag {
|
||||
tag, err := c.Conn.Exec(ctx, sql, args...)
|
||||
if err != nil {
|
||||
|
@ -94,3 +102,9 @@ func (c *Conn) GetBool(ctx context.Context, sql string, args ...interface{}) (bo
|
|||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Conn) GetBytes(ctx context.Context, sql string, args ...interface{}) ([]byte, error) {
|
||||
var result []byte
|
||||
err := c.QueryRow(ctx, sql, args...).Scan(&result)
|
||||
return result, err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/jackc/pgconn"
|
||||
"github.com/jackc/pgx/v4"
|
||||
)
|
||||
|
||||
type Tx struct {
|
||||
pgx.Tx
|
||||
}
|
||||
|
||||
func (tx *Tx) MustCommit(ctx context.Context) {
|
||||
if err := tx.Tx.Commit(ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (tx *Tx) MustExec(ctx context.Context, sql string, args ...interface{}) pgconn.CommandTag {
|
||||
tag, err := tx.Tx.Exec(ctx, sql, args...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return tag
|
||||
}
|
||||
|
||||
func (tx *Tx) GetInt(ctx context.Context, sql string, args ...interface{}) (int, error) {
|
||||
var result int
|
||||
err := tx.QueryRow(ctx, sql, args...).Scan(&result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (tx *Tx) MustGetInt(ctx context.Context, sql string, args ...interface{}) int {
|
||||
if result, err := tx.GetInt(ctx, sql, args...); err == nil {
|
||||
return result
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -43,6 +43,13 @@ func (f *File) FillValue(r *http.Request) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (f *File) Filename() string {
|
||||
if f.header == nil {
|
||||
return ""
|
||||
}
|
||||
return f.header.Filename
|
||||
}
|
||||
|
||||
func (f *File) Close() error {
|
||||
if !f.HasData() {
|
||||
return nil
|
||||
|
|
|
@ -29,21 +29,21 @@ func NewValidator(l *locale.Locale) *Validator {
|
|||
}
|
||||
|
||||
func (v *Validator) CheckRequired(input *Input, message string) bool {
|
||||
return v.check(input, input.Val != "", message)
|
||||
return v.Check(input, input.Val != "", message)
|
||||
}
|
||||
|
||||
func (v *Validator) CheckMinLength(input *Input, min int, message string) bool {
|
||||
return v.check(input, len(input.Val) >= min, message)
|
||||
return v.Check(input, len(input.Val) >= min, message)
|
||||
}
|
||||
|
||||
func (v *Validator) CheckValidEmail(input *Input, message string) bool {
|
||||
_, err := mail.ParseAddress(input.Val)
|
||||
return v.check(input, err == nil, message)
|
||||
return v.Check(input, err == nil, message)
|
||||
}
|
||||
|
||||
func (v *Validator) CheckValidURL(input *Input, message string) bool {
|
||||
_, err := url.Parse(input.Val)
|
||||
return v.check(input, err == nil, message)
|
||||
return v.Check(input, err == nil, message)
|
||||
}
|
||||
|
||||
func (v *Validator) CheckValidVATIN(ctx context.Context, conn *database.Conn, input *Input, country string, message string) (bool, error) {
|
||||
|
@ -51,7 +51,7 @@ func (v *Validator) CheckValidVATIN(ctx context.Context, conn *database.Conn, in
|
|||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return v.check(input, b, message), nil
|
||||
return v.Check(input, b, message), nil
|
||||
}
|
||||
|
||||
func (v *Validator) CheckValidPhone(ctx context.Context, conn *database.Conn, input *Input, country string, message string) (bool, error) {
|
||||
|
@ -59,7 +59,7 @@ func (v *Validator) CheckValidPhone(ctx context.Context, conn *database.Conn, in
|
|||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return v.check(input, b, message), nil
|
||||
return v.Check(input, b, message), nil
|
||||
}
|
||||
|
||||
func (v *Validator) CheckValidColor(ctx context.Context, conn *database.Conn, input *Input, message string) (bool, error) {
|
||||
|
@ -67,7 +67,7 @@ func (v *Validator) CheckValidColor(ctx context.Context, conn *database.Conn, in
|
|||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return v.check(input, b, message), nil
|
||||
return v.Check(input, b, message), nil
|
||||
}
|
||||
|
||||
func (v *Validator) CheckValidPostalCode(ctx context.Context, conn *database.Conn, input *Input, country string, message string) (bool, error) {
|
||||
|
@ -79,26 +79,26 @@ func (v *Validator) CheckValidPostalCode(ctx context.Context, conn *database.Con
|
|||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return v.check(input, match, message), nil
|
||||
return v.Check(input, match, message), nil
|
||||
}
|
||||
|
||||
func (v *Validator) CheckPasswordConfirmation(password *Input, confirm *Input, message string) bool {
|
||||
return v.check(confirm, password.Val == confirm.Val, message)
|
||||
return v.Check(confirm, password.Val == confirm.Val, message)
|
||||
}
|
||||
|
||||
func (v *Validator) CheckSelectedOptions(field *Select, message string) bool {
|
||||
return v.check(field, field.validOptionsSelected(), message)
|
||||
return v.Check(field, field.validOptionsSelected(), message)
|
||||
}
|
||||
|
||||
func (v *Validator) CheckImageFile(field *File, message string) bool {
|
||||
return v.check(field, field.ContentType == "image/png" || field.ContentType == "image/jpeg", message)
|
||||
return v.Check(field, field.ContentType == "image/png" || field.ContentType == "image/jpeg", message)
|
||||
}
|
||||
|
||||
type field interface {
|
||||
setError(error)
|
||||
}
|
||||
|
||||
func (v *Validator) check(field field, ok bool, message string) bool {
|
||||
func (v *Validator) Check(field field, ok bool, message string) bool {
|
||||
if !ok {
|
||||
field.setError(errors.New(v.l.Get(message)))
|
||||
v.AllOK = false
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package hex
|
||||
|
||||
// xvalues returns the value of a byte as a hexadecimal digit or 255.
|
||||
var xvalues = [256]byte{
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255,
|
||||
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
}
|
||||
|
||||
func Valid(x1, x2 byte) bool {
|
||||
return xvalues[x1] != 255 && xvalues[x2] != 255
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package hex
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type test struct {
|
||||
in string
|
||||
isHex bool
|
||||
}
|
||||
|
||||
var tests = []test{
|
||||
{"f4", true},
|
||||
{"2b", true},
|
||||
{"19", true},
|
||||
{"aa", true},
|
||||
{"ff", true},
|
||||
{"00", true},
|
||||
{"bc", true},
|
||||
{"de", true},
|
||||
{"ef", true},
|
||||
{"fe", true},
|
||||
{"1g", false},
|
||||
{"gb", false},
|
||||
{"zb", false},
|
||||
{"2x", false},
|
||||
}
|
||||
|
||||
func testValid(t *testing.T, in string, isHex bool) {
|
||||
if ok := Valid(in[0], in[1]); ok != isHex {
|
||||
t.Errorf("Valid(%s) got %v expected %v", in, ok, isHex)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHex(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
testValid(t, tt.in, tt.isHex)
|
||||
testValid(t, strings.ToUpper(tt.in), tt.isHex)
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
package uuid
|
||||
|
||||
import "dev.tandem.ws/tandem/camper/pkg/hex"
|
||||
|
||||
func Valid(s string) bool {
|
||||
if len(s) != 36 {
|
||||
return false
|
||||
|
@ -19,33 +21,9 @@ func Valid(s string) bool {
|
|||
14, 16,
|
||||
19, 21,
|
||||
24, 26, 28, 30, 32, 34} {
|
||||
if !validHex(s[x], s[x+1]) {
|
||||
if !hex.Valid(s[x], s[x+1]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// xvalues returns the value of a byte as a hexadecimal digit or 255.
|
||||
var xvalues = [256]byte{
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255,
|
||||
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
}
|
||||
|
||||
func validHex(x1, x2 byte) bool {
|
||||
return xvalues[x1] != 255 && xvalues[x2] != 255
|
||||
}
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
begin;
|
||||
|
||||
drop function if exists camper.add_campsite_type(integer, text, text);
|
||||
drop function if exists camper.add_campsite_type(integer, integer, text, text);
|
||||
|
||||
commit;
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
begin;
|
||||
|
||||
drop function if exists camper.edit_campsite_type(uuid, text, text, boolean);
|
||||
drop function if exists camper.edit_campsite_type(uuid, integer, text, text, boolean);
|
||||
|
||||
commit;
|
||||
|
|
|
@ -39,7 +39,10 @@ current_company_id [roles schema_camper] 2023-08-07T10:44:36Z jordi fita mas <jo
|
|||
user_profile [roles schema_camper user company_user current_user_email current_user_cookie current_company_id] 2023-07-21T23:47:36Z jordi fita mas <jordi@tandem.blog> # Add view for user profile
|
||||
policies_company [company user_profile] 2023-08-07T20:04:26Z jordi fita mas <jordi@tandem.blog> # Add row-level security profiles to company
|
||||
change_password [roles schema_auth schema_camper user] 2023-07-21T23:54:52Z jordi fita mas <jordi@tandem.blog> # Add function to change the current user’s password
|
||||
campsite_type [roles schema_camper company user_profile] 2023-07-31T11:20:29Z jordi fita mas <jordi@tandem.blog> # Add relation of campsite type
|
||||
media_type [schema_camper] 2023-09-08T17:17:02Z jordi fita mas <jordi@tandem.blog> # Add domain for media type
|
||||
media [roles schema_camper company user_profile media_type] 2023-09-08T16:50:55Z jordi fita mas <jordi@tandem.blog> # Add relation of uploaded media
|
||||
add_media [roles schema_camper media media_type] 2023-09-08T17:40:28Z jordi fita mas <jordi@tandem.blog> # Add function to create media
|
||||
campsite_type [roles schema_camper company media user_profile] 2023-07-31T11:20:29Z jordi fita mas <jordi@tandem.blog> # Add relation of campsite type
|
||||
add_campsite_type [roles schema_camper campsite_type company] 2023-08-04T16:14:48Z jordi fita mas <jordi@tandem.blog> # Add function to create campsite types
|
||||
edit_campsite_type [roles schema_camper campsite_type company] 2023-08-07T22:21:34Z jordi fita mas <jordi@tandem.blog> # Add function to edit campsite types
|
||||
campsite [roles schema_camper company campsite_type user_profile] 2023-08-14T10:11:51Z jordi fita mas <jordi@tandem.blog> # Add campsite relation
|
||||
|
@ -53,6 +56,3 @@ to_color [roles schema_camper color] 2023-08-16T13:11:32Z jordi fita mas <jordi@
|
|||
season [roles schema_camper company user_profile] 2023-08-16T13:21:28Z jordi fita mas <jordi@tandem.blog> # Add relation of (tourist) season
|
||||
add_season [roles schema_camper season color to_integer] 2023-08-16T16:59:17Z jordi fita mas <jordi@tandem.blog> # Add function to create seasons
|
||||
edit_season [roles schema_camper season color to_integer] 2023-08-16T17:09:02Z jordi fita mas <jordi@tandem.blog> # Add function to update seasons
|
||||
media_type [schema_camper] 2023-09-08T17:17:02Z jordi fita mas <jordi@tandem.blog> # Add domain for media type
|
||||
media [roles schema_camper company user_profile media_type] 2023-09-08T16:50:55Z jordi fita mas <jordi@tandem.blog> # Add relation of uploaded media
|
||||
add_media [roles schema_camper media media_type] 2023-09-08T17:40:28Z jordi fita mas <jordi@tandem.blog> # Add function to create media
|
||||
|
|
|
@ -23,6 +23,7 @@ select function_privs_are('camper', 'add_campsite', array ['integer', 'text'], '
|
|||
set client_min_messages to warning;
|
||||
truncate campsite cascade;
|
||||
truncate campsite_type cascade;
|
||||
truncate media cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
|
@ -32,10 +33,15 @@ values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '
|
|||
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 'ca')
|
||||
;
|
||||
|
||||
insert into campsite_type (campsite_type_id, company_id, name)
|
||||
values (11, 1, 'A')
|
||||
, (12, 1, 'B')
|
||||
, (21, 2, 'C')
|
||||
insert into media (media_id, company_id, original_filename, media_type, content)
|
||||
values (3, 1, 'cover2.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
|
||||
, (4, 2, 'cover4.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
|
||||
;
|
||||
|
||||
insert into campsite_type (campsite_type_id, company_id, media_id, name)
|
||||
values (11, 1, 3, 'A')
|
||||
, (12, 1, 3, 'B')
|
||||
, (21, 2, 4, 'C')
|
||||
;
|
||||
|
||||
select lives_ok(
|
||||
|
|
|
@ -9,19 +9,20 @@ set search_path to camper, public;
|
|||
|
||||
select plan(12);
|
||||
|
||||
select has_function('camper', 'add_campsite_type', array ['integer', 'text', 'text']);
|
||||
select function_lang_is('camper', 'add_campsite_type', array ['integer', 'text', 'text'], 'sql');
|
||||
select function_returns('camper', 'add_campsite_type', array ['integer', 'text', 'text'], 'uuid');
|
||||
select isnt_definer('camper', 'add_campsite_type', array ['integer', 'text', 'text']);
|
||||
select volatility_is('camper', 'add_campsite_type', array ['integer', 'text', 'text'], 'volatile');
|
||||
select function_privs_are('camper', 'add_campsite_type', array ['integer', 'text', 'text'], 'guest', array[]::text[]);
|
||||
select function_privs_are('camper', 'add_campsite_type', array ['integer', 'text', 'text'], 'employee', array[]::text[]);
|
||||
select function_privs_are('camper', 'add_campsite_type', array ['integer', 'text', 'text'], 'admin', array['EXECUTE']);
|
||||
select function_privs_are('camper', 'add_campsite_type', array ['integer', 'text', 'text'], 'authenticator', array[]::text[]);
|
||||
select has_function('camper', 'add_campsite_type', array ['integer', 'integer', 'text', 'text']);
|
||||
select function_lang_is('camper', 'add_campsite_type', array ['integer', 'integer', 'text', 'text'], 'sql');
|
||||
select function_returns('camper', 'add_campsite_type', array ['integer', 'integer', 'text', 'text'], 'uuid');
|
||||
select isnt_definer('camper', 'add_campsite_type', array ['integer', 'integer', 'text', 'text']);
|
||||
select volatility_is('camper', 'add_campsite_type', array ['integer', 'integer', 'text', 'text'], 'volatile');
|
||||
select function_privs_are('camper', 'add_campsite_type', array ['integer', 'integer', 'text', 'text'], 'guest', array[]::text[]);
|
||||
select function_privs_are('camper', 'add_campsite_type', array ['integer', 'integer', 'text', 'text'], 'employee', array[]::text[]);
|
||||
select function_privs_are('camper', 'add_campsite_type', array ['integer', 'integer', 'text', 'text'], 'admin', array['EXECUTE']);
|
||||
select function_privs_are('camper', 'add_campsite_type', array ['integer', 'integer', 'text', 'text'], 'authenticator', array[]::text[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate campsite_type cascade;
|
||||
truncate media cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
|
@ -31,20 +32,25 @@ values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '
|
|||
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 'ca')
|
||||
;
|
||||
|
||||
insert into media (media_id, company_id, original_filename, media_type, content)
|
||||
values (3, 1, 'cover2.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
|
||||
, (4, 2, 'cover4.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
|
||||
;
|
||||
|
||||
select lives_ok(
|
||||
$$ select add_campsite_type(1, 'Type A', '<!-- block --><h2>This is what, exactly?</h2><!-- /block --><p>Dunno</p>') $$,
|
||||
$$ select add_campsite_type(1, 3, 'Type A', '<!-- block --><h2>This is what, exactly?</h2><!-- /block --><p>Dunno</p>') $$,
|
||||
'Should be able to add a campsite type to the first company'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select add_campsite_type(2, 'Type B', '') $$,
|
||||
$$ select add_campsite_type(2, 4, 'Type B', '') $$,
|
||||
'Should be able to add a campsite type to the second company'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select company_id, name, description::text, active from campsite_type $$,
|
||||
$$ values (1, 'Type A', '<!-- block --><h2>This is what, exactly?</h2><!-- /block --><p>Dunno</p>', true)
|
||||
, (2, 'Type B', '', true)
|
||||
$$ select company_id, media_id, name, description::text, active from campsite_type $$,
|
||||
$$ values (1, 3, 'Type A', '<!-- block --><h2>This is what, exactly?</h2><!-- /block --><p>Dunno</p>', true)
|
||||
, (2, 4, 'Type B', '', true)
|
||||
$$,
|
||||
'Should have added all two campsite type'
|
||||
);
|
||||
|
|
|
@ -65,6 +65,7 @@ select col_default_is('campsite', 'active', 'true');
|
|||
set client_min_messages to warning;
|
||||
truncate campsite cascade;
|
||||
truncate campsite_type cascade;
|
||||
truncate media cascade;
|
||||
truncate company_host cascade;
|
||||
truncate company_user cascade;
|
||||
truncate company cascade;
|
||||
|
@ -92,9 +93,14 @@ values (2, 'co2')
|
|||
, (4, 'co4')
|
||||
;
|
||||
|
||||
insert into campsite_type (campsite_type_id, company_id, name)
|
||||
values (22, 2, 'Wooden lodge')
|
||||
, (44, 4, 'Bungalow')
|
||||
insert into media (media_id, company_id, original_filename, media_type, content)
|
||||
values (6, 2, 'cover2.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
|
||||
, (8, 4, 'cover4.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
|
||||
;
|
||||
|
||||
insert into campsite_type (campsite_type_id, company_id, media_id, name)
|
||||
values (22, 2, 6, 'Wooden lodge')
|
||||
, (44, 4, 8, 'Bungalow')
|
||||
;
|
||||
|
||||
insert into campsite (company_id, campsite_type_id, label)
|
||||
|
|
|
@ -5,7 +5,7 @@ reset client_min_messages;
|
|||
|
||||
begin;
|
||||
|
||||
select plan(56);
|
||||
select plan(62);
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
|
@ -48,6 +48,13 @@ select col_type_is('campsite_type', 'name', 'text');
|
|||
select col_not_null('campsite_type', 'name');
|
||||
select col_hasnt_default('campsite_type', 'name');
|
||||
|
||||
select has_column('campsite_type', 'media_id');
|
||||
select col_is_fk('campsite_type', 'media_id');
|
||||
select fk_ok('campsite_type', 'media_id', 'media', 'media_id');
|
||||
select col_type_is('campsite_type', 'media_id', 'integer');
|
||||
select col_not_null('campsite_type', 'media_id');
|
||||
select col_hasnt_default('campsite_type', 'media_id');
|
||||
|
||||
select has_column('campsite_type', 'description');
|
||||
select col_type_is('campsite_type', 'description', 'xml');
|
||||
select col_not_null('campsite_type', 'description');
|
||||
|
@ -63,6 +70,7 @@ select col_default_is('campsite_type', 'active', 'true');
|
|||
|
||||
set client_min_messages to warning;
|
||||
truncate campsite_type cascade;
|
||||
truncate media cascade;
|
||||
truncate company_host cascade;
|
||||
truncate company_user cascade;
|
||||
truncate company cascade;
|
||||
|
@ -89,9 +97,14 @@ values (2, 'co2')
|
|||
, (4, 'co4')
|
||||
;
|
||||
|
||||
insert into campsite_type (company_id, name)
|
||||
values (2, 'Wooden lodge')
|
||||
, (4, 'Bungalow')
|
||||
insert into media (media_id, company_id, original_filename, media_type, content)
|
||||
values (6, 2, 'cover2.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
|
||||
, (8, 4, 'cover4.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
|
||||
;
|
||||
|
||||
insert into campsite_type (company_id, name, media_id)
|
||||
values (2, 'Wooden lodge', 6)
|
||||
, (4, 'Bungalow', 8)
|
||||
;
|
||||
|
||||
prepare campsite_type_data as
|
||||
|
@ -112,7 +125,7 @@ reset role;
|
|||
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2');
|
||||
|
||||
select lives_ok(
|
||||
$$ insert into campsite_type(company_id, name) values (2, 'Another type' ) $$,
|
||||
$$ insert into campsite_type(company_id, name, media_id) values (2, 'Another type', 6) $$,
|
||||
'Admin from company 2 should be able to insert a new campsite type to that company.'
|
||||
);
|
||||
|
||||
|
@ -153,7 +166,7 @@ select bag_eq(
|
|||
);
|
||||
|
||||
select throws_ok(
|
||||
$$ insert into campsite_type (company_id, name) values (4, 'Another type' ) $$,
|
||||
$$ insert into campsite_type (company_id, name, media_id) values (4, 'Another type', 6) $$,
|
||||
'42501', 'new row violates row-level security policy for table "campsite_type"',
|
||||
'Admin from company 2 should NOT be able to insert new campsite types to company 4.'
|
||||
);
|
||||
|
@ -191,7 +204,7 @@ select bag_eq(
|
|||
);
|
||||
|
||||
select throws_ok(
|
||||
$$ insert into campsite_type (company_id, name) values (2, ' ' ) $$,
|
||||
$$ insert into campsite_type (company_id, name, media_id) values (2, ' ', 6) $$,
|
||||
'23514', 'new row for relation "campsite_type" violates check constraint "name_not_empty"',
|
||||
'Should not be able to insert campsite types with a blank name.'
|
||||
);
|
||||
|
|
|
@ -22,6 +22,7 @@ select function_privs_are('camper', 'edit_campsite', array ['uuid', 'integer', '
|
|||
set client_min_messages to warning;
|
||||
truncate campsite cascade;
|
||||
truncate campsite_type cascade;
|
||||
truncate media cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
|
@ -30,10 +31,14 @@ insert into company (company_id, business_name, vatin, trade_name, phone, email,
|
|||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
|
||||
;
|
||||
|
||||
insert into campsite_type (campsite_type_id, company_id, name)
|
||||
values (11, 1, 'Type A')
|
||||
, (12, 1, 'Type B')
|
||||
, (13, 1, 'Type C')
|
||||
insert into media (media_id, company_id, original_filename, media_type, content)
|
||||
values (3, 1, 'cover2.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
|
||||
;
|
||||
|
||||
insert into campsite_type (campsite_type_id, company_id, media_id, name)
|
||||
values (11, 1, 3, 'Type A')
|
||||
, (12, 1, 3, 'Type B')
|
||||
, (13, 1, 3, 'Type C')
|
||||
;
|
||||
|
||||
insert into campsite (company_id, campsite_type_id, slug, label, active)
|
||||
|
|
|
@ -9,18 +9,19 @@ set search_path to camper, public;
|
|||
|
||||
select plan(12);
|
||||
|
||||
select has_function('camper', 'edit_campsite_type', array ['uuid', 'text', 'text', 'boolean']);
|
||||
select function_lang_is('camper', 'edit_campsite_type', array ['uuid', 'text', 'text', 'boolean'], 'sql');
|
||||
select function_returns('camper', 'edit_campsite_type', array ['uuid', 'text', 'text', 'boolean'], 'uuid');
|
||||
select isnt_definer('camper', 'edit_campsite_type', array ['uuid', 'text', 'text', 'boolean']);
|
||||
select volatility_is('camper', 'edit_campsite_type', array ['uuid', 'text', 'text', 'boolean'], 'volatile');
|
||||
select function_privs_are('camper', 'edit_campsite_type', array ['uuid', 'text', 'text', 'boolean'], 'guest', array[]::text[]);
|
||||
select function_privs_are('camper', 'edit_campsite_type', array ['uuid', 'text', 'text', 'boolean'], 'employee', array[]::text[]);
|
||||
select function_privs_are('camper', 'edit_campsite_type', array ['uuid', 'text', 'text', 'boolean'], 'admin', array['EXECUTE']);
|
||||
select function_privs_are('camper', 'edit_campsite_type', array ['uuid', 'text', 'text', 'boolean'], 'authenticator', array[]::text[]);
|
||||
select has_function('camper', 'edit_campsite_type', array ['uuid', 'integer', 'text', 'text', 'boolean']);
|
||||
select function_lang_is('camper', 'edit_campsite_type', array ['uuid', 'integer', 'text', 'text', 'boolean'], 'sql');
|
||||
select function_returns('camper', 'edit_campsite_type', array ['uuid', 'integer', 'text', 'text', 'boolean'], 'uuid');
|
||||
select isnt_definer('camper', 'edit_campsite_type', array ['uuid', 'integer', 'text', 'text', 'boolean']);
|
||||
select volatility_is('camper', 'edit_campsite_type', array ['uuid', 'integer', 'text', 'text', 'boolean'], 'volatile');
|
||||
select function_privs_are('camper', 'edit_campsite_type', array ['uuid', 'integer', 'text', 'text', 'boolean'], 'guest', array[]::text[]);
|
||||
select function_privs_are('camper', 'edit_campsite_type', array ['uuid', 'integer', 'text', 'text', 'boolean'], 'employee', array[]::text[]);
|
||||
select function_privs_are('camper', 'edit_campsite_type', array ['uuid', 'integer', 'text', 'text', 'boolean'], 'admin', array['EXECUTE']);
|
||||
select function_privs_are('camper', 'edit_campsite_type', array ['uuid', 'integer', 'text', 'text', 'boolean'], 'authenticator', array[]::text[]);
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate campsite_type cascade;
|
||||
truncate media cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
|
@ -29,25 +30,31 @@ insert into company (company_id, business_name, vatin, trade_name, phone, email,
|
|||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
|
||||
;
|
||||
|
||||
insert into campsite_type (company_id, slug, name, description, active)
|
||||
values (1, '87452b88-b48f-48d3-bb6c-0296de64164e', 'Type A', '<p>A</p>', true)
|
||||
, (1, '9b6370f7-f941-46f2-bc6e-de455675bd0a', 'Type B', '<p>B</p>', false)
|
||||
insert into media (media_id, company_id, original_filename, media_type, content)
|
||||
values (2, 1, 'cover2.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
|
||||
, (3, 1, 'cover3.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ff00ff","a"};')
|
||||
, (4, 1, 'cover4.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffff00","a"};')
|
||||
;
|
||||
|
||||
insert into campsite_type (company_id, slug, media_id, name, description, active)
|
||||
values (1, '87452b88-b48f-48d3-bb6c-0296de64164e', 2, 'Type A', '<p>A</p>', true)
|
||||
, (1, '9b6370f7-f941-46f2-bc6e-de455675bd0a', 3, 'Type B', '<p>B</p>', false)
|
||||
;
|
||||
|
||||
select lives_ok(
|
||||
$$ select edit_campsite_type('87452b88-b48f-48d3-bb6c-0296de64164e', 'Type 1', '<p>1</p>', false) $$,
|
||||
$$ select edit_campsite_type('87452b88-b48f-48d3-bb6c-0296de64164e', 4, 'Type 1', '<p>1</p>', false) $$,
|
||||
'Should be able to edit the first type'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select edit_campsite_type('9b6370f7-f941-46f2-bc6e-de455675bd0a', 'Type 2', '<p>2</p>', true) $$,
|
||||
$$ select edit_campsite_type('9b6370f7-f941-46f2-bc6e-de455675bd0a', null, 'Type 2', '<p>2</p>', true) $$,
|
||||
'Should be able to edit the second type'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select slug::text, name, description::text, active from campsite_type $$,
|
||||
$$ values ('87452b88-b48f-48d3-bb6c-0296de64164e', 'Type 1', '<p>1</p>', false)
|
||||
, ('9b6370f7-f941-46f2-bc6e-de455675bd0a', 'Type 2', '<p>2</p>', true)
|
||||
$$ select slug::text, media_id, name, description::text, active from campsite_type $$,
|
||||
$$ values ('87452b88-b48f-48d3-bb6c-0296de64164e', 4, 'Type 1', '<p>1</p>', false)
|
||||
, ('9b6370f7-f941-46f2-bc6e-de455675bd0a', 3, 'Type 2', '<p>2</p>', true)
|
||||
$$,
|
||||
'Should have updated all campsite types.'
|
||||
);
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('camper.add_campsite_type(integer, text, text)', 'execute');
|
||||
select has_function_privilege('camper.add_campsite_type(integer, integer, text, text)', 'execute');
|
||||
|
||||
rollback;
|
||||
|
|
|
@ -6,6 +6,7 @@ select campsite_type_id
|
|||
, company_id
|
||||
, slug
|
||||
, name
|
||||
, media_id
|
||||
, description
|
||||
, active
|
||||
from camper.campsite_type
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('camper.edit_campsite_type(uuid, text, text, boolean)', 'execute');
|
||||
select has_function_privilege('camper.edit_campsite_type(uuid, integer, text, text, boolean)', 'execute');
|
||||
|
||||
rollback;
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 124 KiB |
|
@ -15,6 +15,7 @@
|
|||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite/types.typeForm*/ -}}
|
||||
{{ template "settings-tabs" "campsiteTypes" }}
|
||||
<form
|
||||
enctype="multipart/form-data"
|
||||
{{ if .Slug }}
|
||||
data-hx-put="/admin/campsites/types/{{ .Slug }}"
|
||||
{{ else }}
|
||||
|
@ -50,6 +51,18 @@
|
|||
</label>
|
||||
{{ template "error-message" . }}
|
||||
{{- end }}
|
||||
{{ with .Media -}}
|
||||
{{ if .Val -}}
|
||||
<img src="/media/{{ .Val }}" alt="">
|
||||
{{- end }}
|
||||
<label>
|
||||
{{( pgettext "Cover image" "input" )}}
|
||||
<input type="file" name="{{ .Name }}"
|
||||
{{ if not $.Slug }}required{{ end }}
|
||||
{{ template "error-attrs" . }}><br>
|
||||
</label>
|
||||
{{ template "error-message" . }}
|
||||
{{- end }}
|
||||
{{ with .Description -}}
|
||||
<label>
|
||||
{{( pgettext "Description" "input")}}<br>
|
||||
|
|
|
@ -11,15 +11,16 @@
|
|||
{{- end }}
|
||||
|
||||
{{ define "content" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/app.homePage*/ -}}
|
||||
<section class="nature">
|
||||
<div style="--background-image:url('/static/images/todd-trapani-5LHzBpiTuzQ-unsplash.jpg')">
|
||||
<h2>{{ (gettext "The pleasure of camping in the middle of nature…")}}</h2>
|
||||
<a href="/{{ currentLocale }}/reservation">{{( pgettext "Booking" "link" )}} <span>→</span></a>
|
||||
</div>
|
||||
{{ with .Menu.CampsiteTypes -}}
|
||||
{{ with .CampsiteTypes -}}
|
||||
<div>
|
||||
{{ range . -}}
|
||||
<section style="--background-image:url('/static/images/camping_montagut_acampada_tenda.jpg')">
|
||||
<section style="--background-image:url('{{ .Media }}')">
|
||||
<h3><a href="{{ .HRef }}"><span>{{ .Label }}</span></a></h3>
|
||||
</section>
|
||||
{{- end }}
|
||||
|
|
Loading…
Reference in New Issue