diff --git a/.github/commit-hooks/pre-commit b/.github/commit-hooks/pre-commit index 715a914..f76d55a 100755 --- a/.github/commit-hooks/pre-commit +++ b/.github/commit-hooks/pre-commit @@ -1 +1 @@ -docker compose exec --workdir /var/www/vendor/oxid-esales/module-template php composer static \ No newline at end of file +docker compose exec -T --workdir /var/www/vendor/oxid-esales/module-template php composer static \ No newline at end of file diff --git a/.github/workflows/dispatch_module.yaml b/.github/workflows/dispatch_module.yaml index 835e251..0ca494f 100644 --- a/.github/workflows/dispatch_module.yaml +++ b/.github/workflows/dispatch_module.yaml @@ -8,12 +8,17 @@ on: type: choice options: - 'no' - - 'PHP8.1/MySQL5.7' - - 'PHP8.1/MySQL8.0' - 'PHP8.2/MySQL5.7' - 'PHP8.2/MySQL8.0' - default: 'PHP8.1/MySQL5.7' + - 'PHP8.3/MySQL5.7' + - 'PHP8.3/MySQL8.0' + default: 'PHP8.2/MySQL8.0' description: 'Limit to one PHP/MySQL combination' + use_dev_version: + type: choice + options: ['no', 'v0'] + description: 'Use the dev version of github actions' + default: 'no' jobs: build_testplan: @@ -28,19 +33,20 @@ jobs: # shellcheck disable=SC2088 case "${{ inputs.limit }}" in "no") LIMIT='';; - "PHP8.1/MySQL5.7") LIMIT='~/defaults/php8.1_mysql5.7_only.yaml,' ;; - "PHP8.1/MySQL8.0") LIMIT='~/defaults/php8.1_mysql8.0_only.yaml,' ;; "PHP8.2/MySQL5.7") LIMIT='~/defaults/php8.2_mysql5.7_only.yaml,' ;; "PHP8.2/MySQL8.0") LIMIT='~/defaults/php8.2_mysql8.0_only.yaml,' ;; + "PHP8.3/MySQL5.7") LIMIT='~/defaults/php8.3_mysql5.7_only.yaml,' ;; + "PHP8.3/MySQL8.0") LIMIT='~/defaults/php8.3_mysql8.0_only.yaml,' ;; *) echo "Illegal choice, fix the workflow" exit 1 ;; esac # shellcheck disable=SC2088 - TESTPLAN="~/defaults/7.1.x.yaml,${LIMIT}~/module-template.yaml" + TESTPLAN="~/defaults/7.2.x.yaml,${LIMIT}~/module-template.yaml" echo "testplan=${TESTPLAN}" | tee -a "${GITHUB_OUTPUT}" dispatch_stable: + if: ${{ inputs.use_dev_version == 'no' }} needs: build_testplan uses: oxid-eSales/github-actions/.github/workflows/universal_workflow_light.yaml@v4 with: @@ -57,3 +63,22 @@ jobs: enterprise_github_token: ${{ secrets.enterprise_github_token }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + dispatch_v0: + if: ${{ inputs.use_dev_version == 'v0' }} + needs: build_testplan + uses: oxid-eSales/github-actions/.github/workflows/universal_workflow_light.yaml@v0 + with: + testplan: ${{ needs.build_testplan.outputs.testplan }} + runs_on: '"ubuntu-latest"' + defaults: 'v0' + plan_folder: '.github/oxid-esales' + secrets: + DOCKER_HUB_USER: ${{ secrets.DOCKER_HUB_USER }} + DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }} + CACHE_ENDPOINT: ${{ secrets.CACHE_ENDPOINT }} + CACHE_ACCESS_KEY: ${{ secrets.CACHE_ACCESS_KEY }} + CACHE_SECRET_KEY: ${{ secrets.CACHE_SECRET_KEY }} + enterprise_github_token: ${{ secrets.enterprise_github_token }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/scheduled.yaml b/.github/workflows/scheduled.yaml index e5b21ae..de2a2df 100644 --- a/.github/workflows/scheduled.yaml +++ b/.github/workflows/scheduled.yaml @@ -11,7 +11,7 @@ jobs: if: always() uses: oxid-eSales/github-actions/.github/workflows/universal_workflow_light.yaml@v4 with: - testplan: '~/defaults/7.1.x.yaml,~/defaults/scheduled.yaml,~/module-template.yaml' + testplan: '~/defaults/7.2.x.yaml,~/defaults/scheduled.yaml,~/module-template.yaml' runs_on: '"ubuntu-latest"' defaults: 'v4' plan_folder: '.github/oxid-esales' diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ee93e4..d310bab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [v3.1.0] - unreleased + +### Added +- Admin controller example in Greeting namespace +- Template for special greeting admin controller +- Example with extending of current admin template +- ``oxNew`` model object factory example in Greeting namespace infrastructure + +### Fixed +- "the input device is not a TTY" error message during github hook if problems found + +## [v3.1.0] - Unreleased + +### Removed +- PHP 8.1 support removed + ## [v3.0.0] - 2024-06-27 This is the stable release for v3.0.0. No changes have been made since v3.0.0-rc.1. @@ -29,6 +45,14 @@ This is the stable release for v3.0.0. No changes have been made since v3.0.0-rc - Smarty support - Migration are not triggered anymore on module activation. Ensure you run them separately after module **Installation**. +## [v2.2.0] - unreleased + +### Added +- Admin controller +- Template for admin controller +- Example of extending of current admin template +- ``oxNew`` object factory example + ## [v2.1.0] - 2024-05-30 ### Added diff --git a/README.md b/README.md index a906325..02cc03e 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,8 @@ Story: * extending a shop model (`OxidEsales\ModuleTemplate\Extension\Model\User`) / (`OxidEsales\ModuleTemplate\Extension\Model\Basket`) * extending a shop controller (`OxidEsales\ModuleTemplate\Extension\Controller\StartController`) * extending a shop database table (`oxuser`) -* extending a shop template block (`start_welcome_text`) +* extending a shop template block (`start_newest_articles`) +* extending a shop admin template block (`admin_user_main_form` - only an extension of a block, without functionality) **HINT**: only extend the shop core if there is no other way like listen and handle shop events, decorate/replace some DI service. Your module might be one of many in the class chain and you should @@ -227,10 +228,12 @@ If you need to extend the shop class chain by overwriting, try to stick to the p #### Sometimes we need to bring our own * own module controller (`oemtgreeting` with own template and own translations) +* own module admin controller (`oemt_admin_greeting` with own template and own translations) * module setting (`oemoduletemplate_GreetingMode`) * event subscriber (`OxidEsales\ModuleTemplate\Tracker\Subscriber\BeforeModelUpdate`) * model with a database (`OxidEsales\ModuleTemplate\Tracker\Model\GreetingTracker`) * DI service examples +* ``oxNew`` object factory example (`OxidEsales\ModuleTemplate\Greeting\Infrastructure\UserModelFactory`) #### Whatever you do, ensure it is covered with tests * unit/integration test diff --git a/composer.json b/composer.json index 5302c70..bfb6c3e 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "phpstan/phpstan": "^1.9.14", "squizlabs/php_codesniffer": "3.*", "phpmd/phpmd": "^2.11", - "oxid-esales/oxideshop-ce": "dev-b-7.1.x", + "oxid-esales/oxideshop-ce": "dev-b-7.2.x", "phpunit/phpunit": "~10.5.17", "mikey179/vfsstream": "~1.6.8", "codeception/codeception": "*", diff --git a/menu.xml b/menu.xml new file mode 100644 index 0000000..be2a624 --- /dev/null +++ b/menu.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/metadata.php b/metadata.php index 3fda7f7..0ede64c 100644 --- a/metadata.php +++ b/metadata.php @@ -28,7 +28,8 @@ \OxidEsales\Eshop\Application\Model\User::class => \OxidEsales\ModuleTemplate\Extension\Model\User::class, ], 'controllers' => [ - 'oemtgreeting' => \OxidEsales\ModuleTemplate\Greeting\Controller\GreetingController::class + 'oemtgreeting' => \OxidEsales\ModuleTemplate\Greeting\Controller\GreetingController::class, + 'oemt_admin_greeting' => \OxidEsales\ModuleTemplate\Greeting\Controller\Admin\GreetingAdminController::class, ], 'events' => [ 'onActivate' => '\OxidEsales\ModuleTemplate\Core\ModuleEvents::onActivate', diff --git a/src/Core/Module.php b/src/Core/Module.php index fd95178..64cf660 100644 --- a/src/Core/Module.php +++ b/src/Core/Module.php @@ -18,4 +18,6 @@ final class Module public const OEMT_COUNTER_TEMPLATE_VARNAME = 'oemt_greeting_counter'; public const DEFAULT_PERSONAL_GREETING_LANGUAGE_CONST = 'OEMODULETEMPLATE_GREETING_GENERIC'; + + public const OEMT_ADMIN_GREETING_TEMPLATE_VARNAME = 'greeting_message'; } diff --git a/src/Greeting/Controller/Admin/GreetingAdminController.php b/src/Greeting/Controller/Admin/GreetingAdminController.php new file mode 100644 index 0000000..ba03e10 --- /dev/null +++ b/src/Greeting/Controller/Admin/GreetingAdminController.php @@ -0,0 +1,32 @@ +getService(UserServiceInterface::class); + if ($this->getEditObjectId()) { + /** @var TemplateModelUser $oUser */ + $oUser = $userService->getUserById($this->getEditObjectId()); + $this->addTplParam(ModuleCore::OEMT_ADMIN_GREETING_TEMPLATE_VARNAME, $oUser->getPersonalGreeting()); + } + + return parent::render(); + } +} diff --git a/src/Greeting/Infrastructure/UserModelFactory.php b/src/Greeting/Infrastructure/UserModelFactory.php new file mode 100644 index 0000000..ee02c1c --- /dev/null +++ b/src/Greeting/Infrastructure/UserModelFactory.php @@ -0,0 +1,23 @@ +userModelFactory = $userModelFactory; + } + + public function getUserById(string $userId): EshopModelUser + { + $userModel = $this->userModelFactory->create(); + $userModel->load($userId); + + return $userModel; + } +} diff --git a/src/Greeting/Service/UserServiceInterface.php b/src/Greeting/Service/UserServiceInterface.php new file mode 100644 index 0000000..df3d645 --- /dev/null +++ b/src/Greeting/Service/UserServiceInterface.php @@ -0,0 +1,17 @@ +setGreetingModeGeneric(); + $this->setUserPersonalGreeting($I, 'Hello there!'); + } + + public function _after(AcceptanceTester $I): void + { + //clean up after each test + $I->setGreetingModeGeneric(); + } + + /** @param AcceptanceTester $I */ + public function seeGreetingOptionsForUser(AcceptanceTester $I): void + { + $I->openAdmin(); + $adminPage = $I->loginAdmin(); + + $userList = $adminPage->openUsers(); + $userList->find("where[oxuser][oxusername]", $I->getDemoUserName()); + + $I->selectEditFrame(); + $I->see(Translator::translate('OEMODULETEMPLATE_ALLOW_GREETING')); + + $I->selectListFrame(); + $I->click(Translator::translate('tbcluser_greetings')); + + $I->selectEditFrame(); + $I->see(Translator::translate('OEMODULETEMPLATE_GREETING_TITLE')); + $I->see('Hello there!'); + } + + private function setUserPersonalGreeting(AcceptanceTester $I, string $value = ''): void + { + $I->updateInDatabase( + 'oxuser', + [ + 'oemtgreeting' => $value, + ], + [ + 'oxusername' => $I->getDemoUserName(), + ] + ); + } +} diff --git a/tests/Codeception/Acceptance/GreetingCest.php b/tests/Codeception/Acceptance/GreetingCest.php index 8089231..0696853 100644 --- a/tests/Codeception/Acceptance/GreetingCest.php +++ b/tests/Codeception/Acceptance/GreetingCest.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace OxidEsales\ModuleTemplate\Tests\Codeception\Helper; +namespace OxidEsales\ModuleTemplate\Tests\Codeception\Acceptance; use OxidEsales\Codeception\Module\Translation\Translator; use OxidEsales\ModuleTemplate\Tests\Codeception\Support\AcceptanceTester; diff --git a/tests/Codeception/Acceptance/ModuleCest.php b/tests/Codeception/Acceptance/ModuleCest.php index 9242193..88df3b5 100644 --- a/tests/Codeception/Acceptance/ModuleCest.php +++ b/tests/Codeception/Acceptance/ModuleCest.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace OxidEsales\ModuleTemplate\Tests\Codeception\Helper; +namespace OxidEsales\ModuleTemplate\Tests\Codeception\Acceptance; use OxidEsales\Codeception\Module\Translation\Translator; use OxidEsales\ModuleTemplate\Core\Module; diff --git a/tests/Codeception/Acceptance/UpdateGreetingCest.php b/tests/Codeception/Acceptance/UpdateGreetingCest.php index 748c1eb..2af51e1 100644 --- a/tests/Codeception/Acceptance/UpdateGreetingCest.php +++ b/tests/Codeception/Acceptance/UpdateGreetingCest.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace OxidEsales\ModuleTemplate\Tests\Codeception\Helper; +namespace OxidEsales\ModuleTemplate\Tests\Codeception\Acceptance; use OxidEsales\Codeception\Module\Translation\Translator; use OxidEsales\ModuleTemplate\Core\Module as ModuleCore; diff --git a/tests/Codeception/Support/AcceptanceTester.php b/tests/Codeception/Support/AcceptanceTester.php index eb1acbd..6a18554 100644 --- a/tests/Codeception/Support/AcceptanceTester.php +++ b/tests/Codeception/Support/AcceptanceTester.php @@ -10,6 +10,8 @@ namespace OxidEsales\ModuleTemplate\Tests\Codeception\Support; use Codeception\Util\Fixtures; +use OxidEsales\Codeception\Admin\AdminLoginPage; +use OxidEsales\Codeception\Admin\AdminPanel; use OxidEsales\Codeception\Page\Home; use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; use OxidEsales\Facts\Facts; @@ -91,6 +93,21 @@ public function getShopUrl(): string return $facts->getShopUrl(); } + public function openAdmin(): AdminLoginPage + { + $I = $this; + $adminLogin = new AdminLoginPage($I); + $I->amOnPage($adminLogin->URL); + return $adminLogin; + } + + public function loginAdmin(): AdminPanel + { + $adminPage = $this->openAdmin(); + $admin = Fixtures::get('adminUser'); + return $adminPage->login($admin['email'], $admin['password']); + } + protected function getServiceFromContainer(string $serviceName) { return ContainerFactory::getInstance() diff --git a/tests/Codeception/Support/Data/fixtures.php b/tests/Codeception/Support/Data/fixtures.php index f0a0ec2..8203c99 100644 --- a/tests/Codeception/Support/Data/fixtures.php +++ b/tests/Codeception/Support/Data/fixtures.php @@ -13,4 +13,9 @@ 'email' => 'user@oxid-esales.com', 'password' => 'useruser', ], + 'adminUser' => [ + 'OXID' => 'oxadminuser', + 'email' => 'admin@oxid-esales.com', + 'password' => 'useruser', + ], ]; diff --git a/tests/Codeception/Support/Data/fixtures.sql b/tests/Codeception/Support/Data/fixtures.sql index b46ea9b..f0c7152 100644 --- a/tests/Codeception/Support/Data/fixtures.sql +++ b/tests/Codeception/Support/Data/fixtures.sql @@ -1,3 +1,4 @@ #Add default user REPLACE INTO `oxuser` (`OXID`, `OXACTIVE`, `OXRIGHTS`, `OXSHOPID`, `OXUSERNAME`, `OXPASSWORD`, `OXPASSSALT`, `OXCREATE`, `OXREGISTER`, `OXTIMESTAMP`, `OXBIRTHDATE`) VALUES -('oxdefaultuser',1,'user',1,'user@oxid-esales.com','$2y$10$ljaDXMPHOyC7ELlnC5ErK.3ET4B0oAN3WVr/Tk.RKlUfiuBcQEVVC','', '2003-01-01 00:00:00', '2003-01-01 00:00:00', '2003-01-01 00:00:00', '1985-01-01'); +('oxdefaultuser',1,'user',1,'user@oxid-esales.com','$2y$10$ljaDXMPHOyC7ELlnC5ErK.3ET4B0oAN3WVr/Tk.RKlUfiuBcQEVVC','', '2003-01-01 00:00:00', '2003-01-01 00:00:00', '2003-01-01 00:00:00', '1985-01-01'), +('oxadminuser',1,'malladmin',1,'admin@oxid-esales.com','$2y$10$ljaDXMPHOyC7ELlnC5ErK.3ET4B0oAN3WVr/Tk.RKlUfiuBcQEVVC','', '2003-01-01 00:00:00', '2003-01-01 00:00:00', '2003-01-01 00:00:00', '1985-01-01'); diff --git a/tests/Integration/Greeting/Controller/Admin/GreetingAdminControllerTest.php b/tests/Integration/Greeting/Controller/Admin/GreetingAdminControllerTest.php new file mode 100644 index 0000000..fb7e1bf --- /dev/null +++ b/tests/Integration/Greeting/Controller/Admin/GreetingAdminControllerTest.php @@ -0,0 +1,52 @@ +createTestUser(); + + $controller = oxNew(GreetingAdminController::class); + $controller->setEditObjectId(self::TEST_USER_ID); + + $this->assertSame('@oe_moduletemplate/admin/user_greetings', $controller->render()); + + $viewData = $controller->getViewData(); + + $this->assertSame(self::TEST_GREETING, $viewData[ModuleCore::OEMT_ADMIN_GREETING_TEMPLATE_VARNAME]); + } + + private function createTestUser(): void + { + $user = oxNew(EshopModelUser::class); + $user->assign( + [ + 'oxid' => self::TEST_USER_ID, + 'oemtgreeting' => self::TEST_GREETING, + ] + ); + $user->save(); + } +} diff --git a/tests/Integration/Greeting/Service/GreetingMessageServiceTest.php b/tests/Integration/Greeting/Service/GreetingMessageServiceTest.php index 8202911..6d04324 100644 --- a/tests/Integration/Greeting/Service/GreetingMessageServiceTest.php +++ b/tests/Integration/Greeting/Service/GreetingMessageServiceTest.php @@ -54,7 +54,7 @@ private function getSut( ): GreetingMessageService { return new GreetingMessageService( moduleSettings: $moduleSettings ?? $this->createStub(ModuleSettingsServiceInterface::class), - shopRequest: $shopRequest ?? $this->createStub(CoreRequest::class), + shopRequest: $this->createStub(CoreRequest::class), shopLanguage: $shopLanguage ?? $this->createStub(CoreLanguage::class), ); } diff --git a/tests/Unit/Greeting/Infrastructure/UserModelFactoryTest.php b/tests/Unit/Greeting/Infrastructure/UserModelFactoryTest.php new file mode 100644 index 0000000..2e4bd71 --- /dev/null +++ b/tests/Unit/Greeting/Infrastructure/UserModelFactoryTest.php @@ -0,0 +1,29 @@ +getMockBuilder(UserModelFactory::class) + ->onlyMethods(['create']) + ->getMock(); + + $this->assertInstanceOf(User::class, $coreRequestFactoryMock->create()); + } +} diff --git a/views/admin_twig/de/module_options.php b/views/admin_twig/de/module_options.php index e9e60fe..2befaa8 100644 --- a/views/admin_twig/de/module_options.php +++ b/views/admin_twig/de/module_options.php @@ -9,6 +9,15 @@ $aLang = [ 'charset' => 'UTF-8', + 'tbcluser_greetings' => 'Greetings', + + 'OEMODULETEMPLATE_GREETING_TITLE' => 'Beispiel Admin Controller', + 'OEMODULETEMPLATE_GREETING_MESSAGE_TEXT' => 'Begrüßungsnachricht: ', + 'OEMODULETEMPLATE_NO_GREETING_TEXT' => 'Es wurde keine Begrüßungsnachricht hinzugefügt!', + 'OEMODULETEMPLATE_ALLOW_GREETING' => 'Benutzer erlauben die Begrüßungsnachricht zu setzen', + 'OEMODULETEMPLATE_HELP_ALLOW_GREETING' => 'Dies ist ein Beispiel, wie ein Admintemplate erweitert werden kann. Momentan ist keine Funktionalität hinter dieser Checkbox hinterlegt, es wird keine Einstellung in der Datenbank gespeichert.', + + # Module settings 'SHOP_MODULE_GROUP_oemoduletemplate_main' => 'Einstellungen', 'SHOP_MODULE_oemoduletemplate_GreetingMode' => 'Begrüßungsmodus', 'SHOP_MODULE_oemoduletemplate_GreetingMode_generic' => 'höflich', diff --git a/views/admin_twig/en/module_options.php b/views/admin_twig/en/module_options.php index 80de61c..839a250 100644 --- a/views/admin_twig/en/module_options.php +++ b/views/admin_twig/en/module_options.php @@ -9,6 +9,15 @@ $aLang = [ 'charset' => 'UTF-8', + 'tbcluser_greetings' => 'Greetings', + + 'OEMODULETEMPLATE_GREETING_TITLE' => 'Admin controller example', + 'OEMODULETEMPLATE_GREETING_MESSAGE_TEXT' => 'Greeting message: ', + 'OEMODULETEMPLATE_NO_GREETING_TEXT' => 'No greeting message added!', + 'OEMODULETEMPLATE_ALLOW_GREETING' => 'Allow user to set greeting', + 'OEMODULETEMPLATE_HELP_ALLOW_GREETING' => 'This is an example of extending admin template. There is no functionality implemented behind this checkbox and does not save anything to the database', + + # Module settings 'SHOP_MODULE_GROUP_oemoduletemplate_main' => 'Settings', 'SHOP_MODULE_oemoduletemplate_GreetingMode' => 'Greeting mode', 'SHOP_MODULE_oemoduletemplate_GreetingMode_generic' => 'generic', diff --git a/views/twig/admin/user_greetings.html.twig b/views/twig/admin/user_greetings.html.twig new file mode 100644 index 0000000..26b8789 --- /dev/null +++ b/views/twig/admin/user_greetings.html.twig @@ -0,0 +1,18 @@ +{% include "headitem.html.twig" with {title: "GENERAL_ADMIN_TITLE"|translate} %} + +
+ {{ oViewConf.getHiddenSid()|raw }} + + +
+ +

{{ translate({ ident: "OEMODULETEMPLATE_GREETING_TITLE" }) }}

+ +{% if greeting_message %} +
{{ translate({ ident: "OEMODULETEMPLATE_GREETING_MESSAGE_TEXT" }) }} {{ greeting_message }}
+{% else %} +
{{ translate({ ident: "OEMODULETEMPLATE_NO_GREETING_TEXT" }) }}
+{% endif %} + +{% include "bottomnaviitem.html.twig" %} +{% include "bottomitem.html.twig" %} diff --git a/views/twig/extensions/themes/admin_twig/user_main.html.twig b/views/twig/extensions/themes/admin_twig/user_main.html.twig new file mode 100644 index 0000000..77b240d --- /dev/null +++ b/views/twig/extensions/themes/admin_twig/user_main.html.twig @@ -0,0 +1,16 @@ +{% extends 'user_main.html.twig' %} + +{% block admin_user_main_form %} + + + + {{ translate({ ident: "OEMODULETEMPLATE_ALLOW_GREETING" }) }} + + + + {% include "inputhelp.html.twig" with {'sHelpId': help_id("OEMODULETEMPLATE_HELP_ALLOW_GREETING"), 'sHelpText': help_text("OEMODULETEMPLATE_HELP_ALLOW_GREETING")} %} + + + + {{ parent() }} +{% endblock %}