Add remove_campsite_type_option function

This commit is contained in:
jordi fita mas 2024-01-22 20:54:03 +01:00
parent 5cc5fca6b5
commit bc790762d6
11 changed files with 247 additions and 40 deletions

View File

@ -0,0 +1,24 @@
-- Deploy camper:remove_campsite_type_option to pg
-- requires: roles
-- requires: schema_camper
-- requires: campsite_type_option
-- requires: campsite_type_option_i18n
-- requires: campsite_type_option_cost
begin;
set search_path to camper, public;
create or replace function remove_campsite_type_option(option_id integer) returns void as
$$
delete from campsite_type_option_cost where campsite_type_option_id = option_id;
delete from campsite_type_option_i18n where campsite_type_option_id = option_id;
delete from campsite_type_option where campsite_type_option_id = option_id;
$$
language sql
;
revoke execute on function remove_campsite_type_option(integer) from public;
grant execute on function remove_campsite_type_option(integer) to admin;
commit;

View File

@ -88,8 +88,10 @@ func (h *AdminHandler) optionHandler(user *auth.User, company *auth.Company, con
f.MustRender(w, r, user, company)
case http.MethodPut:
editOption(w, r, user, company, conn, f)
case http.MethodDelete:
deleteOption(w, r, user, conn, f.TypeSlug, f.ID)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut, http.MethodDelete)
}
default:
http.NotFound(w, r)
@ -194,6 +196,17 @@ func editOption(w http.ResponseWriter, r *http.Request, user *auth.User, company
})
}
func deleteOption(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn, typeSlug string, id int) {
if err := user.VerifyCSRFToken(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if err := conn.RemoveCampsiteTypeOption(r.Context(), id); err != nil {
panic(err)
}
httplib.Redirect(w, r, "/admin/campsites/types/"+typeSlug+"/options", http.StatusSeeOther)
}
func processOptionForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *optionForm, act func(ctx context.Context, tx *database.Tx) error) {
if ok, err := form.Handle(f, w, r, user); err != nil {
return

View File

@ -51,6 +51,11 @@ func (tx *Tx) TranslateCampsiteTypeOption(ctx context.Context, id int, langTag l
return err
}
func (c *Conn) RemoveCampsiteTypeOption(ctx context.Context, id int) error {
_, err := c.Exec(ctx, "select remove_campsite_type_option($1)", id)
return err
}
func (c *Conn) OrderCampsiteTypeOptions(ctx context.Context, ids []int) error {
_, err := c.Exec(ctx, "select order_campsite_type_options($1)", ids)
return err

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2024-01-22 19:51+0100\n"
"POT-Creation-Date: 2024-01-22 20:50+0100\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"
@ -655,6 +655,7 @@ msgid "Caption"
msgstr "Llegenda"
#: web/templates/admin/campsite/carousel/index.gohtml:32
#: web/templates/admin/campsite/option/index.gohtml:31
#: web/templates/admin/services/index.gohtml:30
#: web/templates/admin/services/index.gohtml:75
#: web/templates/admin/user/index.gohtml:23
@ -672,6 +673,7 @@ msgid "Are you sure you wish to delete this slide?"
msgstr "Esteu segur de voler esborrar aquesta diapositiva?"
#: web/templates/admin/campsite/carousel/index.gohtml:50
#: web/templates/admin/campsite/option/index.gohtml:47
#: web/templates/admin/services/index.gohtml:47
#: web/templates/admin/services/index.gohtml:91
#: web/templates/admin/user/index.gohtml:37
@ -760,7 +762,11 @@ msgctxt "action"
msgid "Add Option"
msgstr "Afegeix opció"
#: web/templates/admin/campsite/option/index.gohtml:47
#: web/templates/admin/campsite/option/index.gohtml:35
msgid "Are you sure you wish to delete this option?"
msgstr "Esteu segur de voler esborrar aquesta opció?"
#: web/templates/admin/campsite/option/index.gohtml:56
msgid "No campsite type options added yet."
msgstr "No sha afegit cap opció al tipus dallotjament encara."
@ -1425,14 +1431,14 @@ msgstr "Estat"
msgid "No booking found."
msgstr "No sha trobat cap reserva."
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:344
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:357
#: pkg/campsite/types/feature.go:259 pkg/campsite/types/admin.go:483
#: pkg/season/admin.go:412 pkg/services/admin.go:316
#: pkg/surroundings/admin.go:321
msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc."
#: pkg/legal/admin.go:259 pkg/campsite/types/option.go:345
#: pkg/legal/admin.go:259 pkg/campsite/types/option.go:358
#: pkg/campsite/types/feature.go:260 pkg/campsite/types/admin.go:484
msgid "Name must have at least one letter."
msgstr "El nom ha de tenir com a mínim una lletra."
@ -1496,39 +1502,39 @@ msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida."
msgid "Access forbidden"
msgstr "Accés prohibit"
#: pkg/campsite/types/option.go:348
#: pkg/campsite/types/option.go:361
msgid "Minimum can not be empty."
msgstr "No podeu deixar el mínim en blanc."
#: pkg/campsite/types/option.go:349
#: pkg/campsite/types/option.go:362
msgid "Minimum must be an integer number."
msgstr "El valor del mínim ha de ser un número enter."
#: pkg/campsite/types/option.go:351
#: pkg/campsite/types/option.go:364
msgid "Minimum must be zero or greater."
msgstr "El valor del mínim ha de ser com a mínim zero."
#: pkg/campsite/types/option.go:354
#: pkg/campsite/types/option.go:367
msgid "Maximum can not be empty."
msgstr "No podeu deixar el màxim en blanc."
#: pkg/campsite/types/option.go:355
#: pkg/campsite/types/option.go:368
msgid "Maximum must be an integer number."
msgstr "El valor del màxim ha de ser un número enter."
#: pkg/campsite/types/option.go:357
#: pkg/campsite/types/option.go:370
msgid "Maximum must be equal or greater than minimum."
msgstr "El valor del màxim ha de ser igual o superir al del mínim."
#: pkg/campsite/types/option.go:361 pkg/campsite/types/admin.go:499
#: pkg/campsite/types/option.go:374 pkg/campsite/types/admin.go:499
msgid "Price per night can not be empty."
msgstr "No podeu deixar el preu per nit en blanc."
#: pkg/campsite/types/option.go:362 pkg/campsite/types/admin.go:500
#: pkg/campsite/types/option.go:375 pkg/campsite/types/admin.go:500
msgid "Price per night must be a decimal number."
msgstr "El preu per nit ha de ser un número decimal."
#: pkg/campsite/types/option.go:363 pkg/campsite/types/admin.go:501
#: pkg/campsite/types/option.go:376 pkg/campsite/types/admin.go:501
msgid "Price per night must be zero or greater."
msgstr "El preu per nit ha de ser com a mínim zero."

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2024-01-22 19:51+0100\n"
"POT-Creation-Date: 2024-01-22 20:50+0100\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"
@ -655,6 +655,7 @@ msgid "Caption"
msgstr "Leyenda"
#: web/templates/admin/campsite/carousel/index.gohtml:32
#: web/templates/admin/campsite/option/index.gohtml:31
#: web/templates/admin/services/index.gohtml:30
#: web/templates/admin/services/index.gohtml:75
#: web/templates/admin/user/index.gohtml:23
@ -672,6 +673,7 @@ msgid "Are you sure you wish to delete this slide?"
msgstr "¿Estáis seguro de querer borrar esta diapositiva?"
#: web/templates/admin/campsite/carousel/index.gohtml:50
#: web/templates/admin/campsite/option/index.gohtml:47
#: web/templates/admin/services/index.gohtml:47
#: web/templates/admin/services/index.gohtml:91
#: web/templates/admin/user/index.gohtml:37
@ -760,7 +762,11 @@ msgctxt "action"
msgid "Add Option"
msgstr "Añadir opción"
#: web/templates/admin/campsite/option/index.gohtml:47
#: web/templates/admin/campsite/option/index.gohtml:35
msgid "Are you sure you wish to delete this option?"
msgstr "¿Estáis seguro de querer borrar esta opción?"
#: web/templates/admin/campsite/option/index.gohtml:56
msgid "No campsite type options added yet."
msgstr "No se ha añadido ninguna opció al tipo de alojamiento todavía."
@ -1425,14 +1431,14 @@ msgstr "Estado"
msgid "No booking found."
msgstr "No se ha encontrado ninguna reserva."
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:344
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:357
#: pkg/campsite/types/feature.go:259 pkg/campsite/types/admin.go:483
#: pkg/season/admin.go:412 pkg/services/admin.go:316
#: pkg/surroundings/admin.go:321
msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco."
#: pkg/legal/admin.go:259 pkg/campsite/types/option.go:345
#: pkg/legal/admin.go:259 pkg/campsite/types/option.go:358
#: pkg/campsite/types/feature.go:260 pkg/campsite/types/admin.go:484
msgid "Name must have at least one letter."
msgstr "El nombre tiene que tener como mínimo una letra."
@ -1496,39 +1502,39 @@ msgstr "El archivo tiene que ser una imagen PNG o JPEG válida."
msgid "Access forbidden"
msgstr "Acceso prohibido"
#: pkg/campsite/types/option.go:348
#: pkg/campsite/types/option.go:361
msgid "Minimum can not be empty."
msgstr "No podéis dejar el mínimo en blanco."
#: pkg/campsite/types/option.go:349
#: pkg/campsite/types/option.go:362
msgid "Minimum must be an integer number."
msgstr "El valor de mínimo tiene que ser un número entero."
#: pkg/campsite/types/option.go:351
#: pkg/campsite/types/option.go:364
msgid "Minimum must be zero or greater."
msgstr "El valor de mínimo tiene que ser como mínimo cero."
#: pkg/campsite/types/option.go:354
#: pkg/campsite/types/option.go:367
msgid "Maximum can not be empty."
msgstr "No podéis dejar el máxmimo en blanco."
#: pkg/campsite/types/option.go:355
#: pkg/campsite/types/option.go:368
msgid "Maximum must be an integer number."
msgstr "El valor del máximo tiene que ser un número entero."
#: pkg/campsite/types/option.go:357
#: pkg/campsite/types/option.go:370
msgid "Maximum must be equal or greater than minimum."
msgstr "El valor del máximo tiene que ser igual o mayor al del mínimo."
#: pkg/campsite/types/option.go:361 pkg/campsite/types/admin.go:499
#: pkg/campsite/types/option.go:374 pkg/campsite/types/admin.go:499
msgid "Price per night can not be empty."
msgstr "No podéis dejar el precio por noche en blanco."
#: pkg/campsite/types/option.go:362 pkg/campsite/types/admin.go:500
#: pkg/campsite/types/option.go:375 pkg/campsite/types/admin.go:500
msgid "Price per night must be a decimal number."
msgstr "El precio por noche tiene que ser un número decimal."
#: pkg/campsite/types/option.go:363 pkg/campsite/types/admin.go:501
#: pkg/campsite/types/option.go:376 pkg/campsite/types/admin.go:501
msgid "Price per night must be zero or greater."
msgstr "El precio por noche tiene que ser como mínimo cero."

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2024-01-22 19:51+0100\n"
"POT-Creation-Date: 2024-01-22 20:50+0100\n"
"PO-Revision-Date: 2023-12-20 10:13+0100\n"
"Last-Translator: Oriol Carbonell <info@oriolcarbonell.cat>\n"
"Language-Team: French <traduc@traduc.org>\n"
@ -656,6 +656,7 @@ msgid "Caption"
msgstr "Légende"
#: web/templates/admin/campsite/carousel/index.gohtml:32
#: web/templates/admin/campsite/option/index.gohtml:31
#: web/templates/admin/services/index.gohtml:30
#: web/templates/admin/services/index.gohtml:75
#: web/templates/admin/user/index.gohtml:23
@ -673,6 +674,7 @@ msgid "Are you sure you wish to delete this slide?"
msgstr "Êtes-vous sûr de vouloir supprimer cette diapositive ?"
#: web/templates/admin/campsite/carousel/index.gohtml:50
#: web/templates/admin/campsite/option/index.gohtml:47
#: web/templates/admin/services/index.gohtml:47
#: web/templates/admin/services/index.gohtml:91
#: web/templates/admin/user/index.gohtml:37
@ -761,7 +763,11 @@ msgctxt "action"
msgid "Add Option"
msgstr "Ajouter une option"
#: web/templates/admin/campsite/option/index.gohtml:47
#: web/templates/admin/campsite/option/index.gohtml:35
msgid "Are you sure you wish to delete this option?"
msgstr "Êtes-vous sûr de vouloir supprimer cette option ?"
#: web/templates/admin/campsite/option/index.gohtml:56
msgid "No campsite type options added yet."
msgstr "Aucune option de type de camping na encore été ajoutée."
@ -1426,14 +1432,14 @@ msgstr "Statut"
msgid "No booking found."
msgstr "Aucune réservation trouvée."
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:344
#: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:357
#: pkg/campsite/types/feature.go:259 pkg/campsite/types/admin.go:483
#: pkg/season/admin.go:412 pkg/services/admin.go:316
#: pkg/surroundings/admin.go:321
msgid "Name can not be empty."
msgstr "Le nom ne peut pas être laissé vide."
#: pkg/legal/admin.go:259 pkg/campsite/types/option.go:345
#: pkg/legal/admin.go:259 pkg/campsite/types/option.go:358
#: pkg/campsite/types/feature.go:260 pkg/campsite/types/admin.go:484
msgid "Name must have at least one letter."
msgstr "Le nom doit comporter au moins une lettre."
@ -1497,39 +1503,39 @@ msgstr "Le fichier doit être une image PNG ou JPEG valide."
msgid "Access forbidden"
msgstr "Accès interdit"
#: pkg/campsite/types/option.go:348
#: pkg/campsite/types/option.go:361
msgid "Minimum can not be empty."
msgstr "Le minimum ne peut pas être vide."
#: pkg/campsite/types/option.go:349
#: pkg/campsite/types/option.go:362
msgid "Minimum must be an integer number."
msgstr "Le minimum doit être un nombre entier."
#: pkg/campsite/types/option.go:351
#: pkg/campsite/types/option.go:364
msgid "Minimum must be zero or greater."
msgstr "Le minimum doit être égal ou supérieur à zéro."
#: pkg/campsite/types/option.go:354
#: pkg/campsite/types/option.go:367
msgid "Maximum can not be empty."
msgstr "Le maximum ne peut pas être vide."
#: pkg/campsite/types/option.go:355
#: pkg/campsite/types/option.go:368
msgid "Maximum must be an integer number."
msgstr "Le maximum doit être un nombre entier."
#: pkg/campsite/types/option.go:357
#: pkg/campsite/types/option.go:370
msgid "Maximum must be equal or greater than minimum."
msgstr "Le maximum doit être égal ou supérieur au minimum."
#: pkg/campsite/types/option.go:361 pkg/campsite/types/admin.go:499
#: pkg/campsite/types/option.go:374 pkg/campsite/types/admin.go:499
msgid "Price per night can not be empty."
msgstr "Le prix par nuit ne peut pas être vide."
#: pkg/campsite/types/option.go:362 pkg/campsite/types/admin.go:500
#: pkg/campsite/types/option.go:375 pkg/campsite/types/admin.go:500
msgid "Price per night must be a decimal number."
msgstr "Le prix par nuit doit être un nombre décimal."
#: pkg/campsite/types/option.go:363 pkg/campsite/types/admin.go:501
#: pkg/campsite/types/option.go:376 pkg/campsite/types/admin.go:501
msgid "Price per night must be zero or greater."
msgstr "Le prix par nuit doit être égal ou supérieur."

View File

@ -0,0 +1,7 @@
-- Revert camper:remove_campsite_type_option from pg
begin;
drop function if exists camper.remove_campsite_type_option(integer);
commit;

View File

@ -167,3 +167,4 @@ add_campsite_type [add_campsite_type@v1 campsite_type__check_in_out] 2024-01-22T
edit_campsite_type [edit_campsite_type@v1 campsite_type__check_in_out] 2024-01-22T18:04:34Z jordi fita mas <jordi@tandem.blog> # Add check_in and check_out parameters to edit_campsite_type function
campsite_type_i18n__check_in_out [campsite_type_i18n] 2024-01-22T18:07:21Z jordi fita mas <jordi@tandem.blog> # Add check_in and check_out fields to campsite_type_i18n
translate_campsite_type [translate_campsite_type@v1 campsite_type_i18n__check_in_out] 2024-01-22T18:14:26Z jordi fita mas <jordi@tandem.blog> # Add check_in and check_out parameters to translate_campsite_type function
remove_campsite_type_option [roles schema_camper campsite_type_option campsite_type_option_i18n campsite_type_option_cost] 2024-01-22T19:25:03Z jordi fita mas <jordi@tandem.blog> # Add function to remove campsite type options

View File

@ -0,0 +1,123 @@
-- Test remove_campsite_type_option
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(14);
set search_path to camper, public;
select has_function('camper', 'remove_campsite_type_option', array['integer']);
select function_lang_is('camper', 'remove_campsite_type_option', array['integer'], 'sql');
select function_returns('camper', 'remove_campsite_type_option', array['integer'], 'void');
select isnt_definer('camper', 'remove_campsite_type_option', array['integer']);
select volatility_is('camper', 'remove_campsite_type_option', array['integer'], 'volatile');
select function_privs_are('camper', 'remove_campsite_type_option', array['integer'], 'guest', array[]::text[]);
select function_privs_are('camper', 'remove_campsite_type_option', array['integer'], 'employee', array[]::text[]);
select function_privs_are('camper', 'remove_campsite_type_option', array['integer'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'remove_campsite_type_option', array['integer'], 'authenticator', array[]::text[]);
set client_min_messages to warning;
truncate campsite_type_option_cost cascade;
truncate campsite_type_option_i18n cascade;
truncate campsite_type_option cascade;
truncate season cascade;
truncate media cascade;
truncate media_content 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, rtc_number, tourist_tax, country_code, currency_code, default_lang_tag)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca')
;
insert into media_content (media_type, bytes)
values ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
, ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ff00ff","a"};')
, ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffff00","a"};')
, ('text/plain', 'hello, world!')
, ('image/svg+xml', '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>')
;
insert into media (media_id, company_id, original_filename, content_hash)
values (2, 1, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};'))
, (3, 1, 'cover3.xpm', sha256('static char *s[]={"1 1 1 1","a c #ff00ff","a"};'))
;
insert into campsite_type (campsite_type_id, company_id, slug, media_id, name, description, max_campers, dogs_allowed, active)
values (10, 1, '87452b88-b48f-48d3-bb6c-0296de64164e', 2, 'Type A', '<p>A</p>', 5, false, true)
, (11, 1, '9b6370f7-f941-46f2-bc6e-de455675bd0a', 3, 'Type B', '<p>B</p>', 4, true, false)
;
insert into campsite_type_option (campsite_type_option_id, campsite_type_id, name, range)
values (12, 10, 'Option 1', '[0, 1]')
, (13, 10, 'Option 2', '[0, 1]')
, (14, 11, 'Option 3', '[0, 1]')
, (15, 11, 'Option 4', '[0, 1]')
;
insert into season (season_id, company_id, name)
values (16, 1, 'High')
;
insert into campsite_type_option_cost (campsite_type_option_id, season_id, cost_per_night)
values (12, 16, 44)
, (13, 16, 54)
, (14, 16, 74)
, (15, 16, 84)
;
insert into campsite_type_option_i18n (campsite_type_option_id, lang_tag, name)
values (12, 'ca', 'Opció 1')
, (12, 'es', 'Opción 1')
, (13, 'ca', 'Opció 2')
, (13, 'es', 'Opción 2')
, (14, 'ca', 'Opció 3')
, (14, 'es', 'Opción 3')
, (15, 'ca', 'Opció 4')
, (15, 'es', 'Opción 4')
;
select lives_ok(
$$ select remove_campsite_type_option(12) $$,
'Should be able to delete an option from the first campsite type'
);
select lives_ok(
$$ select remove_campsite_type_option(15) $$,
'Should be able to delete a slide from the second campsite type'
);
select bag_eq(
$$ select campsite_type_option_id, name from campsite_type_option $$,
$$ values (13, 'Option 2')
, (14, 'Option 3')
$$,
'Should have removed the options'
);
select bag_eq(
$$ select campsite_type_option_id, lang_tag, name from campsite_type_option_i18n $$,
$$ values (13, 'ca', 'Opció 2')
, (13, 'es', 'Opción 2')
, (14, 'ca', 'Opció 3')
, (14, 'es', 'Opción 3')
$$,
'Should have removed the options translations'
);
select bag_eq(
$$ select campsite_type_option_id, season_id, cost_per_night from campsite_type_option_cost $$,
$$ values (13, 16, 54)
, (14, 16, 74)
$$,
'Should have removed the options translations'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,7 @@
-- Verify camper:remove_campsite_type_option on pg
begin;
select has_function_privilege('camper.remove_campsite_type_option(integer)', 'execute');
rollback;

View File

@ -28,9 +28,11 @@
<thead>
<tr>
<th scope="col">{{( pgettext "Name" "header" )}}</th>
<th scope="col">{{( pgettext "Actions" "header" )}}</th>
</tr>
</thead>
<tbody>
{{ $confirm := (gettext "Are you sure you wish to delete this option?")}}
{{ range .Options -}}
<tr>
<td>
@ -38,6 +40,13 @@
<input type="hidden" name="option_id" value="{{ .ID }}">
<a href="{{ .URL }}">{{ .Name }}</a>
</td>
<td>
<button data-hx-delete="{{ .URL }}"
data-hx-confirm="{{ $confirm }}"
data-hx-headers='{ {{ CSRFHeader }} }'>
{{( pgettext "Delete" "action" )}}
</button>
</td>
</tr>
{{- end }}
</tbody>