Make home page’s carousel manageable via the database
I debated with myself whether to create the home_carousel relation or rather if it would be better to have a single carousel relation for all pages. However, i thought that it would be actually harder to maintain a single relation because i would need an additional column to tell one carrousel from another, and what would that column be? An enum? A foreign key to another relation? home_carousel carries no such issues. I was starting to duplicate logic all over the packages, such as the way to encode media paths or “localization” (l10n) input fields. Therefore, i refactorized them. In the case of media path, i added a function that accepts rows of media, because always need the same columns from the row, and it was yet another repetition if i needed to pass them all the time. Plus, these kind of functions can be called as `table.function`, that make them look like columns from the table; if PostgreSQL implemented virtual generated columns, i would have used that instead. I am not sure whether that media_path function can be immutable. An immutable function is “guaranteed to return the same results given the same arguments forever”, which would be true if the inputs where the hash and the original_filename columns, instead of the whole rows, but i left it as static because i did not know whether PostgreSQL interprets the “same row but with different values” as a different input. That is, whether PostgreSQL’s concept of row is the actual tuple or the space that has a rowid, irrespective of contents; in the latter case, the function can not be immutable. Just to be in the safe side, i left it stable. The home page was starting to grow a bit too much inside the app package, new that it has its own admin handler, and moved it all to a separate package.
|
@ -29,6 +29,34 @@ values (52, 'plots.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/plo
|
|||
, (52, 'safari_tents.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/safari_tents.avif]])', 'base64'))
|
||||
, (52, 'bungalows.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/bungalows.avif]])', 'base64'))
|
||||
, (52, 'wooden_lodges.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/wooden_lodges.avif]])', 'base64'))
|
||||
, (52, 'home_carousel0.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel0.jpg]])', 'base64'))
|
||||
, (52, 'home_carousel1.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel1.jpg]])', 'base64'))
|
||||
, (52, 'home_carousel2.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel2.jpg]])', 'base64'))
|
||||
, (52, 'home_carousel3.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel3.jpg]])', 'base64'))
|
||||
, (52, 'home_carousel4.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel4.jpg]])', 'base64'))
|
||||
, (52, 'home_carousel5.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel5.jpg]])', 'base64'))
|
||||
, (52, 'home_carousel6.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel6.jpg]])', 'base64'))
|
||||
, (52, 'home_carousel7.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel7.jpg]])', 'base64'))
|
||||
, (52, 'home_carousel8.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel8.jpg]])', 'base64'))
|
||||
;
|
||||
|
||||
insert into home_carousel (media_id, caption)
|
||||
values (66, 'Volcà de Santa Margarida')
|
||||
, (67, 'Gorga fosca Sadernes')
|
||||
, (68, 'Castellfollit de la Roca')
|
||||
, (69, 'Besalú')
|
||||
, (70, 'Santa Pau')
|
||||
, (71, 'Banyoles')
|
||||
, (72, 'Girona')
|
||||
, (73, 'Costa Brava')
|
||||
, (74, 'Barcelona')
|
||||
;
|
||||
|
||||
insert into home_carousel_i18n (media_id, lang_tag, caption)
|
||||
values (66, 'en', 'Santa Margarida volcano')
|
||||
, (66, 'es', 'Volcán de Santa Margarida')
|
||||
, (67, 'en', 'Sadernes dark gorge')
|
||||
, (67, 'es', 'Piletón oscuro Sadernes')
|
||||
;
|
||||
|
||||
alter sequence campsite_type_campsite_type_id_seq restart with 72;
|
||||
|
|
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 311 KiB |
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 253 KiB |
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 202 KiB |
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 315 KiB |
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 194 KiB |
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 336 KiB |
|
@ -0,0 +1,25 @@
|
|||
-- Deploy camper:add_home_carousel_slide to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: home_carousel
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function add_home_carousel_slide(media_id integer, caption text) returns integer as
|
||||
$$
|
||||
insert into home_carousel (media_id, caption)
|
||||
values (media_id, coalesce(caption, ''))
|
||||
on conflict (media_id) do update
|
||||
set caption = excluded.caption
|
||||
returning media_id
|
||||
;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function add_home_carousel_slide(integer, text) from public;
|
||||
grant execute on function add_home_carousel_slide(integer, text) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,53 @@
|
|||
-- Deploy camper:home_carousel to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_public
|
||||
-- requires: company
|
||||
-- requires: media
|
||||
-- requires: user_profile
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table home_carousel (
|
||||
media_id integer not null references media primary key,
|
||||
caption text not null
|
||||
);
|
||||
|
||||
grant select on table home_carousel to guest;
|
||||
grant select on table home_carousel to employee;
|
||||
grant select, insert, update, delete on table home_carousel to admin;
|
||||
|
||||
alter table home_carousel enable row level security;
|
||||
|
||||
create policy guest_ok
|
||||
on home_carousel
|
||||
for select
|
||||
using (true)
|
||||
;
|
||||
|
||||
create policy insert_to_company
|
||||
on home_carousel
|
||||
for insert
|
||||
with check (
|
||||
exists (select 1 from media join user_profile using (company_id) where media.media_id = home_carousel.media_id)
|
||||
)
|
||||
;
|
||||
|
||||
create policy update_company
|
||||
on home_carousel
|
||||
for update
|
||||
using (
|
||||
exists (select 1 from media join user_profile using (company_id) where media.media_id = home_carousel.media_id)
|
||||
)
|
||||
;
|
||||
|
||||
create policy delete_from_company
|
||||
on home_carousel
|
||||
for delete
|
||||
using (
|
||||
exists (select 1 from media join user_profile using (company_id) where media.media_id = home_carousel.media_id)
|
||||
)
|
||||
;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,22 @@
|
|||
-- Deploy camper:home_carousel_i18n to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: home_carousel
|
||||
-- requires: language
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table home_carousel_i18n (
|
||||
media_id integer not null references home_carousel,
|
||||
lang_tag text not null references language,
|
||||
caption text not null,
|
||||
primary key (media_id, lang_tag)
|
||||
);
|
||||
|
||||
grant select on table home_carousel_i18n to guest;
|
||||
grant select on table home_carousel_i18n to employee;
|
||||
grant select, insert, update, delete on table home_carousel_i18n to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,23 @@
|
|||
-- Deploy camper:media_path to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: media
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function path(media) returns text as
|
||||
$$
|
||||
select '/media/' || encode($1.hash, 'hex') || '/' || $1.original_filename;
|
||||
$$
|
||||
language sql
|
||||
stable
|
||||
;
|
||||
|
||||
revoke execute on function path(media) from public;
|
||||
grant execute on function path(media) to guest;
|
||||
grant execute on function path(media) to employee;
|
||||
grant execute on function path(media) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,22 @@
|
|||
-- Deploy camper:remove_home_carousel_slide to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: home_carousel
|
||||
-- requires: home_carousel_i18n
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function remove_home_carousel_slide (media_id integer) returns void as
|
||||
$$
|
||||
delete from home_carousel_i18n where media_id = $1;
|
||||
delete from home_carousel where media_id = $1;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function remove_home_carousel_slide (integer) from public;
|
||||
grant execute on function remove_home_carousel_slide (integer) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,23 @@
|
|||
-- Deploy camper:translate_home_carousel_slide to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: home_carousel_i18n
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function translate_home_carousel_slide (media_id integer, lang_tag text, caption text) returns void as
|
||||
$$
|
||||
insert into home_carousel_i18n (media_id, lang_tag, caption)
|
||||
values (media_id, lang_tag, coalesce(caption, ''))
|
||||
on conflict (media_id, lang_tag) do update
|
||||
set caption = excluded.caption
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function translate_home_carousel_slide(integer, text, text) from public;
|
||||
grant execute on function translate_home_carousel_slide(integer, text, text) to admin;
|
||||
|
||||
commit;
|
|
@ -12,6 +12,7 @@ import (
|
|||
"dev.tandem.ws/tandem/camper/pkg/campsite"
|
||||
"dev.tandem.ws/tandem/camper/pkg/company"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
"dev.tandem.ws/tandem/camper/pkg/home"
|
||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||
"dev.tandem.ws/tandem/camper/pkg/season"
|
||||
|
@ -21,6 +22,7 @@ import (
|
|||
type adminHandler struct {
|
||||
campsite *campsite.AdminHandler
|
||||
company *company.AdminHandler
|
||||
home *home.AdminHandler
|
||||
season *season.AdminHandler
|
||||
}
|
||||
|
||||
|
@ -28,6 +30,7 @@ func newAdminHandler(locales locale.Locales) *adminHandler {
|
|||
return &adminHandler{
|
||||
campsite: campsite.NewAdminHandler(locales),
|
||||
company: company.NewAdminHandler(),
|
||||
home: home.NewAdminHandler(locales),
|
||||
season: season.NewAdminHandler(),
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +55,8 @@ func (h *adminHandler) Handle(user *auth.User, company *auth.Company, conn *data
|
|||
h.campsite.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "company":
|
||||
h.company.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "home":
|
||||
h.home.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "seasons":
|
||||
h.season.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "":
|
||||
|
|
|
@ -6,23 +6,23 @@
|
|||
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"
|
||||
"dev.tandem.ws/tandem/camper/pkg/home"
|
||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||
"dev.tandem.ws/tandem/camper/pkg/template"
|
||||
)
|
||||
|
||||
type publicHandler struct {
|
||||
home *home.PublicHandler
|
||||
campsite *campsite.PublicHandler
|
||||
}
|
||||
|
||||
func newPublicHandler() *publicHandler {
|
||||
return &publicHandler{
|
||||
home: home.NewPublicHandler(),
|
||||
campsite: campsite.NewPublicHandler(),
|
||||
}
|
||||
}
|
||||
|
@ -33,8 +33,7 @@ func (h *publicHandler) Handler(user *auth.User, company *auth.Company, conn *da
|
|||
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||||
switch head {
|
||||
case "":
|
||||
home := newHomePage()
|
||||
home.MustRender(w, r, user, company, conn)
|
||||
h.home.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "campsites":
|
||||
h.campsite.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
default:
|
||||
|
@ -42,58 +41,3 @@ 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(), 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 coalesce(i18n.name, campsite_type.name) as l10_name
|
||||
, '/campsites/types/' || slug
|
||||
, '/media/' || encode(hash, 'hex') || '/' || original_filename
|
||||
from campsite_type
|
||||
left join campsite_type_i18n as i18n on campsite_type.campsite_type_id = i18n.campsite_type_id and lang_tag = $1
|
||||
join media using (media_id)
|
||||
where campsite_type.company_id = $2
|
||||
and campsite_type.active
|
||||
`, loc.Language, 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
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/jackc/pgx/v4"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
|
@ -66,12 +65,12 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
|
|||
}
|
||||
panic(err)
|
||||
}
|
||||
h.TypeHandler(user, company, conn, f).ServeHTTP(w, r)
|
||||
h.typeHandler(user, company, conn, f).ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) TypeHandler(user *auth.User, company *auth.Company, conn *database.Conn, f *typeForm) http.Handler {
|
||||
func (h *AdminHandler) typeHandler(user *auth.User, company *auth.Company, conn *database.Conn, f *typeForm) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var head string
|
||||
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||||
|
@ -87,18 +86,13 @@ func (h *AdminHandler) TypeHandler(user *auth.User, company *auth.Company, conn
|
|||
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
|
||||
}
|
||||
default:
|
||||
tag, err := language.Parse(head)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
loc, ok := h.locales[tag]
|
||||
loc, ok := h.locales.Get(head)
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
l10n := newTypeL10nForm(f, loc)
|
||||
if err = l10n.FillFromDatabase(r.Context(), conn); err != nil {
|
||||
if err := l10n.FillFromDatabase(r.Context(), conn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch r.Method {
|
||||
|
@ -267,7 +261,7 @@ func (f *typeForm) FillFromDatabase(ctx context.Context, conn *database.Conn, sl
|
|||
row := conn.QueryRow(ctx, `
|
||||
select name
|
||||
, description
|
||||
, encode(hash, 'hex')
|
||||
, media.path
|
||||
, active
|
||||
from campsite_type
|
||||
join media using (media_id)
|
||||
|
|
|
@ -17,34 +17,19 @@ import (
|
|||
"dev.tandem.ws/tandem/camper/pkg/template"
|
||||
)
|
||||
|
||||
type L10nInput struct {
|
||||
form.Input
|
||||
Source string
|
||||
}
|
||||
|
||||
type typeL10nForm struct {
|
||||
Locale *locale.Locale
|
||||
Slug string
|
||||
Name *L10nInput
|
||||
Description *L10nInput
|
||||
Name *form.L10nInput
|
||||
Description *form.L10nInput
|
||||
}
|
||||
|
||||
func newTypeL10nForm(f *typeForm, loc *locale.Locale) *typeL10nForm {
|
||||
return &typeL10nForm{
|
||||
Locale: loc,
|
||||
Slug: f.Slug,
|
||||
Name: &L10nInput{
|
||||
Input: form.Input{
|
||||
Name: f.Name.Name,
|
||||
},
|
||||
Source: f.Name.Val,
|
||||
},
|
||||
Description: &L10nInput{
|
||||
Input: form.Input{
|
||||
Name: f.Description.Name,
|
||||
},
|
||||
Source: f.Description.Val,
|
||||
},
|
||||
Locale: loc,
|
||||
Slug: f.Slug,
|
||||
Name: f.Name.L10nInput(),
|
||||
Description: f.Description.L10nInput(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,3 +27,17 @@ func (input *Input) FillValue(r *http.Request) {
|
|||
func (input *Input) Value() (driver.Value, error) {
|
||||
return input.Val, nil
|
||||
}
|
||||
|
||||
func (input *Input) L10nInput() *L10nInput {
|
||||
return &L10nInput{
|
||||
Input: Input{
|
||||
Name: input.Name,
|
||||
},
|
||||
Source: input.Val,
|
||||
}
|
||||
}
|
||||
|
||||
type L10nInput struct {
|
||||
Input
|
||||
Source string
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package home
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"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"
|
||||
)
|
||||
|
||||
type AdminHandler struct {
|
||||
locales locale.Locales
|
||||
}
|
||||
|
||||
func NewAdminHandler(locales locale.Locales) *AdminHandler {
|
||||
return &AdminHandler{locales}
|
||||
}
|
||||
|
||||
func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var head string
|
||||
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||||
|
||||
switch head {
|
||||
case "":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
serveHomeIndex(w, r, user, company, conn)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||
}
|
||||
case "slides":
|
||||
h.carouselHandler(user, company, conn).ServeHTTP(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func serveHomeIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||
slides, err := collectSlideEntries(r.Context(), company, conn)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
page := &homeIndex{
|
||||
Slides: slides,
|
||||
}
|
||||
page.MustRender(w, r, user, company)
|
||||
}
|
||||
|
||||
type homeIndex struct {
|
||||
Slides []*slideEntry
|
||||
}
|
||||
|
||||
func (page *homeIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||
template.MustRenderAdmin(w, r, user, company, "home/index.gohtml", page)
|
||||
}
|
|
@ -0,0 +1,311 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package home
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/jackc/pgx/v4"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
"dev.tandem.ws/tandem/camper/pkg/form"
|
||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||
"dev.tandem.ws/tandem/camper/pkg/template"
|
||||
)
|
||||
|
||||
func (h *AdminHandler) carouselHandler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var head string
|
||||
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||||
|
||||
switch head {
|
||||
case "":
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
addSlide(w, r, user, company, conn)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||
}
|
||||
case "new":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
f := newSlideForm()
|
||||
f.MustRender(w, r, user, company)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||
}
|
||||
default:
|
||||
id, err := strconv.Atoi(head)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
f := newSlideForm()
|
||||
if err := f.FillFromDatabase(r.Context(), conn, id); err != nil {
|
||||
if database.ErrorIsNotFound(err) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var langTag string
|
||||
langTag, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||||
|
||||
switch langTag {
|
||||
case "":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
f.MustRender(w, r, user, company)
|
||||
case http.MethodPut:
|
||||
editSlide(w, r, user, company, conn, f)
|
||||
case http.MethodDelete:
|
||||
deleteSlide(w, r, user, conn, id)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
|
||||
}
|
||||
default:
|
||||
loc, ok := h.locales.Get(langTag)
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
l10n := newSlideL10nForm(f, loc)
|
||||
if err := l10n.FillFromDatabase(r.Context(), conn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
l10n.MustRender(w, r, user, company)
|
||||
case http.MethodPut:
|
||||
editSlideL10n(w, r, user, company, conn, l10n)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type carouselSlide struct {
|
||||
Media string
|
||||
Caption string
|
||||
}
|
||||
|
||||
func mustCollectCarouselSlides(ctx context.Context, company *auth.Company, conn *database.Conn, loc *locale.Locale) []*carouselSlide {
|
||||
rows, err := conn.Query(ctx, `
|
||||
select coalesce(i18n.caption, slide.caption) as l10_caption
|
||||
, media.path
|
||||
from home_carousel as slide
|
||||
join media using (media_id)
|
||||
left join home_carousel_i18n as i18n on i18n.media_id = slide.media_id and lang_tag = $1
|
||||
where media.company_id = $2
|
||||
`, loc.Language, company.ID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var carousel []*carouselSlide
|
||||
for rows.Next() {
|
||||
slide := &carouselSlide{}
|
||||
err = rows.Scan(&slide.Caption, &slide.Media)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
carousel = append(carousel, slide)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
panic(rows.Err())
|
||||
}
|
||||
|
||||
return carousel
|
||||
}
|
||||
|
||||
type slideEntry struct {
|
||||
carouselSlide
|
||||
ID int
|
||||
Translations []*translation
|
||||
}
|
||||
|
||||
type translation struct {
|
||||
Language string
|
||||
Endonym string
|
||||
Missing bool
|
||||
}
|
||||
|
||||
func collectSlideEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*slideEntry, error) {
|
||||
rows, err := conn.Query(ctx, `
|
||||
select media_id
|
||||
, media.path
|
||||
, caption
|
||||
, array_agg((lang_tag, endonym, not exists (select 1 from home_carousel_i18n as i18n where i18n.media_id = home_carousel.media_id and i18n.lang_tag = language.lang_tag)) order by endonym)
|
||||
from home_carousel
|
||||
join media using (media_id)
|
||||
join company using (company_id)
|
||||
, language
|
||||
where lang_tag <> default_lang_tag
|
||||
and language.selectable
|
||||
and media.company_id = $1
|
||||
group by media_id
|
||||
, media.path
|
||||
, caption
|
||||
order by caption
|
||||
`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, company.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var slides []*slideEntry
|
||||
for rows.Next() {
|
||||
slide := &slideEntry{}
|
||||
var translations database.RecordArray
|
||||
if err = rows.Scan(&slide.ID, &slide.Media, &slide.Caption, &translations); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, el := range translations.Elements {
|
||||
slide.Translations = append(slide.Translations, &translation{
|
||||
el.Fields[0].Get().(string),
|
||||
el.Fields[1].Get().(string),
|
||||
el.Fields[2].Get().(bool),
|
||||
})
|
||||
}
|
||||
slides = append(slides, slide)
|
||||
}
|
||||
|
||||
return slides, nil
|
||||
}
|
||||
|
||||
func addSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||
f := newSlideForm()
|
||||
editSlide(w, r, user, company, conn, f)
|
||||
}
|
||||
|
||||
func editSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *slideForm) {
|
||||
f.process(w, r, user, company, false, func(ctx context.Context) {
|
||||
bytes := f.MustReadAllMedia()
|
||||
if bytes == nil {
|
||||
conn.MustExec(ctx, "select add_home_carousel_slide($1, $2)", f.ID, f.Caption)
|
||||
} else {
|
||||
tx := conn.MustBegin(ctx)
|
||||
defer tx.Rollback(ctx)
|
||||
f.ID = tx.MustGetInt(ctx, "select add_media($1, $2, $3, $4)", company.ID, f.Media.Filename(), f.Media.ContentType, bytes)
|
||||
tx.MustExec(ctx, "select add_home_carousel_slide($1, $2)", f.ID, f.Caption)
|
||||
tx.MustCommit(ctx)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func deleteSlide(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn, id int) {
|
||||
if err := user.VerifyCSRFToken(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
conn.MustExec(r.Context(), "select remove_home_carousel_slide($1)", id)
|
||||
httplib.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
type slideForm struct {
|
||||
ID int
|
||||
Media *form.File
|
||||
Caption *form.Input
|
||||
}
|
||||
|
||||
func newSlideForm() *slideForm {
|
||||
return &slideForm{
|
||||
Media: &form.File{
|
||||
Name: "media",
|
||||
MaxSize: 1 << 20,
|
||||
},
|
||||
Caption: &form.Input{
|
||||
Name: "caption",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *slideForm) FillFromDatabase(ctx context.Context, conn *database.Conn, id int) error {
|
||||
f.ID = id
|
||||
row := conn.QueryRow(ctx, `
|
||||
select caption
|
||||
, media.path
|
||||
from home_carousel
|
||||
join media using (media_id)
|
||||
where media_id = $1
|
||||
`, id)
|
||||
return row.Scan(&f.Caption.Val, &f.Media.Val)
|
||||
}
|
||||
|
||||
func (f *slideForm) process(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, mediaRequired bool, 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, mediaRequired) {
|
||||
if !httplib.IsHTMxRequest(r) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
}
|
||||
f.MustRender(w, r, user, company)
|
||||
return
|
||||
}
|
||||
act(r.Context())
|
||||
httplib.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (f *slideForm) 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.Caption.FillValue(r)
|
||||
if err := f.Media.FillValue(r); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *slideForm) Close() error {
|
||||
return f.Media.Close()
|
||||
}
|
||||
|
||||
func (f *slideForm) Valid(l *locale.Locale, mediaRequired bool) bool {
|
||||
v := form.NewValidator(l)
|
||||
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("Slide image can not be empty."))
|
||||
}
|
||||
return v.AllOK
|
||||
}
|
||||
|
||||
func (f *slideForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||
template.MustRenderAdmin(w, r, user, company, "home/carousel/form.gohtml", f)
|
||||
}
|
||||
|
||||
func (f *slideForm) HasMediaFile() bool {
|
||||
return f.Media.HasData()
|
||||
}
|
||||
|
||||
func (f *slideForm) MustReadAllMedia() []byte {
|
||||
if !f.HasMediaFile() {
|
||||
return nil
|
||||
}
|
||||
bytes, err := io.ReadAll(f.Media)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bytes
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package home
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
"dev.tandem.ws/tandem/camper/pkg/form"
|
||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||
"dev.tandem.ws/tandem/camper/pkg/template"
|
||||
)
|
||||
|
||||
type slideL10nForm struct {
|
||||
Locale *locale.Locale
|
||||
ID int
|
||||
Caption *form.L10nInput
|
||||
}
|
||||
|
||||
func newSlideL10nForm(f *slideForm, loc *locale.Locale) *slideL10nForm {
|
||||
return &slideL10nForm{
|
||||
Locale: loc,
|
||||
ID: f.ID,
|
||||
Caption: f.Caption.L10nInput(),
|
||||
}
|
||||
}
|
||||
|
||||
func (l10n *slideL10nForm) FillFromDatabase(ctx context.Context, conn *database.Conn) error {
|
||||
row := conn.QueryRow(ctx, `
|
||||
select coalesce(i18n.caption, '') as l10n_caption
|
||||
from home_carousel
|
||||
left join home_carousel_i18n as i18n on home_carousel.media_id = i18n.media_id and i18n.lang_tag = $1
|
||||
where home_carousel.media_id = $2
|
||||
`, l10n.Locale.Language, l10n.ID)
|
||||
return row.Scan(&l10n.Caption.Val)
|
||||
}
|
||||
|
||||
func (l10n *slideL10nForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||
template.MustRenderAdmin(w, r, user, company, "home/carousel/l10n.gohtml", l10n)
|
||||
}
|
||||
|
||||
func editSlideL10n(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, l10n *slideL10nForm) {
|
||||
if err := l10n.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 !l10n.Valid(user.Locale) {
|
||||
if !httplib.IsHTMxRequest(r) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
}
|
||||
l10n.MustRender(w, r, user, company)
|
||||
return
|
||||
}
|
||||
conn.MustExec(r.Context(), "select translate_home_carousel_slide($1, $2, $3)", l10n.ID, l10n.Locale.Language, l10n.Caption)
|
||||
httplib.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (l10n *slideL10nForm) Parse(r *http.Request) error {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
l10n.Caption.FillValue(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l10n *slideL10nForm) Valid(l *locale.Locale) bool {
|
||||
v := form.NewValidator(l)
|
||||
return v.AllOK
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package home
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"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"
|
||||
)
|
||||
|
||||
type PublicHandler struct {
|
||||
}
|
||||
|
||||
func NewPublicHandler() *PublicHandler {
|
||||
return &PublicHandler{}
|
||||
}
|
||||
|
||||
func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
home := newHomePage()
|
||||
home.MustRender(w, r, user, company, conn)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type homePage struct {
|
||||
*template.PublicPage
|
||||
CampsiteTypes []*campsiteType
|
||||
Carousel []*carouselSlide
|
||||
}
|
||||
|
||||
func newHomePage() *homePage {
|
||||
return &homePage{PublicPage: template.NewPublicPage()}
|
||||
}
|
||||
|
||||
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)
|
||||
p.Carousel = mustCollectCarouselSlides(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 coalesce(i18n.name, campsite_type.name) as l10_name
|
||||
, '/campsites/types/' || slug
|
||||
, media.path
|
||||
from campsite_type
|
||||
left join campsite_type_i18n as i18n on campsite_type.campsite_type_id = i18n.campsite_type_id and lang_tag = $1
|
||||
join media using (media_id)
|
||||
where campsite_type.company_id = $2
|
||||
and campsite_type.active
|
||||
`, loc.Language, 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
|
||||
}
|
|
@ -33,6 +33,15 @@ func (m Locales) Tags() []language.Tag {
|
|||
return keys
|
||||
}
|
||||
|
||||
func (m Locales) Get(lang string) (loc *Locale, ok bool) {
|
||||
tag, err := language.Parse(lang)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
loc, ok = m[tag]
|
||||
return
|
||||
}
|
||||
|
||||
func GetAll(ctx context.Context, db *database.DB) (Locales, error) {
|
||||
availableLanguages, err := getAvailableLanguages(ctx, db)
|
||||
if err != nil {
|
||||
|
|
103
po/ca.po
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: camper\n"
|
||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||
"POT-Creation-Date: 2023-09-12 20:18+0200\n"
|
||||
"POT-Creation-Date: 2023-09-15 00:05+0200\n"
|
||||
"PO-Revision-Date: 2023-07-22 23:45+0200\n"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Catalan <ca@dodds.net>\n"
|
||||
|
@ -56,15 +56,7 @@ msgstr "A menys d’una hora de <strong>Girona</strong>, a una de <strong>La Bis
|
|||
msgid "Discover the surroundings"
|
||||
msgstr "Descobreix l’entorn"
|
||||
|
||||
#: web/templates/public/home.gohtml:44 web/templates/public/home.gohtml:48
|
||||
#: web/templates/public/home.gohtml:52 web/templates/public/home.gohtml:56
|
||||
#: web/templates/public/home.gohtml:60 web/templates/public/home.gohtml:64
|
||||
#: web/templates/public/home.gohtml:68 web/templates/public/home.gohtml:72
|
||||
#: web/templates/public/home.gohtml:76
|
||||
msgid "Legend"
|
||||
msgstr "Llegenda"
|
||||
|
||||
#: web/templates/public/home.gohtml:80
|
||||
#: web/templates/public/home.gohtml:54
|
||||
msgid "Come and enjoy!"
|
||||
msgstr "Vine a gaudir!"
|
||||
|
||||
|
@ -116,6 +108,7 @@ msgstr "Etiqueta"
|
|||
#: web/templates/admin/campsite/form.gohtml:71
|
||||
#: web/templates/admin/campsite/type/form.gohtml:77
|
||||
#: web/templates/admin/season/form.gohtml:65
|
||||
#: web/templates/admin/home/carousel/form.gohtml:58
|
||||
msgctxt "action"
|
||||
msgid "Update"
|
||||
msgstr "Actualitza"
|
||||
|
@ -123,6 +116,7 @@ msgstr "Actualitza"
|
|||
#: web/templates/admin/campsite/form.gohtml:73
|
||||
#: web/templates/admin/campsite/type/form.gohtml:79
|
||||
#: web/templates/admin/season/form.gohtml:67
|
||||
#: web/templates/admin/home/carousel/form.gohtml:60
|
||||
msgctxt "action"
|
||||
msgid "Add"
|
||||
msgstr "Afegeix"
|
||||
|
@ -192,6 +186,7 @@ msgid "Name"
|
|||
msgstr "Nom"
|
||||
|
||||
#: web/templates/admin/campsite/type/form.gohtml:59
|
||||
#: web/templates/admin/home/carousel/form.gohtml:39
|
||||
msgctxt "input"
|
||||
msgid "Cover image"
|
||||
msgstr "Imatge de portada"
|
||||
|
@ -221,6 +216,7 @@ msgid "Name"
|
|||
msgstr "Nom"
|
||||
|
||||
#: web/templates/admin/campsite/type/index.gohtml:19
|
||||
#: web/templates/admin/home/index.gohtml:20
|
||||
msgctxt "campsite type"
|
||||
msgid "Translations"
|
||||
msgstr "Traduccions"
|
||||
|
@ -237,16 +233,19 @@ msgstr "Traducció del tipus d’allotjament a %s"
|
|||
|
||||
#: web/templates/admin/campsite/type/l10n.gohtml:22
|
||||
#: web/templates/admin/campsite/type/l10n.gohtml:34
|
||||
#: web/templates/admin/home/carousel/l10n.gohtml:22
|
||||
msgid "Source:"
|
||||
msgstr "Origen:"
|
||||
|
||||
#: web/templates/admin/campsite/type/l10n.gohtml:24
|
||||
#: web/templates/admin/campsite/type/l10n.gohtml:37
|
||||
#: web/templates/admin/home/carousel/l10n.gohtml:24
|
||||
msgctxt "input"
|
||||
msgid "Translation:"
|
||||
msgstr "Traducció:"
|
||||
|
||||
#: web/templates/admin/campsite/type/l10n.gohtml:46
|
||||
#: web/templates/admin/home/carousel/l10n.gohtml:33
|
||||
msgctxt "action"
|
||||
msgid "Translate"
|
||||
msgstr "Tradueix"
|
||||
|
@ -442,6 +441,69 @@ msgctxt "action"
|
|||
msgid "Logout"
|
||||
msgstr "Surt"
|
||||
|
||||
#: web/templates/admin/layout.gohtml:76 web/templates/admin/home/index.gohtml:6
|
||||
msgctxt "title"
|
||||
msgid "Home Page"
|
||||
msgstr "Pàgina d’inici"
|
||||
|
||||
#: web/templates/admin/home/carousel/form.gohtml:8
|
||||
#: web/templates/admin/home/carousel/form.gohtml:27
|
||||
msgctxt "title"
|
||||
msgid "Edit Carousel Slide"
|
||||
msgstr "Edició de la diapositiva del carrusel"
|
||||
|
||||
#: web/templates/admin/home/carousel/form.gohtml:10
|
||||
#: web/templates/admin/home/carousel/form.gohtml:29
|
||||
msgctxt "title"
|
||||
msgid "New Carousel Slide"
|
||||
msgstr "Nova diapositiva del carrusel"
|
||||
|
||||
#: web/templates/admin/home/carousel/form.gohtml:48
|
||||
#: web/templates/admin/home/carousel/l10n.gohtml:21
|
||||
msgctxt "input"
|
||||
msgid "Caption"
|
||||
msgstr "Llegenda"
|
||||
|
||||
#: web/templates/admin/home/carousel/l10n.gohtml:7
|
||||
#: web/templates/admin/home/carousel/l10n.gohtml:15
|
||||
msgctxt "title"
|
||||
msgid "Translate Carousel Slide to %s"
|
||||
msgstr "Traducció de la diapositiva del carrusel a %s"
|
||||
|
||||
#: web/templates/admin/home/index.gohtml:12
|
||||
msgctxt "title"
|
||||
msgid "Carousel"
|
||||
msgstr "Carrusel"
|
||||
|
||||
#: web/templates/admin/home/index.gohtml:13
|
||||
msgctxt "action"
|
||||
msgid "Add slide"
|
||||
msgstr "Afegeix diapositiva"
|
||||
|
||||
#: web/templates/admin/home/index.gohtml:18
|
||||
msgctxt "header"
|
||||
msgid "Image"
|
||||
msgstr "Imatge"
|
||||
|
||||
#: web/templates/admin/home/index.gohtml:19
|
||||
msgctxt "header"
|
||||
msgid "Caption"
|
||||
msgstr "Llegenda"
|
||||
|
||||
#: web/templates/admin/home/index.gohtml:21
|
||||
msgctxt "campsite type"
|
||||
msgid "Actions"
|
||||
msgstr "Accions"
|
||||
|
||||
#: web/templates/admin/home/index.gohtml:40
|
||||
msgctxt "action"
|
||||
msgid "Delete"
|
||||
msgstr "Esborra"
|
||||
|
||||
#: web/templates/admin/home/index.gohtml:48
|
||||
msgid "No slides added yet."
|
||||
msgstr "No s’ha afegit cap diapositiva encara."
|
||||
|
||||
#: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:203
|
||||
msgid "Email can not be empty."
|
||||
msgstr "No podeu deixar el correu-e en blanc."
|
||||
|
@ -463,8 +525,8 @@ msgctxt "language option"
|
|||
msgid "Automatic"
|
||||
msgstr "Automàtic"
|
||||
|
||||
#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:105
|
||||
#: pkg/campsite/types/admin.go:301 pkg/season/admin.go:203
|
||||
#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:90
|
||||
#: pkg/campsite/types/admin.go:293 pkg/season/admin.go:203
|
||||
msgid "Name can not be empty."
|
||||
msgstr "No podeu deixar el nom en blanc."
|
||||
|
||||
|
@ -476,15 +538,15 @@ msgstr "La confirmació no es correspon amb la contrasenya."
|
|||
msgid "Selected language is not valid."
|
||||
msgstr "L’idioma escollit no és vàlid."
|
||||
|
||||
#: pkg/app/user.go:253 pkg/campsite/types/admin.go:303
|
||||
#: pkg/app/user.go:253 pkg/campsite/types/admin.go:295 pkg/home/carousel.go:286
|
||||
msgid "File must be a valid PNG or JPEG image."
|
||||
msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida."
|
||||
|
||||
#: pkg/app/admin.go:44
|
||||
#: pkg/app/admin.go:47
|
||||
msgid "Access forbidden"
|
||||
msgstr "Accés prohibit"
|
||||
|
||||
#: pkg/campsite/types/admin.go:305
|
||||
#: pkg/campsite/types/admin.go:297
|
||||
msgid "Cover image can not be empty."
|
||||
msgstr "No podeu deixar la imatge de portada en blanc."
|
||||
|
||||
|
@ -568,13 +630,16 @@ msgstr "No podeu deixar el format del número de factura en blanc."
|
|||
msgid "Cross-site request forgery detected."
|
||||
msgstr "S’ha detectat un intent de falsificació de petició a llocs creuats."
|
||||
|
||||
#: pkg/home/carousel.go:288
|
||||
msgid "Slide image can not be empty."
|
||||
msgstr "No podeu deixar la imatge de la diapositiva en blanc."
|
||||
|
||||
#~ msgid "Legend"
|
||||
#~ msgstr "Llegenda"
|
||||
|
||||
#~ msgid "Environment"
|
||||
#~ msgstr "Entorn"
|
||||
|
||||
#~ msgctxt "title"
|
||||
#~ msgid "New Page"
|
||||
#~ msgstr "Nova pàgina"
|
||||
|
||||
#~ msgctxt "input"
|
||||
#~ msgid "Title"
|
||||
#~ msgstr "Títol"
|
||||
|
|
103
po/es.po
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: camper\n"
|
||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||
"POT-Creation-Date: 2023-09-12 20:18+0200\n"
|
||||
"POT-Creation-Date: 2023-09-15 00:05+0200\n"
|
||||
"PO-Revision-Date: 2023-07-22 23:46+0200\n"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Spanish <es@tp.org.es>\n"
|
||||
|
@ -56,15 +56,7 @@ msgstr "A menos de una hora de <strong>Girona</strong>, a una de <strong>La Bisb
|
|||
msgid "Discover the surroundings"
|
||||
msgstr "Descubre el entorno"
|
||||
|
||||
#: web/templates/public/home.gohtml:44 web/templates/public/home.gohtml:48
|
||||
#: web/templates/public/home.gohtml:52 web/templates/public/home.gohtml:56
|
||||
#: web/templates/public/home.gohtml:60 web/templates/public/home.gohtml:64
|
||||
#: web/templates/public/home.gohtml:68 web/templates/public/home.gohtml:72
|
||||
#: web/templates/public/home.gohtml:76
|
||||
msgid "Legend"
|
||||
msgstr "Leyenda"
|
||||
|
||||
#: web/templates/public/home.gohtml:80
|
||||
#: web/templates/public/home.gohtml:54
|
||||
msgid "Come and enjoy!"
|
||||
msgstr "¡Ven a disfrutar!"
|
||||
|
||||
|
@ -116,6 +108,7 @@ msgstr "Etiqueta"
|
|||
#: web/templates/admin/campsite/form.gohtml:71
|
||||
#: web/templates/admin/campsite/type/form.gohtml:77
|
||||
#: web/templates/admin/season/form.gohtml:65
|
||||
#: web/templates/admin/home/carousel/form.gohtml:58
|
||||
msgctxt "action"
|
||||
msgid "Update"
|
||||
msgstr "Actualizar"
|
||||
|
@ -123,6 +116,7 @@ msgstr "Actualizar"
|
|||
#: web/templates/admin/campsite/form.gohtml:73
|
||||
#: web/templates/admin/campsite/type/form.gohtml:79
|
||||
#: web/templates/admin/season/form.gohtml:67
|
||||
#: web/templates/admin/home/carousel/form.gohtml:60
|
||||
msgctxt "action"
|
||||
msgid "Add"
|
||||
msgstr "Añadir"
|
||||
|
@ -192,6 +186,7 @@ msgid "Name"
|
|||
msgstr "Nombre"
|
||||
|
||||
#: web/templates/admin/campsite/type/form.gohtml:59
|
||||
#: web/templates/admin/home/carousel/form.gohtml:39
|
||||
msgctxt "input"
|
||||
msgid "Cover image"
|
||||
msgstr "Imagen de portada"
|
||||
|
@ -221,6 +216,7 @@ msgid "Name"
|
|||
msgstr "Nombre"
|
||||
|
||||
#: web/templates/admin/campsite/type/index.gohtml:19
|
||||
#: web/templates/admin/home/index.gohtml:20
|
||||
msgctxt "campsite type"
|
||||
msgid "Translations"
|
||||
msgstr "Traducciones"
|
||||
|
@ -237,16 +233,19 @@ msgstr "Traducción de tipo de alojamiento a %s"
|
|||
|
||||
#: web/templates/admin/campsite/type/l10n.gohtml:22
|
||||
#: web/templates/admin/campsite/type/l10n.gohtml:34
|
||||
#: web/templates/admin/home/carousel/l10n.gohtml:22
|
||||
msgid "Source:"
|
||||
msgstr "Origen:"
|
||||
|
||||
#: web/templates/admin/campsite/type/l10n.gohtml:24
|
||||
#: web/templates/admin/campsite/type/l10n.gohtml:37
|
||||
#: web/templates/admin/home/carousel/l10n.gohtml:24
|
||||
msgctxt "input"
|
||||
msgid "Translation:"
|
||||
msgstr "Traducción"
|
||||
|
||||
#: web/templates/admin/campsite/type/l10n.gohtml:46
|
||||
#: web/templates/admin/home/carousel/l10n.gohtml:33
|
||||
msgctxt "action"
|
||||
msgid "Translate"
|
||||
msgstr "Traducir"
|
||||
|
@ -442,6 +441,69 @@ msgctxt "action"
|
|||
msgid "Logout"
|
||||
msgstr "Salir"
|
||||
|
||||
#: web/templates/admin/layout.gohtml:76 web/templates/admin/home/index.gohtml:6
|
||||
msgctxt "title"
|
||||
msgid "Home Page"
|
||||
msgstr "Página de inicio"
|
||||
|
||||
#: web/templates/admin/home/carousel/form.gohtml:8
|
||||
#: web/templates/admin/home/carousel/form.gohtml:27
|
||||
msgctxt "title"
|
||||
msgid "Edit Carousel Slide"
|
||||
msgstr "Edición de la diapositiva del carrusel"
|
||||
|
||||
#: web/templates/admin/home/carousel/form.gohtml:10
|
||||
#: web/templates/admin/home/carousel/form.gohtml:29
|
||||
msgctxt "title"
|
||||
msgid "New Carousel Slide"
|
||||
msgstr "Nueva diapositiva del carrusel"
|
||||
|
||||
#: web/templates/admin/home/carousel/form.gohtml:48
|
||||
#: web/templates/admin/home/carousel/l10n.gohtml:21
|
||||
msgctxt "input"
|
||||
msgid "Caption"
|
||||
msgstr "Leyenda"
|
||||
|
||||
#: web/templates/admin/home/carousel/l10n.gohtml:7
|
||||
#: web/templates/admin/home/carousel/l10n.gohtml:15
|
||||
msgctxt "title"
|
||||
msgid "Translate Carousel Slide to %s"
|
||||
msgstr "Traducción de la diapositiva de carrusel a %s"
|
||||
|
||||
#: web/templates/admin/home/index.gohtml:12
|
||||
msgctxt "title"
|
||||
msgid "Carousel"
|
||||
msgstr "Carrusel"
|
||||
|
||||
#: web/templates/admin/home/index.gohtml:13
|
||||
msgctxt "action"
|
||||
msgid "Add slide"
|
||||
msgstr "Añadir diapositiva"
|
||||
|
||||
#: web/templates/admin/home/index.gohtml:18
|
||||
msgctxt "header"
|
||||
msgid "Image"
|
||||
msgstr "Imagen"
|
||||
|
||||
#: web/templates/admin/home/index.gohtml:19
|
||||
msgctxt "header"
|
||||
msgid "Caption"
|
||||
msgstr "Leyenda"
|
||||
|
||||
#: web/templates/admin/home/index.gohtml:21
|
||||
msgctxt "campsite type"
|
||||
msgid "Actions"
|
||||
msgstr "Acciones"
|
||||
|
||||
#: web/templates/admin/home/index.gohtml:40
|
||||
msgctxt "action"
|
||||
msgid "Delete"
|
||||
msgstr "Borrar"
|
||||
|
||||
#: web/templates/admin/home/index.gohtml:48
|
||||
msgid "No slides added yet."
|
||||
msgstr "No se ha añadido ninguna diapositiva todavía."
|
||||
|
||||
#: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:203
|
||||
msgid "Email can not be empty."
|
||||
msgstr "No podéis dejar el correo-e en blanco."
|
||||
|
@ -463,8 +525,8 @@ msgctxt "language option"
|
|||
msgid "Automatic"
|
||||
msgstr "Automático"
|
||||
|
||||
#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:105
|
||||
#: pkg/campsite/types/admin.go:301 pkg/season/admin.go:203
|
||||
#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:90
|
||||
#: pkg/campsite/types/admin.go:293 pkg/season/admin.go:203
|
||||
msgid "Name can not be empty."
|
||||
msgstr "No podéis dejar el nombre en blanco."
|
||||
|
||||
|
@ -476,15 +538,15 @@ msgstr "La confirmación no se corresponde con la contraseña."
|
|||
msgid "Selected language is not valid."
|
||||
msgstr "El idioma escogido no es válido."
|
||||
|
||||
#: pkg/app/user.go:253 pkg/campsite/types/admin.go:303
|
||||
#: pkg/app/user.go:253 pkg/campsite/types/admin.go:295 pkg/home/carousel.go:286
|
||||
msgid "File must be a valid PNG or JPEG image."
|
||||
msgstr "El archivo tiene que ser una imagen PNG o JPEG válida."
|
||||
|
||||
#: pkg/app/admin.go:44
|
||||
#: pkg/app/admin.go:47
|
||||
msgid "Access forbidden"
|
||||
msgstr "Acceso prohibido"
|
||||
|
||||
#: pkg/campsite/types/admin.go:305
|
||||
#: pkg/campsite/types/admin.go:297
|
||||
msgid "Cover image can not be empty."
|
||||
msgstr "No podéis dejar la imagen de portada en blanco."
|
||||
|
||||
|
@ -568,13 +630,16 @@ msgstr "No podéis dejar el formato de número de factura en blanco."
|
|||
msgid "Cross-site request forgery detected."
|
||||
msgstr "Se ha detectado un intento de falsificación de petición en sitios cruzados."
|
||||
|
||||
#: pkg/home/carousel.go:288
|
||||
msgid "Slide image can not be empty."
|
||||
msgstr "No podéis dejar la imagen de la diapositiva en blanco."
|
||||
|
||||
#~ msgid "Legend"
|
||||
#~ msgstr "Leyenda"
|
||||
|
||||
#~ msgid "Environment"
|
||||
#~ msgstr "Entorno"
|
||||
|
||||
#~ msgctxt "title"
|
||||
#~ msgid "New Page"
|
||||
#~ msgstr "Nueva página"
|
||||
|
||||
#~ msgctxt "input"
|
||||
#~ msgid "Title"
|
||||
#~ msgstr "Título"
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert camper:add_home_carousel_slide from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists camper.add_home_carousel_slide(integer, text);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert camper:home_carousel from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop table if exists camper.home_carousel;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert camper:home_carousel_i18n from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop table if exists camper.home_carousel_i18n;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert camper:media_path from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists camper.path(camper.media);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert camper:remove_home_carousel_slide from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists camper.remove_home_carousel_slide(integer);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert camper:translate_home_carousel_slide from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists camper.translate_home_carousel_slide(integer, text, text);
|
||||
|
||||
commit;
|
|
@ -42,6 +42,7 @@ change_password [roles schema_auth schema_camper user] 2023-07-21T23:54:52Z jord
|
|||
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
|
||||
media_path [roles schema_camper media] 2023-09-13T22:50:14Z jordi fita mas <jordi@tandem.blog> # Add function to get the URL path of a 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
|
||||
campsite_type_i18n [roles schema_camper campsite_type language] 2023-09-12T10:31:29Z jordi fita mas <jordi@tandem.blog> # Add relation for campsite_type translations
|
||||
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
|
||||
|
@ -57,3 +58,8 @@ 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
|
||||
home_carousel [roles schema_public company media user_profile] 2023-09-13T17:16:34Z jordi fita mas <jordi@tandem.blog> # Add relation for home page’s image carousel
|
||||
home_carousel_i18n [roles schema_camper home_carousel language] 2023-09-13T23:22:42Z jordi fita mas <jordi@tandem.blog> # Add relation for home carousel translations
|
||||
add_home_carousel_slide [roles schema_camper home_carousel] 2023-09-14T17:49:21Z jordi fita mas <jordi@tandem.blog> # Add function to create slides for the home carousel
|
||||
translate_home_carousel_slide [roles schema_camper home_carousel_i18n] 2023-09-14T18:17:36Z jordi fita mas <jordi@tandem.blog> # Add function to translate a home carousel slider
|
||||
remove_home_carousel_slide [roles schema_camper home_carousel home_carousel_i18n] 2023-09-14T21:57:48Z jordi fita mas <jordi@tandem.blog> # Add function to remove sliders from the home carousel
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
-- Test add_home_carousel_slide
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
select plan(14);
|
||||
|
||||
select has_function('camper', 'add_home_carousel_slide', array['integer', 'text']);
|
||||
select function_lang_is('camper', 'add_home_carousel_slide', array['integer', 'text'], 'sql');
|
||||
select function_returns('camper', 'add_home_carousel_slide', array['integer', 'text'], 'integer');
|
||||
select isnt_definer('camper', 'add_home_carousel_slide', array['integer', 'text']);
|
||||
select volatility_is('camper', 'add_home_carousel_slide', array['integer', 'text'], 'volatile');
|
||||
select function_privs_are('camper', 'add_home_carousel_slide', array['integer', 'text'], 'guest', array[]::text[]);
|
||||
select function_privs_are('camper', 'add_home_carousel_slide', array['integer', 'text'], 'employee', array[]::text[]);
|
||||
select function_privs_are('camper', 'add_home_carousel_slide', array['integer', 'text'], 'admin', array['EXECUTE']);
|
||||
select function_privs_are('camper', 'add_home_carousel_slide', array['integer', 'text'], 'authenticator', array[]::text[]);
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate home_carousel_i18n cascade;
|
||||
truncate home_carousel cascade;
|
||||
truncate media cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
|
||||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
|
||||
;
|
||||
|
||||
insert into media (media_id, company_id, original_filename, media_type, content)
|
||||
values (5, 1, 'text.txt', 'text/plain', 'hello, world!')
|
||||
, (6, 1, 'image.svg', 'image/svg+xml', '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>')
|
||||
, (7, 1, 'cover4.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
|
||||
;
|
||||
|
||||
insert into home_carousel (media_id, caption)
|
||||
values (5, 'Previous caption')
|
||||
;
|
||||
|
||||
select lives_ok(
|
||||
$$ select add_home_carousel_slide(6, 'A caption') $$,
|
||||
'Should be able to add a carousel slide with a caption'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select add_home_carousel_slide(7, null) $$,
|
||||
'Should be able to add a carousel slide without caption'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select add_home_carousel_slide(5, 'New caption') $$,
|
||||
'Should be able to overwrite a slide with a new caption'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select media_id, caption from home_carousel $$,
|
||||
$$ values (5, 'New caption')
|
||||
, (6, 'A caption')
|
||||
, (7, '')
|
||||
$$,
|
||||
'Should have all three slides'
|
||||
);
|
||||
|
||||
select is_empty(
|
||||
$$ select * from home_carousel_i18n $$,
|
||||
'Should not have added any translation'
|
||||
);
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,178 @@
|
|||
-- Test home_carousel
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(30);
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
select has_table('home_carousel');
|
||||
select has_pk('home_carousel');
|
||||
select table_privs_are('home_carousel', 'guest', array['SELECT']);
|
||||
select table_privs_are('home_carousel', 'employee', array['SELECT']);
|
||||
select table_privs_are('home_carousel', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
|
||||
select table_privs_are('home_carousel', 'authenticator', array[]::text[]);
|
||||
|
||||
select has_column('home_carousel', 'media_id');
|
||||
select col_is_pk('home_carousel', 'media_id');
|
||||
select col_is_fk('home_carousel', 'media_id');
|
||||
select fk_ok('home_carousel', 'media_id', 'media', 'media_id');
|
||||
select col_type_is('home_carousel', 'media_id', 'integer');
|
||||
select col_not_null('home_carousel', 'media_id');
|
||||
select col_hasnt_default('home_carousel', 'media_id');
|
||||
|
||||
select has_column('home_carousel', 'caption');
|
||||
select col_type_is('home_carousel', 'caption', 'text');
|
||||
select col_not_null('home_carousel', 'caption');
|
||||
select col_hasnt_default('home_carousel', 'caption');
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate home_carousel cascade;
|
||||
truncate media cascade;
|
||||
truncate company_host cascade;
|
||||
truncate company_user cascade;
|
||||
truncate company cascade;
|
||||
truncate auth."user" cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
|
||||
insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at)
|
||||
values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
|
||||
, (5, 'admin@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
|
||||
;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
|
||||
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
|
||||
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 'ca')
|
||||
;
|
||||
|
||||
insert into company_user (company_id, user_id, role)
|
||||
values (2, 1, 'admin')
|
||||
, (4, 5, 'admin')
|
||||
;
|
||||
|
||||
insert into company_host (company_id, host)
|
||||
values (2, 'co2')
|
||||
, (4, 'co4')
|
||||
;
|
||||
|
||||
insert into media (media_id, company_id, original_filename, media_type, content)
|
||||
values ( 7, 2, 'text2.txt', 'text/plain', 'content2')
|
||||
, ( 8, 2, 'text3.txt', 'text/plain', 'content3')
|
||||
, ( 9, 4, 'text4.txt', 'text/plain', 'content4')
|
||||
, (10, 4, 'text5.txt', 'text/plain', 'content5')
|
||||
;
|
||||
|
||||
insert into home_carousel (media_id, caption)
|
||||
values (7, 'Caption 7')
|
||||
, (9, 'Caption 9')
|
||||
;
|
||||
|
||||
prepare carousel_data as
|
||||
select media_id, caption
|
||||
from home_carousel
|
||||
order by media_id, caption;
|
||||
|
||||
set role guest;
|
||||
select bag_eq(
|
||||
'carousel_data',
|
||||
$$ values (7, 'Caption 7')
|
||||
, (9, 'Caption 9')
|
||||
$$,
|
||||
'Everyone should be able to list all carousel media across all companies'
|
||||
);
|
||||
reset role;
|
||||
|
||||
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2');
|
||||
|
||||
select lives_ok(
|
||||
$$ insert into home_carousel(media_id, caption)
|
||||
values (8, 'Caption 8') $$,
|
||||
'Admin from company 2 should be able to insert a new carousel media to that company.'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'carousel_data',
|
||||
$$ values (7, 'Caption 7')
|
||||
, (8, 'Caption 8')
|
||||
, (9, 'Caption 9')
|
||||
$$,
|
||||
'The new row should have been added'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ update home_carousel set caption = 'Caption 8.8' where media_id = 8 $$,
|
||||
'Admin from company 2 should be able to update carousel media of that company.'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'carousel_data',
|
||||
$$ values (7, 'Caption 7')
|
||||
, (8, 'Caption 8.8')
|
||||
, (9, 'Caption 9')
|
||||
$$,
|
||||
'The row should have been updated.'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ delete from home_carousel where media_id = 8 $$,
|
||||
'Admin from company 2 should be able to delete carousel media from that company.'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'carousel_data',
|
||||
$$ values (7, 'Caption 7')
|
||||
, (9, 'Caption 9')
|
||||
$$,
|
||||
'The row should have been deleted.'
|
||||
);
|
||||
|
||||
select throws_ok(
|
||||
$$ insert into home_carousel (media_id, caption)
|
||||
values (10, 'Caption 10') $$,
|
||||
'42501', 'new row violates row-level security policy for table "home_carousel"',
|
||||
'Admin from company 2 should NOT be able to insert new media to company 4.'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ update home_carousel set caption = 'Nope' where media_id = 9 $$,
|
||||
'Admin from company 2 should not be able to update new carousel media of company 4, but no error if media_id is not changed.'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'carousel_data',
|
||||
$$ values (7, 'Caption 7')
|
||||
, (9, 'Caption 9')
|
||||
$$,
|
||||
'No row should have been changed.'
|
||||
);
|
||||
|
||||
select throws_ok(
|
||||
$$ update home_carousel set media_id = 10 where media_id = 7 $$,
|
||||
'42501', 'new row violates row-level security policy for table "home_carousel"',
|
||||
'Admin from company 2 should NOT be able to move carousel media to company 4'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ delete from home_carousel where media_id = 9 $$,
|
||||
'Admin from company 2 should NOT be able to delete carousel media from company 4, but no error is thrown'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
'carousel_data',
|
||||
$$ values (7, 'Caption 7')
|
||||
, (9, 'Caption 9')
|
||||
$$,
|
||||
'No row should have been changed'
|
||||
);
|
||||
|
||||
reset role;
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
-- Test home_carousel_i18n
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(23);
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
select has_table('home_carousel_i18n');
|
||||
select has_pk('home_carousel_i18n');
|
||||
select col_is_pk('home_carousel_i18n', array['media_id', 'lang_tag']);
|
||||
select table_privs_are('home_carousel_i18n', 'guest', array['SELECT']);
|
||||
select table_privs_are('home_carousel_i18n', 'employee', array['SELECT']);
|
||||
select table_privs_are('home_carousel_i18n', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
|
||||
select table_privs_are('home_carousel_i18n', 'authenticator', array[]::text[]);
|
||||
|
||||
select has_column('home_carousel_i18n', 'media_id');
|
||||
select col_is_fk('home_carousel_i18n', 'media_id');
|
||||
select fk_ok('home_carousel_i18n', 'media_id', 'home_carousel', 'media_id');
|
||||
select col_type_is('home_carousel_i18n', 'media_id', 'integer');
|
||||
select col_not_null('home_carousel_i18n', 'media_id');
|
||||
select col_hasnt_default('home_carousel_i18n', 'media_id');
|
||||
|
||||
select has_column('home_carousel_i18n', 'lang_tag');
|
||||
select col_is_fk('home_carousel_i18n', 'lang_tag');
|
||||
select fk_ok('home_carousel_i18n', 'lang_tag', 'language', 'lang_tag');
|
||||
select col_type_is('home_carousel_i18n', 'lang_tag', 'text');
|
||||
select col_not_null('home_carousel_i18n', 'lang_tag');
|
||||
select col_hasnt_default('home_carousel_i18n', 'lang_tag');
|
||||
|
||||
select has_column('home_carousel_i18n', 'caption');
|
||||
select col_type_is('home_carousel_i18n', 'caption', 'text');
|
||||
select col_not_null('home_carousel_i18n', 'caption');
|
||||
select col_hasnt_default('home_carousel_i18n', 'caption');
|
||||
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
-- Test media_path
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(10);
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
select has_function('camper', 'path', array['media']);
|
||||
select function_lang_is('camper', 'path', array['media'], 'sql');
|
||||
select function_returns('camper', 'path', array['media'], 'text');
|
||||
select isnt_definer('camper', 'path', array['media']);
|
||||
select volatility_is('camper', 'path', array['media'], 'stable');
|
||||
select function_privs_are('camper', 'path', array['media'], 'guest', array['EXECUTE']);
|
||||
select function_privs_are('camper', 'path', array['media'], 'employee', array['EXECUTE']);
|
||||
select function_privs_are('camper', 'path', array['media'], 'admin', array['EXECUTE']);
|
||||
select function_privs_are('camper', 'path', array['media'], 'authenticator', array[]::text[]);
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate media cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
|
||||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
|
||||
;
|
||||
|
||||
insert into media (media_id, company_id, original_filename, media_type, content)
|
||||
values (3, 1, 'cover1.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ff0000","a"};')
|
||||
, (4, 1, 'cover2.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","b c #00ff00","b"};')
|
||||
, (5, 1, 'cover3.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","c c #0000ff","c"};')
|
||||
, (6, 1, 'image.svg', 'image/svg+xml', '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>')
|
||||
, (7, 1, 'text.txt', 'text/plain', 'hello, world!')
|
||||
;
|
||||
|
||||
select bag_eq(
|
||||
$$ select media_id, media.path from media order by media_id $$,
|
||||
$$ values (3, '/media/fbbac389f80d12ef13a3dd6e5dec9541622b7f870f22cfa94c3ca374d6dbc6b4/cover1.xpm')
|
||||
, (4, '/media/b1ac118723104963266ceff35cc5170802885412d1bc6ea247981c15eb4b00b1/cover2.xpm')
|
||||
, (5, '/media/72ccd5c651416d714740715035e0e099c18785ce9f9a8f4222bfb72f663b1c18/cover3.xpm')
|
||||
, (6, '/media/ffc9f5e4fdeea83920c171e2bd17577127c5d1a2c3c76f07440e10d387132280/image.svg')
|
||||
, (7, '/media/68e656b251e67e8358bef8483ab0d51c6619f3e7a1a9f0e75838d41ff368f728/text.txt')
|
||||
$$,
|
||||
'Should give out the URL path for each media'
|
||||
);
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,80 @@
|
|||
-- Test remove_home_carousel_slide
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(12);
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
select has_function('camper', 'remove_home_carousel_slide', array['integer']);
|
||||
select function_lang_is('camper', 'remove_home_carousel_slide', array['integer'], 'sql');
|
||||
select function_returns('camper', 'remove_home_carousel_slide', array['integer'], 'void');
|
||||
select isnt_definer('camper', 'remove_home_carousel_slide', array['integer']);
|
||||
select volatility_is('camper', 'remove_home_carousel_slide', array['integer'], 'volatile');
|
||||
select function_privs_are('camper', 'remove_home_carousel_slide', array['integer'], 'guest', array[]::text[]);
|
||||
select function_privs_are('camper', 'remove_home_carousel_slide', array['integer'], 'employee', array[]::text[]);
|
||||
select function_privs_are('camper', 'remove_home_carousel_slide', array['integer'], 'admin', array['EXECUTE']);
|
||||
select function_privs_are('camper', 'remove_home_carousel_slide', array['integer'], 'authenticator', array[]::text[]);
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate home_carousel_i18n cascade;
|
||||
truncate home_carousel cascade;
|
||||
truncate media cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
|
||||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
|
||||
;
|
||||
|
||||
insert into media (media_id, company_id, original_filename, media_type, content)
|
||||
values (5, 1, 'text.txt', 'text/plain', 'hello, world!')
|
||||
, (6, 1, 'image.svg', 'image/svg+xml', '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>')
|
||||
, (7, 1, 'cover4.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
|
||||
;
|
||||
|
||||
insert into home_carousel (media_id, caption)
|
||||
values (5, 'Source caption')
|
||||
, (6, 'Another caption')
|
||||
, (7, 'N/A')
|
||||
;
|
||||
|
||||
insert into home_carousel_i18n (media_id, lang_tag, caption)
|
||||
values (5, 'en', 'Target caption')
|
||||
, (5, 'es', 'Target caption (spanish)')
|
||||
, (6, 'en', 'Target caption')
|
||||
, (6, 'es', 'Target caption (spanish)')
|
||||
, (7, 'en', 'Target caption')
|
||||
, (7, 'es', 'Target caption (spanish)')
|
||||
;
|
||||
|
||||
select lives_ok(
|
||||
$$ select remove_home_carousel_slide(6) $$,
|
||||
'Should be able to delete a slide'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select media_id, caption from home_carousel $$,
|
||||
$$ values (5, 'Source caption')
|
||||
, (7, 'N/A')
|
||||
$$,
|
||||
'Should have removed the slide'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select media_id, lang_tag, caption from home_carousel_i18n $$,
|
||||
$$ values (5, 'en', 'Target caption')
|
||||
, (5, 'es', 'Target caption (spanish)')
|
||||
, (7, 'en', 'Target caption')
|
||||
, (7, 'es', 'Target caption (spanish)')
|
||||
$$,
|
||||
'Should have removed the slide’s translations'
|
||||
);
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,78 @@
|
|||
-- Test translate_home_carousel_slide
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(13);
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
select has_function('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text']);
|
||||
select function_lang_is('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text'], 'sql');
|
||||
select function_returns('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text'], 'void');
|
||||
select isnt_definer('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text']);
|
||||
select volatility_is('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text'], 'volatile');
|
||||
select function_privs_are('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text'], 'guest', array[]::text[]);
|
||||
select function_privs_are('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text'], 'employee', array[]::text[]);
|
||||
select function_privs_are('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text'], 'admin', array['EXECUTE']);
|
||||
select function_privs_are('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text'], 'authenticator', array[]::text[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate home_carousel_i18n cascade;
|
||||
truncate home_carousel cascade;
|
||||
truncate media cascade;
|
||||
truncate company cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
|
||||
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
|
||||
;
|
||||
|
||||
insert into media (media_id, company_id, original_filename, media_type, content)
|
||||
values (5, 1, 'text.txt', 'text/plain', 'hello, world!')
|
||||
, (6, 1, 'image.svg', 'image/svg+xml', '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>')
|
||||
, (7, 1, 'cover4.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
|
||||
;
|
||||
|
||||
insert into home_carousel (media_id, caption)
|
||||
values (5, 'Source caption')
|
||||
, (6, 'Another caption')
|
||||
, (7, 'N/A')
|
||||
;
|
||||
|
||||
insert into home_carousel_i18n (media_id, lang_tag, caption)
|
||||
values (5, 'en', 'Target caption')
|
||||
;
|
||||
|
||||
select lives_ok(
|
||||
$$ select translate_home_carousel_slide(5, 'ca', 'Traducció') $$,
|
||||
'Should be able to translate a carousel slide'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select translate_home_carousel_slide(6, 'es', null) $$,
|
||||
'Should be able to “translate” a carousel slide to the empty string'
|
||||
);
|
||||
|
||||
select lives_ok(
|
||||
$$ select translate_home_carousel_slide(5, 'en', 'Not anymore') $$,
|
||||
'Should be able to overwrite a slide’s translation'
|
||||
);
|
||||
|
||||
select bag_eq(
|
||||
$$ select media_id, lang_tag, caption from home_carousel_i18n $$,
|
||||
$$ values (5, 'ca', 'Traducció')
|
||||
, (5, 'en', 'Not anymore')
|
||||
, (6, 'es', '')
|
||||
$$,
|
||||
'Should have all three slides'
|
||||
);
|
||||
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify camper:add_home_carousel_slide on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('camper.add_home_carousel_slide(integer, text)', 'execute');
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,16 @@
|
|||
-- Verify camper:home_carousel on pg
|
||||
|
||||
begin;
|
||||
|
||||
select media_id
|
||||
, caption
|
||||
from camper.home_carousel
|
||||
where false;
|
||||
|
||||
select 1 / count(*) from pg_class where oid = 'camper.home_carousel'::regclass and relrowsecurity;
|
||||
select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.home_carousel'::regclass;
|
||||
select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.home_carousel'::regclass;
|
||||
select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.home_carousel'::regclass;
|
||||
select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.home_carousel'::regclass;
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,11 @@
|
|||
-- Verify camper:home_carousel_i18n on pg
|
||||
|
||||
begin;
|
||||
|
||||
select media_id
|
||||
, lang_tag
|
||||
, caption
|
||||
from camper.home_carousel_i18n
|
||||
where false;
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify camper:media_path on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('camper.path(camper.media)', 'execute');
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify camper:remove_home_carousel_slide on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('camper.remove_home_carousel_slide(integer)', 'execute');
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify camper:translate_home_carousel_slide on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('camper.translate_home_carousel_slide(integer, text, text)', 'execute');
|
||||
|
||||
rollback;
|
|
@ -432,12 +432,12 @@ nav .has-submenu:hover ul, nav .has-submenu:focus-within ul {
|
|||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.surroundings figure {
|
||||
.surroundings figure, .surroundings .slick-track > img {
|
||||
margin-right: 5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.surroundings figure img {
|
||||
.surroundings img {
|
||||
height: 40rem;
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
{{- end }}
|
||||
{{ with .Media -}}
|
||||
{{ if .Val -}}
|
||||
<img src="/media/{{ .Val }}" alt="">
|
||||
<img src="{{ .Val }}" alt="">
|
||||
{{- end }}
|
||||
<label>
|
||||
{{( pgettext "Cover image" "input" )}}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
{{ define "title" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/home.slideForm*/ -}}
|
||||
{{ if .ID}}
|
||||
{{( pgettext "Edit Carousel Slide" "title" )}}
|
||||
{{ else }}
|
||||
{{( pgettext "New Carousel Slide" "title" )}}
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
|
||||
{{ define "content" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/home.slideForm*/ -}}
|
||||
{{ template "settings-tabs" "home" }}
|
||||
<form
|
||||
enctype="multipart/form-data"
|
||||
{{ if .ID }}
|
||||
data-hx-put="/admin/home/slides/{{ .ID }}"
|
||||
{{ else }}
|
||||
action="/admin/home/slides" method="post"
|
||||
{{ end }}
|
||||
>
|
||||
<h2>
|
||||
{{ if .ID }}
|
||||
{{( pgettext "Edit Carousel Slide" "title" )}}
|
||||
{{ else }}
|
||||
{{( pgettext "New Carousel Slide" "title" )}}
|
||||
{{ end }}
|
||||
</h2>
|
||||
{{ CSRFInput }}
|
||||
<fieldset>
|
||||
{{ with .Media -}}
|
||||
{{ if .Val -}}
|
||||
<img src="{{ .Val }}" alt="">
|
||||
{{- end }}
|
||||
<label>
|
||||
{{( pgettext "Cover image" "input" )}}
|
||||
<input type="file" name="{{ .Name }}"
|
||||
{{ if not $.ID }}required{{ end }}
|
||||
{{ template "error-attrs" . }}><br>
|
||||
</label>
|
||||
{{ template "error-message" . }}
|
||||
{{- end }}
|
||||
{{ with .Caption -}}
|
||||
<label>
|
||||
{{( pgettext "Caption" "input")}}<br>
|
||||
<input type="text" name="{{ .Name }}" value="{{ .Val }}"
|
||||
{{ template "error-attrs" . }}><br>
|
||||
</label>
|
||||
{{ template "error-message" . }}
|
||||
{{- end }}
|
||||
</fieldset>
|
||||
<footer>
|
||||
<button type="submit">
|
||||
{{ if .ID }}
|
||||
{{( pgettext "Update" "action" )}}
|
||||
{{ else }}
|
||||
{{( pgettext "Add" "action" )}}
|
||||
{{ end }}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
{{- end }}
|
|
@ -0,0 +1,36 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
{{ define "title" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/home.slideL10nForm*/ -}}
|
||||
{{printf (pgettext "Translate Carousel Slide to %s" "title") .Locale.Endonym }}
|
||||
{{- end }}
|
||||
|
||||
{{ define "content" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/home.slideL10nForm*/ -}}
|
||||
{{ template "settings-tabs" "campsiteTypes" }}
|
||||
<form data-hx-put="/admin/home/slides/{{ .ID }}/{{ .Locale.Language }}">
|
||||
<h2>
|
||||
{{printf (pgettext "Translate Carousel Slide to %s" "title") .Locale.Endonym }}
|
||||
</h2>
|
||||
{{ CSRFInput }}
|
||||
<fieldset>
|
||||
{{ with .Caption -}}
|
||||
<fieldset>
|
||||
<legend>{{( pgettext "Caption" "input")}}</legend>
|
||||
{{( gettext "Source:" )}} {{ .Source }}<br>
|
||||
<label>
|
||||
{{( pgettext "Translation:" "input" )}}
|
||||
<input type="text" name="{{ .Name }}" value="{{ .Val }}"
|
||||
{{ template "error-attrs" . }}><br>
|
||||
</label>
|
||||
{{ template "error-message" . }}
|
||||
</fieldset>
|
||||
{{- end }}
|
||||
</fieldset>
|
||||
<footer>
|
||||
<button type="submit">{{( pgettext "Translate" "action" )}}</button>
|
||||
</footer>
|
||||
</form>
|
||||
{{- end }}
|
|
@ -0,0 +1,50 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
{{ define "title" -}}
|
||||
{{( pgettext "Home Page" "title" )}}
|
||||
{{- end }}
|
||||
|
||||
{{ define "content" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/home.homeIndex*/ -}}
|
||||
{{ template "settings-tabs" "home" }}
|
||||
<h2>{{( pgettext "Carousel" "title" )}}</h2>
|
||||
<a href="/admin/home/slides/new">{{( pgettext "Add slide" "action" )}}</a>
|
||||
{{ if .Slides -}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{( pgettext "Image" "header" )}}</th>
|
||||
<th scope="col">{{( pgettext "Caption" "header" )}}</th>
|
||||
<th scope="col">{{( pgettext "Translations" "campsite type" )}}</th>
|
||||
<th scope="col">{{( pgettext "Actions" "campsite type" )}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $slide := .Slides -}}
|
||||
<tr>
|
||||
<td><a href="/admin/home/slides/{{ .ID }}"><img src="{{ .Media }}" alt=""></a></td>
|
||||
<td><a href="/admin/home/slides/{{ .ID }}">{{ .Caption }}</a></td>
|
||||
<td>
|
||||
{{ range .Translations }}
|
||||
<a
|
||||
{{ if .Missing }}
|
||||
class="missing-translation"
|
||||
{{ end }}
|
||||
href="/admin/home/slides/{{ $slide.ID }}/{{ .Language }}">{{ .Endonym }}</a>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>
|
||||
<form data-hx-delete="/admin/home/slides/{{ .ID }}" data-hx-headers='{ {{ CSRFHeader }} }'>
|
||||
<button type="submit">{{( pgettext "Delete" "action" )}}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ else -}}
|
||||
<p>{{( gettext "No slides added yet." )}}</p>
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -72,6 +72,9 @@
|
|||
<li>
|
||||
<a {{ if ne . "seasons"}}href="/admin/seasons"{{ end }}>{{( pgettext "Seasons" "title" )}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a {{ if ne . "home"}}href="/admin/home"{{ end }}>{{( pgettext "Home Page" "title" )}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{{- end }}
|
||||
|
|
|
@ -39,42 +39,16 @@
|
|||
<p>{{(gettext "Less than an hour from <strong>Girona</strong>, one from <strong>La Bisbal d’Empordà</strong>, and two from <strong>Barcelona</strong>.") | raw}}</p>
|
||||
<p><a href="/{{currentLocale}}/surroundings">{{( gettext "Discover the surroundings" )}} <span>→</span></a></p>
|
||||
</div>
|
||||
<figure>
|
||||
<img src="/static/images/Volca_de_Santa_Margarida.jpg" alt=""/>
|
||||
<figcaption>{{( gettext "Legend" )}}</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="/static/images/Gorga_fosca_Sadernes.jpg" alt=""/>
|
||||
<figcaption>{{( gettext "Legend" )}}</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="/static/images/castellfolit_de_la_roca.jpg" alt=""/>
|
||||
<figcaption>{{( gettext "Legend" )}}</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="/static/images/besalu.jpg" alt=""/>
|
||||
<figcaption>{{( gettext "Legend" )}}</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="/static/images/santa_pau.jpg" alt=""/>
|
||||
<figcaption>{{( gettext "Legend" )}}</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="/static/images/banyoles.jpg" alt=""/>
|
||||
<figcaption>{{( gettext "Legend" )}}</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="/static/images/girn-a.jpg" alt=""/>
|
||||
<figcaption>{{( gettext "Legend" )}}</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="/static/images/costa_brava.jpg" alt=""/>
|
||||
<figcaption>{{( gettext "Legend" )}}</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="/static/images/barcelona-1.jpg" alt=""/>
|
||||
<figcaption>{{( gettext "Legend" )}}</figcaption>
|
||||
</figure>
|
||||
{{ range .Carousel -}}
|
||||
{{ if .Caption -}}
|
||||
<figure>
|
||||
<img src="{{ .Media }}" alt=""/>
|
||||
<figcaption>{{ .Caption }}</figcaption>
|
||||
</figure>
|
||||
{{- else -}}
|
||||
<img src="{{ .Media }}" alt=""/>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
</div>
|
||||
</section>
|
||||
<p class="enjoy">{{( gettext "Come and enjoy!")}}</p>
|
||||
|
|