diff --git a/cmd/camper/main.go b/cmd/camper/main.go index c18cdfa..04040bb 100644 --- a/cmd/camper/main.go +++ b/cmd/camper/main.go @@ -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) } diff --git a/deploy/add_campsite_type.sql b/deploy/add_campsite_type.sql index 138eb3e..7472829 100644 --- a/deploy/add_campsite_type.sql +++ b/deploy/add_campsite_type.sql @@ -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; diff --git a/deploy/campsite_type.sql b/deploy/campsite_type.sql index 9ce481f..54674d6 100644 --- a/deploy/campsite_type.sql +++ b/deploy/campsite_type.sql @@ -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 ); diff --git a/deploy/edit_campsite_type.sql b/deploy/edit_campsite_type.sql index 2315986..a22c4f5 100644 --- a/deploy/edit_campsite_type.sql +++ b/deploy/edit_campsite_type.sql @@ -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; diff --git a/pkg/app/app.go b/pkg/app/app.go index 94bbb41..2b1d545 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -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 { diff --git a/pkg/app/media.go b/pkg/app/media.go new file mode 100644 index 0000000..a504fd6 --- /dev/null +++ b/pkg/app/media.go @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * 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 +} diff --git a/pkg/app/media_test.go b/pkg/app/media_test.go new file mode 100644 index 0000000..8b93ff6 --- /dev/null +++ b/pkg/app/media_test.go @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * 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) + } +} diff --git a/pkg/app/public.go b/pkg/app/public.go index fa7bd8e..a67abd5 100644 --- a/pkg/app/public.go +++ b/pkg/app/public.go @@ -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 +} diff --git a/pkg/campsite/types/admin.go b/pkg/campsite/types/admin.go index 80600d9..f113064 100644 --- a/pkg/campsite/types/admin.go +++ b/pkg/campsite/types/admin.go @@ -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 +} diff --git a/pkg/database/db.go b/pkg/database/db.go index abbbc96..3a91028 100644 --- a/pkg/database/db.go +++ b/pkg/database/db.go @@ -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 +} diff --git a/pkg/database/tx.go b/pkg/database/tx.go new file mode 100644 index 0000000..fad6cdc --- /dev/null +++ b/pkg/database/tx.go @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * 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) + } +} diff --git a/pkg/form/file.go b/pkg/form/file.go index 4160564..29eb658 100644 --- a/pkg/form/file.go +++ b/pkg/form/file.go @@ -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 diff --git a/pkg/form/validator.go b/pkg/form/validator.go index 0fa46db..abdeb54 100644 --- a/pkg/form/validator.go +++ b/pkg/form/validator.go @@ -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 diff --git a/pkg/hex/hex.go b/pkg/hex/hex.go new file mode 100644 index 0000000..a0ce2d8 --- /dev/null +++ b/pkg/hex/hex.go @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * 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 +} diff --git a/pkg/hex/hex_test.go b/pkg/hex/hex_test.go new file mode 100644 index 0000000..6298b30 --- /dev/null +++ b/pkg/hex/hex_test.go @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * 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) + } +} diff --git a/pkg/uuid/uuid.go b/pkg/uuid/uuid.go index 3a06145..84dfd9e 100644 --- a/pkg/uuid/uuid.go +++ b/pkg/uuid/uuid.go @@ -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 -} diff --git a/revert/add_campsite_type.sql b/revert/add_campsite_type.sql index 2a80e4f..88a5efc 100644 --- a/revert/add_campsite_type.sql +++ b/revert/add_campsite_type.sql @@ -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; diff --git a/revert/edit_campsite_type.sql b/revert/edit_campsite_type.sql index ab3ba75..82b461d 100644 --- a/revert/edit_campsite_type.sql +++ b/revert/edit_campsite_type.sql @@ -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; diff --git a/sqitch.plan b/sqitch.plan index ad4b1de..2495afe 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -39,7 +39,10 @@ current_company_id [roles schema_camper] 2023-08-07T10:44:36Z jordi fita mas # Add view for user profile policies_company [company user_profile] 2023-08-07T20:04:26Z jordi fita mas # Add row-level security profiles to company change_password [roles schema_auth schema_camper user] 2023-07-21T23:54:52Z jordi fita mas # 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 # Add relation of campsite type +media_type [schema_camper] 2023-09-08T17:17:02Z jordi fita mas # Add domain for media type +media [roles schema_camper company user_profile media_type] 2023-09-08T16:50:55Z jordi fita mas # Add relation of uploaded media +add_media [roles schema_camper media media_type] 2023-09-08T17:40:28Z jordi fita mas # Add function to create media +campsite_type [roles schema_camper company media user_profile] 2023-07-31T11:20:29Z jordi fita mas # Add relation of campsite type add_campsite_type [roles schema_camper campsite_type company] 2023-08-04T16:14:48Z jordi fita mas # Add function to create campsite types edit_campsite_type [roles schema_camper campsite_type company] 2023-08-07T22:21:34Z jordi fita mas # Add function to edit campsite types campsite [roles schema_camper company campsite_type user_profile] 2023-08-14T10:11:51Z jordi fita mas # Add campsite relation @@ -53,6 +56,3 @@ to_color [roles schema_camper color] 2023-08-16T13:11:32Z jordi fita mas # Add relation of (tourist) season add_season [roles schema_camper season color to_integer] 2023-08-16T16:59:17Z jordi fita mas # Add function to create seasons edit_season [roles schema_camper season color to_integer] 2023-08-16T17:09:02Z jordi fita mas # Add function to update seasons -media_type [schema_camper] 2023-09-08T17:17:02Z jordi fita mas # Add domain for media type -media [roles schema_camper company user_profile media_type] 2023-09-08T16:50:55Z jordi fita mas # Add relation of uploaded media -add_media [roles schema_camper media media_type] 2023-09-08T17:40:28Z jordi fita mas # Add function to create media diff --git a/test/add_campsite.sql b/test/add_campsite.sql index 85734fa..075c81b 100644 --- a/test/add_campsite.sql +++ b/test/add_campsite.sql @@ -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( diff --git a/test/add_campsite_type.sql b/test/add_campsite_type.sql index 0d5be2d..4b91d29 100644 --- a/test/add_campsite_type.sql +++ b/test/add_campsite_type.sql @@ -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', '

This is what, exactly?

Dunno

') $$, + $$ select add_campsite_type(1, 3, 'Type A', '

This is what, exactly?

Dunno

') $$, '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', '

This is what, exactly?

Dunno

', true) - , (2, 'Type B', '', true) + $$ select company_id, media_id, name, description::text, active from campsite_type $$, + $$ values (1, 3, 'Type A', '

This is what, exactly?

Dunno

', true) + , (2, 4, 'Type B', '', true) $$, 'Should have added all two campsite type' ); diff --git a/test/campsite.sql b/test/campsite.sql index 2dd4232..b1053e4 100644 --- a/test/campsite.sql +++ b/test/campsite.sql @@ -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) diff --git a/test/campsite_type.sql b/test/campsite_type.sql index e7892a0..1598ab0 100644 --- a/test/campsite_type.sql +++ b/test/campsite_type.sql @@ -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.' ); diff --git a/test/edit_campsite.sql b/test/edit_campsite.sql index 29427e0..8c13e15 100644 --- a/test/edit_campsite.sql +++ b/test/edit_campsite.sql @@ -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) diff --git a/test/edit_campsite_type.sql b/test/edit_campsite_type.sql index 395cd73..2963ada 100644 --- a/test/edit_campsite_type.sql +++ b/test/edit_campsite_type.sql @@ -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', '

A

', true) - , (1, '9b6370f7-f941-46f2-bc6e-de455675bd0a', 'Type B', '

B

', 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', '

A

', true) + , (1, '9b6370f7-f941-46f2-bc6e-de455675bd0a', 3, 'Type B', '

B

', false) ; select lives_ok( - $$ select edit_campsite_type('87452b88-b48f-48d3-bb6c-0296de64164e', 'Type 1', '

1

', false) $$, + $$ select edit_campsite_type('87452b88-b48f-48d3-bb6c-0296de64164e', 4, 'Type 1', '

1

', false) $$, 'Should be able to edit the first type' ); select lives_ok( - $$ select edit_campsite_type('9b6370f7-f941-46f2-bc6e-de455675bd0a', 'Type 2', '

2

', true) $$, + $$ select edit_campsite_type('9b6370f7-f941-46f2-bc6e-de455675bd0a', null, 'Type 2', '

2

', 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', '

1

', false) - , ('9b6370f7-f941-46f2-bc6e-de455675bd0a', 'Type 2', '

2

', true) + $$ select slug::text, media_id, name, description::text, active from campsite_type $$, + $$ values ('87452b88-b48f-48d3-bb6c-0296de64164e', 4, 'Type 1', '

1

', false) + , ('9b6370f7-f941-46f2-bc6e-de455675bd0a', 3, 'Type 2', '

2

', true) $$, 'Should have updated all campsite types.' ); diff --git a/verify/add_campsite_type.sql b/verify/add_campsite_type.sql index 28ba0f7..a62f1a3 100644 --- a/verify/add_campsite_type.sql +++ b/verify/add_campsite_type.sql @@ -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; diff --git a/verify/campsite_type.sql b/verify/campsite_type.sql index db74927..7c76034 100644 --- a/verify/campsite_type.sql +++ b/verify/campsite_type.sql @@ -6,6 +6,7 @@ select campsite_type_id , company_id , slug , name + , media_id , description , active from camper.campsite_type diff --git a/verify/edit_campsite_type.sql b/verify/edit_campsite_type.sql index 6a82ba0..76f1e4c 100644 --- a/verify/edit_campsite_type.sql +++ b/verify/edit_campsite_type.sql @@ -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; diff --git a/web/static/images/camping_montagut_acampada_tenda.jpg b/web/static/images/camping_montagut_acampada_tenda.jpg deleted file mode 100644 index ca73dab..0000000 Binary files a/web/static/images/camping_montagut_acampada_tenda.jpg and /dev/null differ diff --git a/web/templates/admin/campsite/type/form.gohtml b/web/templates/admin/campsite/type/form.gohtml index 8758369..5894b77 100644 --- a/web/templates/admin/campsite/type/form.gohtml +++ b/web/templates/admin/campsite/type/form.gohtml @@ -15,6 +15,7 @@ {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite/types.typeForm*/ -}} {{ template "settings-tabs" "campsiteTypes" }}
{{ template "error-message" . }} {{- end }} + {{ with .Media -}} + {{ if .Val -}} + + {{- end }} + + {{ template "error-message" . }} + {{- end }} {{ with .Description -}}