feat: integrate travelinfo with myep API to fetch travel data

This commit is contained in:
Björn Fromme
2025-05-15 11:34:27 +02:00
parent c492a524c4
commit e8f21b1eff
14 changed files with 347 additions and 119 deletions
+4
View File
@@ -0,0 +1,4 @@
services:
web:
external_links:
- "ddev-router:myep-next.ddev.site"
+1
View File
@@ -26,6 +26,7 @@
"helhum/typo3-console": "^6.4",
"kigkonsult/icalcreator": "^2.29",
"league/csv": "^9.2",
"league/oauth2-client": "^2.8",
"league/period": "^4.9",
"lochmueller/staticfilecache": "^12.5",
"sjbr/sr-freecap": "^2.6",
Generated
+66 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f4b99aa9b07e56210c516195545a1d40",
"content-hash": "4f59a07672e06df6976e44c1999be0f2",
"packages": [
{
"name": "b13/container",
@@ -1747,6 +1747,71 @@
],
"time": "2022-01-04T00:13:07+00:00"
},
{
"name": "league/oauth2-client",
"version": "2.8.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/oauth2-client.git",
"reference": "9df2924ca644736c835fc60466a3a60390d334f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/9df2924ca644736c835fc60466a3a60390d334f9",
"reference": "9df2924ca644736c835fc60466a3a60390d334f9",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/guzzle": "^6.5.8 || ^7.4.5",
"php": "^7.1 || >=8.0.0 <8.5.0"
},
"require-dev": {
"mockery/mockery": "^1.3.5",
"php-parallel-lint/php-parallel-lint": "^1.4",
"phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
"squizlabs/php_codesniffer": "^3.11"
},
"type": "library",
"autoload": {
"psr-4": {
"League\\OAuth2\\Client\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Alex Bilbie",
"email": "[email protected]",
"homepage": "http://www.alexbilbie.com",
"role": "Developer"
},
{
"name": "Woody Gilk",
"homepage": "https://github.com/shadowhand",
"role": "Contributor"
}
],
"description": "OAuth 2.0 Client Library",
"keywords": [
"Authentication",
"SSO",
"authorization",
"identity",
"idp",
"oauth",
"oauth2",
"single sign on"
],
"support": {
"issues": "https://github.com/thephpleague/oauth2-client/issues",
"source": "https://github.com/thephpleague/oauth2-client/tree/2.8.1"
},
"time": "2025-02-26T04:37:30+00:00"
},
{
"name": "league/period",
"version": "4.12.0",
+6 -9
View File
@@ -255,28 +255,25 @@ routeEnhancers:
- 2736
routes:
-
routePath: '/{travel_info}/{travel_date}'
_controller: 'Travelinfo::index'
routePath: '/{travel_info}'
_controller: 'Travelinfo::travelInfo'
_arguments:
travel_info: travelinfo
travel_date: travelDate
travel_info: travelInfo
-
routePath: '/{travel_code}'
_controller: 'Travelinfo::travelCode'
_arguments:
travel_code: travelCode
defaultController: 'Travelinfo::index'
defaultController: 'Travelinfo::travelInfo'
requirements:
travel_code: '[A-Z]+\d{6}$'
aspects:
travel_info:
type: PersistedAliasMapper
tableName: tx_epproducts_domain_model_travelinfo
routeFieldName: path_segment
travel_date:
type: PassthroughMapper
travel_code:
type: PassthroughMapper
requirements:
travel_date: '\d{4}\-\d{2}\-\d{2}|\d{2}\d{2}\d{4}'
PageTypeSuffix:
type: PageType
default: /
@@ -2,10 +2,10 @@
namespace EP\EpProducts\Controller;
use EP\EpProducts\Domain\Model\Date;
use EP\EpProducts\Domain\Model\Travelinfo;
use EP\EpProducts\Domain\Repository\DateRepository;
use EP\EpProducts\Domain\Repository\TravelinfoRepository;
use EP\EpProducts\MyEP\ApiClient;
use EP\EpProducts\MyEP\ApiException;
use TYPO3\CMS\Core\Http\ImmediateResponseException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
@@ -14,52 +14,34 @@ use TYPO3\CMS\Frontend\Controller\ErrorController;
class TravelinfoController extends ActionController
{
/**
* @var DateRepository
* @var ApiClient
*/
private $dateRepository;
private $apiClient;
/**
* @var TravelinfoRepository
*/
private $travelinfoRepository;
public function __construct(
DateRepository $dateRepository,
TravelinfoRepository $travelinfoRepository
) {
$this->dateRepository = $dateRepository;
public function __construct(ApiClient $apiClient, TravelinfoRepository $travelinfoRepository)
{
$this->apiClient = $apiClient;
$this->travelinfoRepository = $travelinfoRepository;
}
public function travelCodeAction(string $travelCode)
{
$travelCode = strtoupper($travelCode);
$travelCodeDate = str_replace('-', '/', $travelCode);
// find specific travel info by code including travel date first
$travelInfo = $this->travelinfoRepository->findOneByTravelCode($travelCode);
/** @var Date $date */
$date = $this->dateRepository->findOneByCode($travelCodeDate);
if (null === $date) {
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
$GLOBALS['TYPO3_REQUEST'],
'Termin nicht gefunden'
);
throw new ImmediateResponseException($response, 1731071856);
// find general travel info by code without travel date as fallback
if (null === $travelInfo) {
$travelCodeBase = substr($travelCode, 0, -6);
$travelInfo = $this->travelinfoRepository->findOneByTravelCode($travelCodeBase);
}
$product = $date->getProduct();
if (null === $product) {
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
$GLOBALS['TYPO3_REQUEST'],
'Produkt nicht gefunden'
);
throw new ImmediateResponseException($response, 1731071856);
}
$travelinfo = $this->travelinfoRepository->findOneByProduct($product);
if (null === $travelinfo) {
// return 404 in case no travel info is available as last resort
if (null === $travelInfo) {
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
$GLOBALS['TYPO3_REQUEST'],
'Reiseinformationen nicht gefunden'
@@ -67,22 +49,39 @@ class TravelinfoController extends ActionController
throw new ImmediateResponseException($response, 1731071856);
}
$this->view->assign('travelinfo', $travelinfo);
$this->view->assign('date', $date);
$this->handleRequest($travelCode, $travelInfo);
}
public function indexAction(Travelinfo $travelinfo, string $travelDate)
public function travelInfoAction(Travelinfo $travelInfo)
{
$product = $travelinfo->getProduct();
$travelCode = $travelInfo->getTravelCode();
if (preg_match('/^\d{2}\d{2}\d{4}$/', $travelDate)) {
$travelDate = (\DateTimeImmutable::createFromFormat('dmY', $travelDate))->format('Y-m-d');
$this->handleRequest($travelCode, $travelInfo);
}
private function handleRequest(string $travelCode, Travelinfo $travelInfo)
{
// fetch travel data via API call
try {
$travelData = $this->apiClient->getTravel($travelCode);
} catch (ApiException $e) {
$travelData = [];
}
/** @var Date $date */
$date = $this->dateRepository->findForProductAndDate($product, $travelDate)->getFirst();
// extract courses from additional services
$courses = array_filter($travelData['additionalServices'] ?? [], function ($service) {
return 'KUR' === $service['subType'];
});
$this->view->assign('travelinfo', $travelinfo);
$this->view->assign('date', $date);
// extract board services from additional services
$board = array_filter($travelData['additionalServices'] ?? [], function ($service) {
return 'VPF' === $service['subType'];
});
$this->view->assign('travelInfo', $travelInfo);
$this->view->assign('travelData', $travelData);
$this->view->assign('courses', $courses);
$this->view->assign('board', $board);
}
}
@@ -0,0 +1,149 @@
<?php
namespace EP\EpProducts\MyEP;
use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\GenericProvider;
use League\OAuth2\Client\Token\AccessToken;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Utility\GeneralUtility;
class ApiClient
{
private static ?AccessToken $accessToken = null;
/**
* @throws ApiException
*/
public function getLastUpdateAt(): ?\DateTimeImmutable
{
$httpClient = $this->getHttpClient();
try {
$response = $httpClient->request('GET', 'last-update');
} catch (TransportExceptionInterface $e) {
throw new ApiException($e->getMessage());
}
try {
if (200 !== $response->getStatusCode()) {
throw new ApiException($response->getStatusCode());
}
$data = $response->toArray(false);
if (null === $data['timestamp'] ?? null) {
return null;
}
return new \DateTimeImmutable($data['timestamp']);
} catch (\Throwable $e) {
throw new ApiException($e->getMessage());
}
}
/**
* @throws ApiException
*/
public function getPickups(): array
{
$httpClient = $this->getHttpClient();
try {
$response = $httpClient->request('GET', 'pickups');
} catch (TransportExceptionInterface $e) {
throw new ApiException($e->getMessage());
}
try {
if (200 !== $response->getStatusCode()) {
throw new ApiException($response->getStatusCode());
}
return $response->toArray(false);
} catch (\Throwable $e) {
throw new ApiException($e->getMessage());
}
}
/**
* @throws ApiException
*/
public function getTravels(): array
{
$httpClient = $this->getHttpClient();
try {
$response = $httpClient->request('GET', 'travels');
} catch (TransportExceptionInterface $e) {
throw new ApiException($e->getMessage());
}
try {
if (200 !== $response->getStatusCode()) {
throw new ApiException($response->getStatusCode());
}
return $response->toArray(false);
} catch (\Throwable $e) {
throw new ApiException($e->getMessage());
}
}
/**
* @throws ApiException
*/
public function getTravel(string $productCode): array
{
$httpClient = $this->getHttpClient();
try {
$response = $httpClient->request('GET', 'travels/' . $productCode);
} catch (TransportExceptionInterface $e) {
throw new ApiException($e->getMessage());
}
try {
if (200 !== $response->getStatusCode()) {
throw new ApiException($response->getStatusCode());
}
return $response->toArray(false);
} catch (\Throwable $e) {
throw new ApiException($e->getMessage());
}
}
private function getHttpClient(): HttpClientInterface
{
$config = GeneralUtility::makeInstance(ExtensionConfiguration::class)
->get('ep_products');
if (null === static::$accessToken || true === static::$accessToken->hasExpired()) {
static::$accessToken = $this
->getProvider($config)
->getAccessToken('client_credentials')
;
}
return HttpClient::createForBaseUri($config['myEpApiBaseUrl'], [
'auth_bearer' => static::$accessToken->getToken(),
]);
}
private function getProvider(array $config): AbstractProvider
{
return new GenericProvider([
'clientId' => $config['myEpApiClientId'],
'clientSecret' => $config['myEpApiClientSecret'],
'redirectUri' => null,
'urlAuthorize' => $config['myEpApiUrlAuthorize'],
'urlAccessToken' => $config['myEpApiUrlToken'],
'urlResourceOwnerDetails' => null,
'scopes' => 'api',
]);
}
}
@@ -0,0 +1,7 @@
<?php
namespace EP\EpProducts\MyEP;
class ApiException extends \Exception
{
}
@@ -27,7 +27,7 @@ return [
],
],
'palettes' => [
'basics' => ['showitem' => 'title,path_segment,--linebreak--,product,hide_address,--linebreak--,links,
'basics' => ['showitem' => 'title,path_segment,--linebreak--,travel_code,hide_address,--linebreak--,links,
--linebreak--,teasers'],
'text' => ['showitem' => 'subline,headline,--linebreak--,intro_text,--linebreak--, bus_info_text,--linebreak--,
departure_text,--linebreak--,footer_text,--linebreak--,self_arranged_text,--linebreak--,additional_info,
@@ -191,32 +191,13 @@ return [
$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
),
],
'product' => [
'travel_code' => [
'exclude' => false,
'label' => 'Produkt',
'label' => 'Reisecode',
'config' => [
'type' => 'group',
'internal_type' => 'db',
'allowed' => 'tx_epproducts_domain_model_product',
'minitems' => 1,
'maxitems' => 1,
'size' => 1,
'fieldControl' => [
'elementBrowser' => [
'disabled' => true,
],
],
'fieldWizard' => [
'recordsOverview' => [
'disabled' => true,
],
],
'suggestOptions' => [
'default' => [
'additionalSearchFields' => 'code,name,keywords',
'orderBy' => 'code',
],
],
'type' => 'input',
'size' => 30,
'eval' => 'trim,required'
],
],
'hide_address' => [
@@ -1,3 +1,18 @@
# customsubcategory=100=General
# cat=epproducts/100/100; type=int; label=Searchresult PID
# cat=General; type=int; label=Searchresult PID
searchPageUid = 0
# cat=MyEpAPI; type=string; label=MyE&P API base URL (trailing slash!)
myEpApiBaseUrl =
# cat=MyEpAPI; type=string; label=MyE&P API client id
myEpApiClientId =
# cat=MyEpAPI; type=string; label=MyE&P API client secret
myEpApiClientSecret =
# cat=MyEpAPI; type=string; label=MyE&P API authorization URL
myEpApiUrlAuthorize =
# cat=MyEpAPI; type=string; label=MyE&P API token URL
myEpApiUrlToken =
@@ -252,7 +252,7 @@ $boot = function () {
'EP.ep_products',
'travelinfo',
[
'Travelinfo' => 'index,travelCode',
'Travelinfo' => 'travelInfo,travelCode',
]
);
@@ -7,9 +7,9 @@
<f:link.typolink parameter="{f:if(condition: settings.logoLink, then: settings.logoLink, else: settings.defaultHomeUid)}"
class="block lg:pb-4"
title="zur Startseite">
<f:image src="Logo"
<f:image src="{settings.logoImage}"
class="block w-auto {ep:themeClasses(classes: '{ep: \'h-10 lg:h-12\', sbw: \'h-14 lg:h-24\', snz: \'h-14 lg:h-24\', suz: \'h-14 lg:h-24\', uch: \'h-12 lg:h-20\', ser: \'h-10 lg:h-12\'}', key: settings.themekey)}"
alt="Logo"/>
alt="Bildmarke"/>
</f:link.typolink>
<f:link.typolink parameter="{settings.myEpPageUid}"
title="My E&P"
@@ -7,7 +7,7 @@
<f:section name="Main">
<div class="container relative">
<div class="h-80 lg:h-128">
<f:image image="{travelinfo.headerImage}"
<f:image image="{travelInfo.headerImage}"
width="1280"
alt="Reiseinformationen"
class="block w-full h-full object-cover object-center" />
@@ -18,8 +18,8 @@
</div>
<div class="grid grid-cols-2 lg:grid-cols-4">
<f:variable name="bgColors" value="{0: 'bg-[#165883]/90', 1: 'bg-[#9DBACE]/90', 2: 'bg-[#3396E4]/90', 3: 'bg-[#0268AA]/90'}"/>
<f:variable name="linksCount" value="{travelinfo.links -> f:count()}"/>
<f:for each="{travelinfo.links}" as="link" iteration="iteration">
<f:variable name="linksCount" value="{travelInfo.links -> f:count()}"/>
<f:for each="{travelInfo.links}" as="link" iteration="iteration">
<f:render section="Link" arguments="{item: link, color: '{bgColors.{iteration.index}}'}"/>
</f:for>
<a href="#goodtoknow"
@@ -40,13 +40,13 @@
<div class="pb-32 pt-32 lg:pt-52 -mt-24 bg-ep-primary-bg">
<div class="container px-8 lg:px-24" data-rte-content>
<h1 class="headline--underlined headline--has-subline">
<span class="subline">{travelinfo.subline}</span>
{travelinfo.headline}
<span class="subline">{travelInfo.subline}</span>
{travelInfo.headline}
</h1>
{travelinfo.introText -> f:format.html()}
{travelInfo.introText -> f:format.html()}
<f:if condition="{date}">
<p>
{date.product.name} {date.dateStart -> f:format.date(format: 'd.m.Y')} - {date.dateEnd -> f:format.date(format: 'd.m.Y')}
{travelData.label} {travelData.dateFrom -> f:format.date(format: 'd.m.Y')} - {travelData.dateTo -> f:format.date(format: 'd.m.Y')}
</p>
</f:if>
</div>
@@ -63,20 +63,22 @@
</div>
<div class="lg:w-3/4 pt-8 lg:pt-0 lg:px-8 text-white" data-rte-content>
<f:if condition="{date}">
<f:if condition="{travelinfo.hideAddress}">
<f:if condition="{travelInfo.hideAddress}">
<f:else>
<h3 class="text-white">
Adresse
</h3>
<p>
{date.hotel.name}
{travelData.hotel.name}
<br>
{date.hotel.address -> f:format.nl2br()}
{travelData.hotel.street}
<br>
{travelData.hotel.country} {travelData.hotel.city}
</p>
</f:else>
</f:if>
<f:if condition="{date.pickups -> f:count()} > 0">
<f:if condition="{date.pickups -> f:count()} <= 3">
<f:if condition="{travelData.pickupsTo -> f:count()} > 0">
<f:if condition="{travelData.pickupsTo -> f:count()} <= 3">
<f:then>
<f:render section="Pickups" arguments="{_all}"/>
</f:then>
@@ -98,11 +100,11 @@
</f:if>
</f:if>
</f:if>
<f:if condition="{travelinfo.selfArrangedText}">
<f:if condition="{travelInfo.selfArrangedText}">
<h3 class="text-white">
Eigenanreise
</h3>
{travelinfo.selfArrangedText -> f:format.html()}
{travelInfo.selfArrangedText -> f:format.html()}
</f:if>
</div>
</div>
@@ -114,13 +116,13 @@
</h2>
</div>
<div class="container px-4 lg:px-24 flex flex-col space-y-16 pb-16">
<f:for each="{travelinfo.teasers}" as="teaser">
<f:for each="{travelInfo.teasers}" as="teaser">
<f:render section="Teaser" arguments="{_all}"/>
</f:for>
</div>
<div id="goodtoknow" class="bg-ep-primary-bg">
<div class="container">
<f:if condition="{travelinfo.additionalInfo}">
<f:if condition="{travelInfo.additionalInfo}">
<div class="flex flex-col lg:flex-row divide-y lg:divide-y-0 lg:divide-x divide-white bg-[#B48F1D] px-8 lg:px-0 py-8">
<div class="lg:w-1/4 flex flex-col space-y-4 pb-8 lg:pb-0 lg:px-8">
<div class="w-32 h-32 xl:w-16 xl:h-16 text-[#F9C700]">
@@ -131,11 +133,11 @@
</div>
</div>
<div class="lg:w-3/4 pt-8 lg:pt-0 lg:px-8 text-white" data-rte-content>
{travelinfo.additionalInfo -> f:format.html()}
{travelInfo.additionalInfo -> f:format.html()}
</div>
</div>
</f:if>
<f:if condition="{travelinfo.importantPhoneNumbers}">
<f:if condition="{travelInfo.importantPhoneNumbers}">
<div class="flex flex-col lg:flex-row divide-y lg:divide-y-0 lg:divide-x divide-white bg-[#F8C62F] px-8 lg:px-0 py-8">
<div class="lg:w-1/4 flex flex-col space-y-4 pb-8 lg:pb-0 lg:px-8">
<div class="text-2xl lg:text-4xl text-white uppercase font-bold">
@@ -143,18 +145,18 @@
</div>
</div>
<div class="lg:w-3/4 pt-8 lg:pt-0 lg:px-8 text-white" data-rte-content>
<f:if condition="{date.guide}">
<f:if condition="{travelData.guide}">
<p>
<strong>Busbegleitung / Reiseleitung</strong>
<br>
{date.guide.name} {date.guide.phone}
{travelData.guide.name} {travelData.guide.phone}
</p>
</f:if>
{travelinfo.importantPhoneNumbers -> f:format.html()}
{travelInfo.importantPhoneNumbers -> f:format.html()}
</div>
</div>
</f:if>
<f:if condition="{travelinfo.importantLinks}">
<f:if condition="{travelInfo.importantLinks}">
<div class="flex flex-col lg:flex-row divide-y lg:divide-y-0 lg:divide-x divide-white bg-[#18527B] px-8 lg:px-0 py-8">
<div class="lg:w-1/4 flex flex-col space-y-4 pb-8 lg:pb-0 lg:px-8">
<div class="text-2xl lg:text-4xl text-white uppercase font-bold">
@@ -162,18 +164,18 @@
</div>
</div>
<div class="lg:w-3/4 pt-8 lg:pt-0 lg:px-8 text-white" data-rte-content>
{travelinfo.importantLinks -> f:format.html()}
{travelInfo.importantLinks -> f:format.html()}
</div>
</div>
</f:if>
</div>
</div>
<div class="container px-4 lg:px-24 py-16">
{travelinfo.footerText -> f:format.html()}
{travelInfo.footerText -> f:format.html()}
<f:image src="EXT:ep_theme/Resources/Public/images/signature_ep_team.png" class="block w-full max-w-64 h-auto mt-8" alt="" />
</div>
<div class="container">
<f:image image="{travelinfo.footerImage}" width="1028" class="block w-full h-auto" alt="" />
<f:image image="{travelInfo.footerImage}" width="1028" class="block w-full h-auto" alt="" />
</div>
</f:section>
@@ -196,20 +198,28 @@
<h2 class="headline--primary">
{teaser.title}
</h2>
<f:if condition="{date}">
<f:if condition="{travelData}">
<f:if condition="{teaser.category} == 'food'">
<p>
{date.board}
</p>
<f:if condition="{board -> f:count()} > 0">
<f:then>
<ul>
<f:for as="item" each="{board}">
<li>
{item.label}
</li>
</f:for>
</ul>
</f:then>
</f:if>
</f:if>
<f:if condition="{teaser.category} == 'course'">
<f:if condition="{date.activeCoursesForTravelinfo -> f:count()} > 0">
<f:if condition="{courses -> f:count()} > 0">
<f:then>
<p>
<strong>Folgende Kurse finden statt</strong>
</p>
<ul>
<f:for as="course" each="{date.activeCoursesForTravelinfo}">
<f:for as="course" each="{courses}">
<li>
{course.label}
</li>
@@ -257,7 +267,7 @@
<p>
Bitte findet euch mindestens <strong>20 Minuten vor Abfahrt</strong> an der gebuchten Haltestelle ein.
</p>
{travelinfo.busInfoText -> f:format.html()}
{travelInfo.busInfoText -> f:format.html()}
<table class="w-full bg-zinc-200 text-zinc-800 text-xs md:text-base mb-4">
<tr>
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-left text-sm md:text-base">
@@ -267,18 +277,18 @@
Datum/Uhrzeit
</th>
</tr>
<f:for each="{date.pickups}" as="pickup">
<f:for each="{travelData.pickupsTo}" as="pickup">
<tr class="odd:bg-zinc-50 align-top">
<td class="border-r border-zinc-200 px-2 py-1 md:p-2">
{pickup.city}<br><span class="text-xs">{pickup.street}</span>
</td>
<td class="border-r border-zinc-200 px-2 py-1 md:p-2">
{pickup.date}<br>{f:if(condition: pickup.time, then: '{pickup.time} Uhr', else: '???')}
{pickup.time -> f:format.date(format: 'd.m.Y')}<br>{pickup.time -> f:format.date(format: 'H:i')} Uhr
</td>
</tr>
</f:for>
</table>
{travelinfo.departureText -> f:format.html()}
{travelInfo.departureText -> f:format.html()}
</f:section>
<f:section name="Icon">
@@ -6,6 +6,6 @@
<f:section name="Main">
<f:render partial="Travelinfo" section="Main" arguments="{_all}"/>
</f:section>
</f:section>
</html>