Add support for additional images added via TYPO3 (#112)

Some installations might need to add further images
to records imported from ThüCAT.
The records are now extended to support adding images by editors.
The images are not touched during import.
The images are also ignored during clean ups,
the editor is in full control.

This feature for now is only added to tourist attractions by default.
The feature is implemented in a way that all objects extending the
``WerkraumMedia\ThueCat\Domain\Model\Frontend\Base`` class are usable
by adding an ``editorial_images`` field to their table.
This commit is contained in:
Daniel Siepmann 2023-08-29 13:54:16 +02:00 committed by GitHub
parent 2a39dc7753
commit 8663d5a759
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 897 additions and 14 deletions

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace WerkraumMedia\ThueCat\Domain\Model\Frontend; namespace WerkraumMedia\ThueCat\Domain\Model\Frontend;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Core\Type\TypeInterface; use TYPO3\CMS\Core\Type\TypeInterface;
class Media implements TypeInterface class Media implements TypeInterface
@ -37,6 +38,11 @@ class Media implements TypeInterface
*/ */
private $data; private $data;
/**
* @var FileReference[]
*/
protected $editorialImages = [];
public function __construct(string $serialized) public function __construct(string $serialized)
{ {
$this->serialized = $serialized; $this->serialized = $serialized;
@ -73,6 +79,28 @@ class Media implements TypeInterface
}); });
} }
public function getAllImages(): array
{
return array_merge($this->getEditorialImages(), $this->getImages());
}
/**
* @return FileReference[]
*/
public function getEditorialImages(): array
{
return $this->editorialImages;
}
/**
* @internal Only used to set the values while mapping objects.
* @see: AfterObjectThawedHandler
*/
public function setEditorialImages(array $images): void
{
$this->editorialImages = $images;
}
public function __toString(): string public function __toString(): string
{ {
return $this->serialized; return $this->serialized;

View file

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2023 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace WerkraumMedia\ThueCat\Typo3\Extbase\DataMapping;
use TYPO3\CMS\Core\Resource\FileRepository;
use TYPO3\CMS\Extbase\Event\Persistence\AfterObjectThawedEvent;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory;
use WerkraumMedia\ThueCat\Domain\Model\Frontend\Base;
use WerkraumMedia\ThueCat\Domain\Model\Frontend\Media;
/**
* Will extend mapped objects with further info.
*
* E.g. will add editorial images to media property.
*/
class AfterObjectThawedHandler
{
/**
* @var FileRepository
*/
private $fileRepository;
/**
* @var DataMapFactory
*/
private $dataMapFactory;
public function __construct(
FileRepository $fileRepository,
DataMapFactory $dataMapFactory
) {
$this->fileRepository = $fileRepository;
$this->dataMapFactory = $dataMapFactory;
}
public function __invoke(AfterObjectThawedEvent $event): void
{
$object = $event->getObject();
$record = $event->getRecord();
if (
$object instanceof Base
&& ($record['editorial_images'] ?? 0) > 0
) {
$this->attachEditorialImages($object);
}
}
private function attachEditorialImages(Base $object): void
{
$uid = $object->getUid();
if ($uid === null) {
return;
}
$images = $this->fileRepository->findByRelation(
$this->getTableNameForObject($object),
'editorial_images',
$uid
);
if ($images === []) {
return;
}
$this->getMedia($object)->setEditorialImages($images);
}
private function getTableNameForObject(Base $object): string
{
return $this->dataMapFactory
->buildDataMap(get_class($object))
->getTableName()
;
}
private function getMedia(Base $object): Media
{
$media = $object->getMedia();
if (!$media instanceof Media) {
$media = new Media('');
$object->_setProperty('media', $media);
}
return $media;
}
}

View file

@ -25,6 +25,11 @@ services:
class: 'WerkraumMedia\ThueCat\Service\DateBasedFilter\FilterBasedOnTypo3Context' class: 'WerkraumMedia\ThueCat\Service\DateBasedFilter\FilterBasedOnTypo3Context'
public: true public: true
WerkraumMedia\ThueCat\Typo3\Extbase\DataMapping\AfterObjectThawedHandler:
tags:
- name: 'event.listener'
event: 'TYPO3\CMS\Extbase\Event\Persistence\AfterObjectThawedEvent'
'cache.thuecat_fetchdata': 'cache.thuecat_fetchdata':
class: 'TYPO3\CMS\Core\Cache\Frontend\FrontendInterface' class: 'TYPO3\CMS\Core\Cache\Frontend\FrontendInterface'
factory: factory:

View file

@ -310,6 +310,16 @@ return (static function (string $extensionKey, string $tableName) {
'readOnly' => true, 'readOnly' => true,
], ],
], ],
'editorial_images' => [
'label' => $languagePath . '.editorial_images',
'l10n_mode' => 'exclude',
'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig(
'editorial_images',
[],
$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
),
],
], ],
'palettes' => [ 'palettes' => [
'language' => [ 'language' => [
@ -319,7 +329,7 @@ return (static function (string $extensionKey, string $tableName) {
], ],
'types' => [ 'types' => [
'0' => [ '0' => [
'showitem' => '--palette--;;language, title, description, slogan, start_of_construction, sanitation, other_service, museum_service, architectural_style, traffic_infrastructure, payment_accepted, digital_offer, photography, pets_allowed, is_accessible_for_free, public_access, available_languages, distance_to_public_transport, opening_hours, special_opening_hours, offers, accessibility_specification, address, url, media, remote_id, --div--;' . $languagePath . '.tab.relations, town, managed_by, parking_facility_near_by', 'showitem' => '--palette--;;language, title, description, slogan, start_of_construction, sanitation, other_service, museum_service, architectural_style, traffic_infrastructure, payment_accepted, digital_offer, photography, pets_allowed, is_accessible_for_free, public_access, available_languages, distance_to_public_transport, opening_hours, special_opening_hours, offers, accessibility_specification, address, url, media, remote_id, --div--;' . $languagePath . '.tab.relations, town, managed_by, parking_facility_near_by, --div--;' . $languagePath . '.tab.editorial_additions, editorial_images',
], ],
], ],
]; ];

View file

@ -14,6 +14,15 @@ Features
This finally allows to regularly execute imports. This finally allows to regularly execute imports.
This also allows to import from CLI context with differently configured timeouts. This also allows to import from CLI context with differently configured timeouts.
* Add support for additional images added via TYPO3.
Some installations might need to add further images to records imported from ThüCAT.
The records are now extended to support adding images by editors.
The images are not touched during import.
The images are also ignored during clean ups, the editor is in full control.
This feature for now is only added to tourist attractions by default.
The feature is implemented in a way that all objects extending the ``WerkraumMedia\ThueCat\Domain\Model\Frontend\Base`` class are usable by adding an ``editorial_images`` field to their table.
Fixes Fixes
----- -----
@ -28,6 +37,11 @@ Tasks
As this might hint at an issue but most probably is okay, e.g. due to none active As this might hint at an issue but most probably is okay, e.g. due to none active
language, missing name, etc. language, missing name, etc.
* Separate default templates from templates for testing.
That way we no longer test the delivered templates, but they should not be used anyway.
Also we can now use templates only for testing to ensure that frontend rendering works as expected, without worrying about sites using the templates.
The templates were copied and extended for editorial images.
Deprecation Deprecation
----------- -----------

View file

@ -84,6 +84,9 @@
<trans-unit id="tx_thuecat_tourist_attraction.tab.relations" xml:space="preserve"> <trans-unit id="tx_thuecat_tourist_attraction.tab.relations" xml:space="preserve">
<source>Relations</source> <source>Relations</source>
</trans-unit> </trans-unit>
<trans-unit id="tx_thuecat_tourist_attraction.tab.editorial_additions" xml:space="preserve">
<source>Editorial Additions</source>
</trans-unit>
<trans-unit id="tx_thuecat_tourist_attraction.title" xml:space="preserve"> <trans-unit id="tx_thuecat_tourist_attraction.title" xml:space="preserve">
<source>Title</source> <source>Title</source>
</trans-unit> </trans-unit>
@ -171,6 +174,9 @@
<trans-unit id="tx_thuecat_tourist_attraction.managed_by.unkown" xml:space="preserve"> <trans-unit id="tx_thuecat_tourist_attraction.managed_by.unkown" xml:space="preserve">
<source>Unkown</source> <source>Unkown</source>
</trans-unit> </trans-unit>
<trans-unit id="tx_thuecat_tourist_attraction.editorial_images" xml:space="preserve">
<source>Images</source>
</trans-unit>
<trans-unit id="tx_thuecat_parking_facility" xml:space="preserve"> <trans-unit id="tx_thuecat_parking_facility" xml:space="preserve">
<source>Parking Facility</source> <source>Parking Facility</source>

View file

@ -1,2 +1,11 @@
page = PAGE page = PAGE
page.10 < styles.content.get page.10 < styles.content.get
lib.thuecatContentElement {
partialRootPaths {
0 = EXT:thuecat/Tests/Functional/Fixtures/Frontend/Resources/Private/Partials/ContentElement/
}
templateRootPaths {
0 = EXT:thuecat/Tests/Functional/Fixtures/Frontend/Resources/Private/Templates/ContentElement/
}
}

View file

@ -0,0 +1,136 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<div class="accessibility">
<div class="row">
<div class="col-12 col-md-6">
<f:if condition="{specification.searchCriteria}">
<ul>
<f:for each="{specification.searchCriteria}" key="id" as="criterias">
<f:for each="{criterias}" as="criteria">
<li>{f:translate(id: 'content.accessibilitySpecification.searchCriteria.criteria.{criteria}', default: criteria, extensionName: 'Thuecat')}</li>
</f:for>
</f:for>
</ul>
</f:if>
</div>
<div class="col-12 col-md-6">
<ul>
<f:if condition="{specification.certificationDeaf}">
<li>{f:translate(id: 'content.accessibilitySpecification.certification.{specification.certificationDeaf}', default: specification.certificationDeaf, extensionName: 'Thuecat')} {f:translate(id: 'content.accessibilitySpecification.certification.deaf', extensionName: 'Thuecat')}</li>
</f:if>
<f:if condition="{specification.certificationMental}">
<li>{f:translate(id: 'content.accessibilitySpecification.certification.{specification.certificationMental}', default: specification.certificationMental, extensionName: 'Thuecat')} {f:translate(id: 'content.accessibilitySpecification.certification.mental', extensionName: 'Thuecat')}</li>
</f:if>
<f:if condition="{specification.certificationPartiallyDeaf}">
<li>{f:translate(id: 'content.accessibilitySpecification.certification.{specification.certificationPartiallyDeaf}', default: specification.certificationPartiallyDeaf, extensionName: 'Thuecat')} {f:translate(id: 'content.accessibilitySpecification.certification.partiallyDeaf', extensionName: 'Thuecat')}</li>
</f:if>
<f:if condition="{specification.certificationPartiallyVisual}">
<li>{f:translate(id: 'content.accessibilitySpecification.certification.{specification.certificationPartiallyVisual}', default: specification.certificationPartiallyVisual, extensionName: 'Thuecat')} {f:translate(id: 'content.accessibilitySpecification.certification.partiallyVisual', extensionName: 'Thuecat')}</li>
</f:if>
<f:if condition="{specification.certificationVisual}">
<li>{f:translate(id: 'content.accessibilitySpecification.certification.{specification.certificationVisual}', default: specification.certificationVisual, extensionName: 'Thuecat')} {f:translate(id: 'content.accessibilitySpecification.certification.visual', extensionName: 'Thuecat')}</li>
</f:if>
<f:if condition="{specification.certificationWalking}">
<li>{f:translate(id: 'content.accessibilitySpecification.certification.{specification.certificationWalking}', default: specification.certificationWalking, extensionName: 'Thuecat')} {f:translate(id: 'content.accessibilitySpecification.certification.walking', extensionName: 'Thuecat')}</li>
</f:if>
<f:if condition="{specification.certificationWheelchair}">
<li>{f:translate(id: 'content.accessibilitySpecification.certification.{specification.certificationWheelchair}', default: specification.certificationWheelchair, extensionName: 'Thuecat')} {f:translate(id: 'content.accessibilitySpecification.certification.wheelchair', extensionName: 'Thuecat')}</li>
</f:if>
</ul>
</div>
</div>
<div class="row accordion" id="accordionShortDescriptions">
<f:if condition="{specification.shortDescriptionAllGenerations}">
<div class="accordion-item">
<h3 class="accordion-header" id="headingAllGenerations">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#shortDescriptionAllGenerations-{uid}" role="button" aria-expanded="false" aria-controls="shortDescriptionAllGenerations-{uid}">
{f:translate(id: 'content.accessibilitySpecification.shortDescriptionAllGenerations', extensionName: 'Thuecat')}
</button>
</h3>
<div class="accordion-collapse collapse" id="shortDescriptionAllGenerations-{uid}">
<div class="accordion-body">
<p>{specification.shortDescriptionAllGenerations -> f:format.nl2br()}</p>
</div>
</div>
</div>
</f:if>
<f:if condition="{specification.shortDescriptionAllergic}">
<div class="accordion-item">
<h3 class="accordion-header" id="headingAllergic">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#shortDescriptionAllergic-{uid}" role="button" aria-expanded="false" aria-controls="shortDescriptionAllergic-{uid}">
{f:translate(id: 'content.accessibilitySpecification.shortDescriptionAllergic', extensionName: 'Thuecat')}
</button>
</h3>
<div class="accordion-collapse collapse" id="shortDescriptionAllergic-{uid}">
<div class="accordion-body">
<p>{specification.shortDescriptionAllergic -> f:format.nl2br()}</p>
</div>
</div>
</div>
</f:if>
<f:if condition="{specification.shortDescriptionDeaf}">
<div class="accordion-item">
<h3 class="accordion-header" id="headingDeaf">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#shortDescriptionDeaf-{uid}" role="button" aria-expanded="false" aria-controls="shortDescriptionDeaf-{uid}">
{f:translate(id: 'content.accessibilitySpecification.shortDescriptionDeaf', extensionName: 'Thuecat')}
</button>
</h3>
<div class="accordion-collapse collapse" id="shortDescriptionDeaf-{uid}">
<div class="accordion-body">
<p>{specification.shortDescriptionDeaf -> f:format.nl2br()}</p>
</div>
</div>
</div>
</f:if>
<f:if condition="{specification.shortDescriptionMental}">
<div class="accordion-item">
<h3 class="accordion-header" id="headingMetal">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" href="#shortDescriptionMental-{uid}" role="button" aria-expanded="false" aria-controls="shortDescriptionMental-{uid}">
{f:translate(id: 'content.accessibilitySpecification.shortDescriptionMental', extensionName: 'Thuecat')}
</button>
</h3>
<div class="accordion-collapse collapse" id="shortDescriptionMental-{uid}">
<div class="accordion-body">
<p>{specification.shortDescriptionMental -> f:format.nl2br()}</p>
</div>
</div>
</div>
</f:if>
<f:if condition="{specification.shortDescriptionVisual}">
<div class="accordion-item">
<h3 class="accordion-header" id="headingVisual">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" href="#shortDescriptionVisual-{uid}" role="button" aria-expanded="false" aria-controls="shortDescriptionVisual-{uid}">
{f:translate(id: 'content.accessibilitySpecification.shortDescriptionVisual', extensionName: 'Thuecat')}
</button>
</h3>
<div class="accordion-collapse collapse" id="shortDescriptionVisual-{uid}">
<div class="accordion-body">
<p>{specification.shortDescriptionVisual -> f:format.nl2br()}</p>
</div>
</div>
</div>
</f:if>
<f:if condition="{specification.shortDescriptionWalking}">
<div class="accordion-item">
<h3 class="accordion-header" id="headingVisual">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" href="#shortDescriptionWalking-{uid}" role="button" aria-expanded="false" aria-controls="shortDescriptionWalking-{uid}">
{f:translate(id: 'content.accessibilitySpecification.shortDescriptionWalking', extensionName: 'Thuecat')}
</button>
</h3>
<div class="accordion-collapse collapse" id="shortDescriptionWalking-{uid}">
<div class="accordion-body">
<p>{specification.shortDescriptionWalking -> f:format.nl2br()}</p>
</div>
</div>
</div>
</f:if>
</div>
</div>
</html>

View file

@ -0,0 +1,24 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<div class="address">
<p>
<f:if condition="{address.street}">
{address.street}<br>
</f:if>
<f:if condition="{address.zip} && {address.city}">
{address.zip} {address.city}<br>
</f:if>
<f:if condition="{address.email}">
<f:link.email email="{address.email}">{address.email}</f:link.email><br>
</f:if>
<f:if condition="{address.phone}">
<a href="tel:{address.phone}">{address.phone}</a><br>
</f:if>
<f:if condition="{address.fax}">
{address.fax}
</f:if>
<f:if condition="{url}">
<a href="{url}" referrerpolicy="no-referrer" rel="noreferrer noopener">{f:translate(id: 'content.url', extensionName: 'Thuecat')}</a>
</f:if>
</p>
</div>
</html>

View file

@ -0,0 +1,9 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<div class="digital">
<p>
<f:for each="{digitalOffer}" as="offer">
<span class="badge bg-light">{f:translate(id: 'content.digitalOffer.{offer}', default: offer, extensionName: 'Thuecat')}</span>
</f:for>
</p>
</div>
</html>

View file

@ -0,0 +1,9 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<div class="languages">
<p>
<f:for each="{availableLanguages}" as="language">
<span class="badge bg-light">{f:translate(id: 'content.availableLanguage.{language}', default: language, extensionName: 'Thuecat')}</span>
</f:for>
</p>
</div>
</html>

View file

@ -0,0 +1,9 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<div class="museum">
<p>
<f:for each="{museumServices}" as="service">
<span class="badge bg-light">{f:translate(id: 'content.museumService.{service}', default: service, extensionName: 'Thuecat')}</span>
</f:for>
</p>
</div>
</html>

View file

@ -0,0 +1,80 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<div class="offers">
<f:if condition="{offers -> f:count()} > 1">
<f:then>
<f:render section="MultiOffers" arguments="{offers: offers, uid: uid}" />
</f:then>
<f:else>
<f:render section="SingleOffer" arguments="{offers: offers}" />
</f:else>
</f:if>
</div>
<f:section name="MultiOffers">
<div class="accordion" id="accordionOfferPrices">
<f:for each="{offers}" as="offer" key="key" iteration="iterator">
<div class="accordion-item">
<h3 class="accordion-header">
<button class="accordion-button collapsed" data-bs-toggle="collapse" data-bs-target="#offerPriceGroup-{key}-{uid}" role="button" aria-expanded="false" aria-controls="offerGroup-{key}-{uid}">
<f:if condition="{offer.title}"><f:then>{offer.title} <span class="ps-1">({f:translate(id: 'content.price.type.{offer.type}', default: offer.type, extensionName: 'Thuecat')})</span></f:then><f:else>Preisgruppe</f:else></f:if>
</button>
</h3>
<div class="accordion-collapse collapse" id="offerPriceGroup-{key}-{uid}">
<div class="accordion-body row">
<f:if condition="{offer.description}">
<div class="description col-md-4">
<p><small>{offer.description}</small></p>
</div>
</f:if>
<div class="prices col-md-8">
<div class="row">
<f:for each="{offer.prices}" as="price">
<div class="price-item col-md-5">
<strong>{price.title}</strong>
<span>{price.price -> f:format.currency(decimalSeparator: ',', thousandsSeparator: '.', decimals: 2, currencySign: price.currency)}</span>
<small>{f:translate(id: 'content.price.rule.{price.rules.0}', default: price.rule, extensionName: 'Thuecat')}</small>
<f:if condition="{price.description}">
<p><small>{price.description}</small></p>
</f:if>
</div>
</f:for>
</div>
</div>
</div>
</div>
</div>
</f:for>
</div>
</f:section>
<f:section name="SingleOffer">
<f:for each="{offers}" as="offer" key="key">
<div class="offer-group">
<div class="content">
<h3>{offer.title} <span class="ps-1">({f:translate(id: 'content.price.type.{offer.type}', default: offer.type, extensionName: 'Thuecat')})</span></h3>
<div class="row">
<f:if condition="{offer.description}">
<div class="description col-md-4">
<p><small>{offer.description}</small></p>
</div>
</f:if>
<div class="prices col-md-8">
<div class="masonry-prices row">
<f:for each="{offer.prices}" as="price">
<div class="price-item col-md-5">
<strong>{price.title}</strong>
<span>{price.price -> f:format.currency(decimalSeparator: ',', thousandsSeparator: '.', decimals: 2, currencySign: price.currency)}</span>
<small>{f:translate(id: 'content.price.rule.{price.rules.0}', default: price.rule, extensionName: 'Thuecat')}</small>
<f:if condition="{price.description}">
<p><small>{price.description}</small></p>
</f:if>
</div>
</f:for>
</div>
</div>
</div>
</div>
</div>
</f:for>
</f:section>
</html>

View file

@ -0,0 +1,27 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<div class="openingHours row">
<f:for each="{openingHours}" as="openingHour">
<f:if condition="{openingHours -> f:count()} == 1">
<f:then>
<div class="col-12">
</f:then>
<f:else>
<div class="col-md-6">
</f:else>
</f:if>
<h3>
{openingHour.from -> f:format.date(format: 'd.m.Y')} -
{openingHour.through -> f:format.date(format: 'd.m.Y')}
</h3>
<div>
<f:for each="{openingHour.daysOfWeekWithMondayFirstWeekDay}" as="weekday">
<div class="day-row">
<div class="day"><span>{f:translate(id: 'content.openingHour.weekday.{weekday}', default: weekday, extensionName: 'Thuecat')}</span></div>
<div class="time"><span>{openingHour.opens} - {openingHour.closes}</span></div>
</div>
</f:for>
</div>
</div>
</f:for>
</div>
</html>

View file

@ -0,0 +1,22 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<div class="parking">
<f:if condition="{parkingFacilitiesNearBy -> f:count()} > 3">
<f:then>
<div class="row">
<f:for each="{parkingFacilitiesNearBy}" as="parkingFacility">
<div class="col-md-6">
{parkingFacility.title}
{f:render(partial: 'Address', arguments: {address: parkingFacility.address})}
</div>
</f:for>
</div>
</f:then>
<f:else>
<f:for each="{parkingFacilitiesNearBy}" as="parkingFacility">
{parkingFacility.title}
{f:render(partial: 'Address', arguments: {address: parkingFacility.address})}
</f:for>
</f:else>
</f:if>
</div>
</html>

View file

@ -0,0 +1,9 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<div class="payment">
<p>
<f:for each="{paymentAccepted}" as="payment">
<span class="badge bg-light">{f:translate(id: 'content.paymentAccepted.{payment}', default: payment, extensionName: 'Thuecat')}</span>
</f:for>
</p>
</div>
</html>

View file

@ -0,0 +1,9 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<div class="photography">
<p>
<f:for each="{photography}" as="photography">
<span class="badge bg-light">{f:translate(id: 'content.photography.{photography}', default: photography, extensionName: 'Thuecat')}</span>
</f:for>
</p>
</div>
</html>

View file

@ -0,0 +1,9 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<div class="sanitation">
<p>
<f:for each="{sanitation}" as="sanitation">
<span class="badge bg-light">{f:translate(id: 'content.sanitation.{sanitation}', default: sanitation, extensionName: 'Thuecat')}</span>
</f:for>
</p>
</div>
</html>

View file

@ -0,0 +1,5 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:for each="{otherServices}" as="service">
<span class="badge bg-light">{f:translate(id: 'content.otherService.{service}', default: service, extensionName: 'Thuecat')}</span>
</f:for>
</html>

View file

@ -0,0 +1,9 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<div class="traffic">
<p>
<f:for each="{trafficInfrastructures}" as="trafficInfrastructure">
<span class="badge bg-light">{f:translate(id: 'content.trafficInfrastructure.{trafficInfrastructure}', default: trafficInfrastructure, extensionName: 'Thuecat')}</span>
</f:for>
</p>
</div>
</html>

View file

@ -0,0 +1,170 @@
<html
xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true"
>
<div class="thuecat">
<f:for each="{entities}" as="entity">
<div class="row thuecat__abstract">
<div class="col-12 col-md-6">
<f:if condition="{entity.slogan}"><span class="badge bg-danger">{entity.slogan}</span></f:if>
<h1>{entity.title} ({entity.town.title})</h1>
{entity.description -> f:format.html()}
</div>
<div class="col-12 col-md-6 order-first order-md-last">
<f:if condition="{entity.media.mainImage}">
<img class="img-fluid" src="{entity.media.mainImage.url}" />
<f:if condition="{entity.media.mainImage.author}">
<p>ⓒ {entity.media.mainImage.author}</p>
</f:if>
</f:if>
<f:for each="{entity.media.editorialImages}" as="image">
<f:image image="{image}" />
</f:for>
</div>
</div>
<div class="row thuecat__address">
<f:if condition="{entity.parkingFacilitiesNearBy -> f:count()} > 3">
<f:then>
<div class="col-md-4">
<f:if condition="{entity.address}">
<img src="{f:uri.resource(path:'Icons/icon-info.svg', extensionName:'Thuecat')}" width="30" class="svg-icon" alt="Address">
<h2>{f:translate(id: 'content.address', extensionName: 'Thuecat')}</h2>
{f:render(partial: 'Address', arguments: {address: entity.address, url: entity.url})}
</f:if>
<f:if condition="{entity.distanceToPublicTransport}">
<div class="distance-transport mt-5">
<img src="{f:uri.resource(path:'Icons/icon-bus.svg', extensionName:'Thuecat')}" width="30" class="svg-icon" alt="Address">
<h2>{f:translate(id: 'content.distanceToPublicTransport', extensionName: 'Thuecat')}</h2>
<p>
{entity.distanceToPublicTransport.value} {f:translate(id: 'content.unit.{entity.distanceToPublicTransport.unit}', default: entity.distanceToPublicTransport.unit, extensionName: 'Thuecat')}
</p>
</div>
</f:if>
</div>
<div class="col-md-8 multiple-parking">
<f:if condition="{entity.parkingFacilitiesNearBy}">
<img src="{f:uri.resource(path:'Icons/icon-parking.svg', extensionName:'Thuecat')}" width="30" class="svg-icon" alt="Address">
<h2>{f:translate(id: 'content.parkingFacilitiesNearBy', extensionName: 'Thuecat')}</h2>
{f:render(partial: 'Parking', arguments: {parkingFacilitiesNearBy: entity.parkingFacilitiesNearBy})}
</f:if>
</div>
</f:then>
<f:else>
<div class="col-md-4">
<f:if condition="{entity.address}">
<img src="{f:uri.resource(path:'Icons/icon-info.svg', extensionName:'Thuecat')}" width="30" class="svg-icon" alt="Address">
<h2>{f:translate(id: 'content.address', extensionName: 'Thuecat')}</h2>
{f:render(partial: 'Address', arguments: {address: entity.address, url: entity.url})}
</f:if>
</div>
<div class="col-md-4">
<f:if condition="{entity.distanceToPublicTransport}">
<img src="{f:uri.resource(path:'Icons/icon-bus.svg', extensionName:'Thuecat')}" width="30" class="svg-icon" alt="Address">
<h2>{f:translate(id: 'content.distanceToPublicTransport', extensionName: 'Thuecat')}</h2>
<p>
{entity.distanceToPublicTransport.value} {f:translate(id: 'content.unit.{entity.distanceToPublicTransport.unit}', default: entity.distanceToPublicTransport.unit, extensionName: 'Thuecat')}
</p>
</f:if>
</div>
<div class="col-md-4">
<f:if condition="{entity.parkingFacilitiesNearBy}">
<img src="{f:uri.resource(path:'Icons/icon-parking.svg', extensionName:'Thuecat')}" width="30" class="svg-icon" alt="Address">
<h2>{f:translate(id: 'content.parkingFacilitiesNearBy', extensionName: 'Thuecat')}</h2>
{f:render(partial: 'Parking', arguments: {parkingFacilitiesNearBy: entity.parkingFacilitiesNearBy})}
</f:if>
</div>
</f:else>
</f:if>
</div>
<div class="row thuecat__services">
<f:if condition="{entity.generalInformation || entity.otherServices || entity.petsAllowed || entity.isAccessibleForFree
|| entity.publicAccess || entity.accessibilitySpecification.certificationStatus || entity.museumServices
|| entity.digitalOffer || entity.trafficInfrastructures || entity.paymentAccepted || entity.availableLanguages
|| entity.sanitation || entity.photography || entity.startOfConstruction || entity.architecturalStyles}"
>
<div class="col-md-6">
<h2>{f:translate(id: 'content.generalInformation', extensionName: 'Thuecat')}</h2>
<p>
<f:if condition="{entity.otherServices}">
{f:render(partial: 'Service', arguments: {otherServices: entity.otherServices})}
</f:if>
<span class="badge bg-light">{f:translate(id: 'content.petsAllowed.{entity.petsAllowed}', default: entity.petsAllowed, extensionName: 'Thuecat')}</span>
<span class="badge bg-light">{f:translate(id: 'content.isAccessibleForFree.{entity.isAccessibleForFree}', default: entity.isAccessibleForFree, extensionName: 'Thuecat')}</span>
<span class="badge bg-light">{f:translate(id: 'content.publicAccess.{entity.publicAccess}', default: entity.publicAccess, extensionName: 'Thuecat')}</span>
<span class="badge bg-light">{f:translate(id: 'content.accessibilitySpecification.certificationStatus.{entity.accessibilitySpecification.certificationStatus}', default: entity.accessibilitySpecification.certificationStatus, extensionName: 'Thuecat')}</span>
</p>
<f:if condition="{entity.museumServices}">
<h3>{f:translate(id: 'content.museum', extensionName: 'Thuecat')}</h3>
{f:render(partial: 'Museum', arguments: {museumServices: entity.museumServices})}
</f:if>
<f:if condition="{entity.digitalOffer}">
<h3>{f:translate(id: 'content.digital', extensionName: 'Thuecat')}</h3>
{f:render(partial: 'Digital', arguments: {digitalOffer: entity.digitalOffer})}
</f:if>
<f:if condition="{entity.trafficInfrastructures}">
<h3>{f:translate(id: 'content.traffic', extensionName: 'Thuecat')}</h3>
{f:render(partial: 'Traffic', arguments: {trafficInfrastructures: entity.trafficInfrastructures})}
</f:if>
<f:if condition="{entity.paymentAccepted}">
<h3>{f:translate(id: 'content.payment', extensionName: 'Thuecat')}</h3>
{f:render(partial: 'Payment', arguments: {paymentAccepted: entity.paymentAccepted})}
</f:if>
<f:if condition="{entity.availableLanguages}">
<h3>{f:translate(id: 'content.languages', extensionName: 'Thuecat')}</h3>
{f:render(partial: 'Payment', arguments: {availableLanguages: entity.availableLanguages})}
</f:if>
<f:if condition="{entity.sanitation}">
<h3>{f:translate(id: 'content.sanitation', extensionName: 'Thuecat')}</h3>
{f:render(partial: 'Sanitation', arguments: {sanitation: entity.sanitation})}
</f:if>
<f:if condition="{entity.photography}">
<h3>{f:translate(id: 'content.photography', extensionName: 'Thuecat')}</h3>
{f:render(partial: 'Photography', arguments: {photography: entity.photography})}
</f:if>
<f:if condition="{entity.startOfConstruction}">
<h3>{f:translate(id: 'content.construction', extensionName: 'Thuecat')}</h3>
<p>
<span class="badge bg-light">{entity.startOfConstruction}</span>
</p>
</f:if>
<f:if condition="{entity.architecturalStyles}">
<h3>{f:translate(id: 'content.architecture', extensionName: 'Thuecat')}</h3>
<p>
<f:for each="{entity.architecturalStyles}" as="style">
<span class="badge bg-light">{f:translate(id: 'content.architecturalStyle.{style}', default: style, extensionName: 'Thuecat')}</span>
</f:for>
</p>
</f:if>
</div>
</f:if>
<f:if condition="{entity.openingHours}">
<div class="col-md-6">
<h2>{f:translate(id: 'content.openingHours', extensionName: 'Thuecat')}</h2>
{f:render(partial: 'Opening', arguments: {openingHours: entity.openingHours})}
</div>
</f:if>
<f:if condition="{entity.specialOpeningHours}">
<div class="col-md-6">
<h2>{f:translate(id: 'content.specialOpeningHours', extensionName: 'Thuecat')}</h2>
{f:render(partial: 'Opening', arguments: {openingHours: entity.specialOpeningHours})}
</div>
</f:if>
</div>
<div class="thuecat__offers">
<f:if condition="{entity.offers}">
<div class="row">
<h2 class="text-center">{f:translate(id: 'content.offers', extensionName: 'Thuecat')}</h2>
{f:render(partial: 'Offers', arguments: {offers: entity.offers, uid: entity.uid})}
</div>
</f:if>
<f:if condition="{entity.accessibilitySpecification.certificationStatus} != ''">
<div class="row">
<h2 class="text-center">{f:translate(id: 'content.accessibility', extensionName: 'Thuecat')}</h2>
{f:render(partial: 'Accessibility', arguments: {specification: entity.accessibilitySpecification, uid: entity.uid})}
</div>
</f:if>
</div>
</f:for>
</div>
</html>

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<dataset>
<tx_thuecat_tourist_attraction>
<uid>1</uid>
<pid>3</pid>
<title>Attraktion mit redaktionellen Bildern</title>
<editorial_images>2</editorial_images>
</tx_thuecat_tourist_attraction>
<sys_file_reference>
<uid>1</uid>
<uid_local>1</uid_local>
<uid_foreign>1</uid_foreign>
<tablenames>tx_thuecat_tourist_attraction</tablenames>
<fieldname>editorial_images</fieldname>
<sorting_foreign>1</sorting_foreign>
<table_local>sys_file</table_local>
</sys_file_reference>
<sys_file_reference>
<uid>2</uid>
<uid_local>2</uid_local>
<uid_foreign>1</uid_foreign>
<tablenames>tx_thuecat_tourist_attraction</tablenames>
<fieldname>editorial_images</fieldname>
<sorting_foreign>2</sorting_foreign>
<table_local>sys_file</table_local>
</sys_file_reference>
<sys_file>
<uid>1</uid>
<type>2</type>
<storage>1</storage>
<identifier>/tourismus/images/inhalte/sehenswertes/parks_gaerten/hirschgarten/2998_Spielplaetze_Hirschgarten.jpg</identifier>
<extension>jpg</extension>
<mime_type>image/jpeg</mime_type>
<name>2998_Spielplaetze_Hirschgarten.jpg</name>
<sha1>61079cbeb5d13c21d20dbbcc2e28e9c8fa04b3b4</sha1>
<size>7329219</size>
<identifier_hash>69066cc9c3b5ff135a7daa36059b18c75b3d9a23</identifier_hash>
<folder_hash>4dd66a1c0a2a0ab89a22bfe734df75d9750d28f2</folder_hash>
</sys_file>
<sys_file>
<uid>2</uid>
<type>2</type>
<storage>1</storage>
<identifier>/tourismus/images/inhalte/sehenswertes/sehenswuerdigkeiten/Petersberg/20_Erfurt-Schriftzug_Petersberg_2021__c_Stadtverwaltung_Erfurt_CC-BY-NC-SA.JPG</identifier>
<extension>JPG</extension>
<mime_type>image/jpeg</mime_type>
<name>20_Erfurt-Schriftzug_Petersberg_2021__c_Stadtverwaltung_Erfurt_CC-BY-NC-SA.JPG</name>
<sha1>f4c45d3c738d29162759ecd7d2dbc9af2a8f515f</sha1>
<size>2807135</size>
<identifier_hash>384f006a1452e901badb0db799fa7ff364e88a5e</identifier_hash>
<folder_hash>01086eae3464ef516edc0756ba3e12e35e09c33d</folder_hash>
</sys_file>
</dataset>

View file

@ -28,27 +28,27 @@ use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
class FrontendTest extends FunctionalTestCase class FrontendTest extends FunctionalTestCase
{ {
protected $coreExtensionsToLoad = [ protected function setUp(): void
{
$this->coreExtensionsToLoad = [
'fluid_styled_content', 'fluid_styled_content',
]; ];
protected $testExtensionsToLoad = [ $this->testExtensionsToLoad = [
'typo3conf/ext/thuecat/', 'typo3conf/ext/thuecat/',
]; ];
protected $pathsToLinkInTestInstance = [ $this->pathsToLinkInTestInstance = [
'typo3conf/ext/thuecat/Tests/Functional/Fixtures/Frontend/Sites/' => 'typo3conf/sites', 'typo3conf/ext/thuecat/Tests/Functional/Fixtures/Frontend/Sites/' => 'typo3conf/sites',
]; ];
protected function setUp(): void
{
parent::setUp(); parent::setUp();
$this->importDataSet('EXT:thuecat/Tests/Functional/Fixtures/Frontend/Content.xml'); $this->importDataSet('EXT:thuecat/Tests/Functional/Fixtures/Frontend/Content.xml');
$this->setUpFrontendRootPage(1, [ $this->setUpFrontendRootPage(1, [
'EXT:thuecat/Tests/Functional/Fixtures/Frontend/Rendering.typoscript',
'EXT:fluid_styled_content/Configuration/TypoScript/setup.typoscript', 'EXT:fluid_styled_content/Configuration/TypoScript/setup.typoscript',
'EXT:thuecat/Configuration/TypoScript/ContentElements/setup.typoscript', 'EXT:thuecat/Configuration/TypoScript/ContentElements/setup.typoscript',
'EXT:thuecat/Tests/Functional/Fixtures/Frontend/Rendering.typoscript',
]); ]);
} }
@ -690,4 +690,26 @@ class FrontendTest extends FunctionalTestCase
self::assertLessThan($positionSecondHour, $positionFirstHour, 'Second hour does not come after first hour.'); self::assertLessThan($positionSecondHour, $positionFirstHour, 'Second hour does not come after first hour.');
} }
/**
* @test
*/
public function editorialImagesOfTouristAttractionAreRenderedForDefaultLanguage(): void
{
$this->importDataSet('EXT:thuecat/Tests/Functional/Fixtures/Frontend/TouristAttractionWithEditorialImages.xml');
$request = new InternalRequest();
$request = $request->withPageId(2);
$html = (string)$this->executeFrontendRequest($request)->getBody();
self::assertStringContainsString(
'<img src="/fileadmin/tourismus/images/inhalte/sehenswertes/parks_gaerten/hirschgarten/2998_Spielplaetze_Hirschgarten.jpg" width="" height="" alt="" />',
$html
);
self::assertStringContainsString(
'<img src="/fileadmin/tourismus/images/inhalte/sehenswertes/sehenswuerdigkeiten/Petersberg/20_Erfurt-Schriftzug_Petersberg_2021__c_Stadtverwaltung_Erfurt_CC-BY-NC-SA.JPG" width="" height="" alt="" />',
$html,
);
}
} }

View file

@ -24,6 +24,7 @@ namespace WerkraumMedia\ThueCat\Tests\Unit\Domain\Model\Frontend;
*/ */
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use TYPO3\CMS\Core\Resource\FileReference;
use WerkraumMedia\ThueCat\Domain\Model\Frontend\Media; use WerkraumMedia\ThueCat\Domain\Model\Frontend\Media;
/** /**
@ -229,4 +230,98 @@ class MediaTest extends TestCase
$subject->getExtraImages()[1]['copyrightAuthor'] $subject->getExtraImages()[1]['copyrightAuthor']
); );
} }
/**
* @test
*/
public function returnsEmptyArrayAsDefaultForEditorialImages(): void
{
$subject = new Media('');
self::assertSame(
[],
$subject->getEditorialImages()
);
}
/**
* @test
*/
public function returnsSetEditorialImages(): void
{
$subject = new Media('');
$reference1 = $this->createStub(FileReference::class);
$reference2 = $this->createStub(FileReference::class);
$subject->setEditorialImages([
$reference1,
$reference2,
]);
$images = $subject->getEditorialImages();
self::assertCount(2, $images);
self::assertSame($reference1, $images[0]);
self::assertSame($reference2, $images[1]);
}
/**
* @test
*/
public function returnsEmptyArrayAsDefaultForAllImages(): void
{
$subject = new Media('');
self::assertSame(
[],
$subject->getAllImages()
);
}
/**
* @test
*/
public function returnsAllImages(): void
{
$subject = new Media(json_encode([
[
'mainImage' => false,
'type' => 'image',
'title' => 'Erfurt-Dom-und-Severikirche.jpg',
'author' => 'Full Name 1',
'license' => [
'author' => 'Full Name 1 license',
],
],
[
'mainImage' => false,
'type' => 'image',
'title' => 'Erfurt-Dom und Severikirche-beleuchtet.jpg',
'author' => 'Full Name 2',
'license' => [
'author' => 'Full Name 2 license',
],
],
]) ?: '');
$reference1 = $this->createStub(FileReference::class);
$reference2 = $this->createStub(FileReference::class);
$subject->setEditorialImages([
$reference1,
$reference2,
]);
self::assertSame(
$reference1,
$subject->getAllImages()[0]
);
self::assertSame(
$reference2,
$subject->getAllImages()[1]
);
self::assertSame(
'Full Name 1',
$subject->getAllImages()[2]['copyrightAuthor']
);
self::assertSame(
'Full Name 2',
$subject->getAllImages()[3]['copyrightAuthor']
);
}
} }

View file

@ -78,6 +78,7 @@ CREATE TABLE tx_thuecat_tourist_attraction (
available_languages text, available_languages text,
distance_to_public_transport text, distance_to_public_transport text,
accessibility_specification text, accessibility_specification text,
editorial_images int(11) unsigned DEFAULT '0' NOT NULL,
); );
CREATE TABLE tx_thuecat_parking_facility ( CREATE TABLE tx_thuecat_parking_facility (