From 8acbe136d4f3fa2d0acdc65461208d3cb1f7e115 Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Mon, 28 Nov 2022 11:06:54 +0100 Subject: [PATCH] Add special opening hours (#84) It is possible to define special opening hours, e.g. for holidays. Those are now also imported and provided to the templates. Relates: #10185 --- Classes/Domain/Import/Entity/Place.php | 34 +++++ .../Typo3Converter/GeneralConverter.php | 11 +- Classes/Domain/Model/Frontend/Place.php | 10 ++ .../TCA/tx_thuecat_parking_facility.php | 10 +- .../TCA/tx_thuecat_tourist_attraction.php | 10 +- Documentation/Changelog/1.3.0.rst | 4 + Resources/Private/Language/de.locallang.xlf | 4 + Resources/Private/Language/locallang.xlf | 3 + Resources/Private/Language/locallang_tca.xlf | 6 + .../ContentElement/TouristAttraction.html | 6 + .../resources/special-opening-hours.json | 120 ++++++++++++++++++ ...uristAttractionWithSpecialOpeningHours.xml | 76 +++++++++++ ...ristAttractionsWithSpecialOpeningHours.csv | 4 + Tests/Functional/FrontendTest.php | 83 ++++++++++++ Tests/Functional/ImportTest.php | 15 +++ ext_tables.sql | 2 + phpstan-baseline.neon | 4 +- 17 files changed, 395 insertions(+), 7 deletions(-) create mode 100644 Tests/Functional/Fixtures/Import/Guzzle/thuecat.org/resources/special-opening-hours.json create mode 100644 Tests/Functional/Fixtures/Import/ImportsTouristAttractionWithSpecialOpeningHours.xml create mode 100644 Tests/Functional/Fixtures/Import/ImportsTouristAttractionsWithSpecialOpeningHours.csv diff --git a/Classes/Domain/Import/Entity/Place.php b/Classes/Domain/Import/Entity/Place.php index c83fea3..dfc905e 100644 --- a/Classes/Domain/Import/Entity/Place.php +++ b/Classes/Domain/Import/Entity/Place.php @@ -53,6 +53,11 @@ class Place extends Base */ protected $openingHours = []; + /** + * @var OpeningHour[] + */ + protected $specialOpeningHours = []; + /** * @var ForeignReference[] */ @@ -162,6 +167,20 @@ class Place extends Base ); } + /** + * @return OpeningHour[] + */ + public function getSpecialOpeningHoursSpecification(): array + { + return GeneralUtility::makeInstance(DateBasedFilter::class) + ->filterOutPreviousDates( + $this->specialOpeningHours, + function (OpeningHour $hour): ?\DateTimeImmutable { + return $hour->getValidThrough(); + } + ); + } + /** * @return ForeignReference[] */ @@ -201,6 +220,21 @@ class Place extends Base { } + /** + * @internal for mapping via Symfony component. + */ + public function addSpecialOpeningHoursSpecification(OpeningHour $openingHour): void + { + $this->specialOpeningHours[] = $openingHour; + } + + /** + * @internal for mapping via Symfony component. + */ + public function removeSpecialOpeningHoursSpecification(OpeningHour $openingHour): void + { + } + /** * @internal for mapping via Symfony component. */ diff --git a/Classes/Domain/Import/Typo3Converter/GeneralConverter.php b/Classes/Domain/Import/Typo3Converter/GeneralConverter.php index bc19cf0..0eba844 100644 --- a/Classes/Domain/Import/Typo3Converter/GeneralConverter.php +++ b/Classes/Domain/Import/Typo3Converter/GeneralConverter.php @@ -35,6 +35,7 @@ use WerkraumMedia\ThueCat\Domain\Import\Entity\Organisation; use WerkraumMedia\ThueCat\Domain\Import\Entity\ParkingFacility; use WerkraumMedia\ThueCat\Domain\Import\Entity\Place; use WerkraumMedia\ThueCat\Domain\Import\Entity\Properties\ForeignReference; +use WerkraumMedia\ThueCat\Domain\Import\Entity\Properties\OpeningHour; use WerkraumMedia\ThueCat\Domain\Import\Entity\Properties\PriceSpecification; use WerkraumMedia\ThueCat\Domain\Import\Entity\TouristAttraction; use WerkraumMedia\ThueCat\Domain\Import\Entity\TouristInformation; @@ -228,7 +229,8 @@ class GeneralConverter implements Converter, LoggerAwareInterface 'parking_facility_near_by' => $entity instanceof Base ? implode(',', $this->getParkingFacilitiesNearByUids($entity)) : '', - 'opening_hours' => $entity instanceof Place ? $this->getOpeningHours($entity) : '', + 'opening_hours' => $entity instanceof Place ? $this->getOpeningHours($entity->getOpeningHoursSpecification()) : '', + 'special_opening_hours' => $entity instanceof Place ? $this->getOpeningHours($entity->getSpecialOpeningHoursSpecification()) : '', 'address' => $entity instanceof Place ? $this->getAddress($entity) : '', 'offers' => $entity instanceof Place ? $this->getOffers($entity) : '', 'other_service' => method_exists($entity, 'getOtherServices') ? implode(',', $entity->getOtherServices()) : '', @@ -404,11 +406,14 @@ class GeneralConverter implements Converter, LoggerAwareInterface ]; } - private function getOpeningHours(Place $entity): string + /** + * @param OpeningHour[] $openingHours + */ + private function getOpeningHours(array $openingHours): string { $data = []; - foreach ($entity->getOpeningHoursSpecification() as $openingHour) { + foreach ($openingHours as $openingHour) { $data[] = array_filter([ 'opens' => $openingHour->getOpens()->format('H:i:s'), 'closes' => $openingHour->getCloses()->format('H:i:s'), diff --git a/Classes/Domain/Model/Frontend/Place.php b/Classes/Domain/Model/Frontend/Place.php index 3bcb831..ac79592 100644 --- a/Classes/Domain/Model/Frontend/Place.php +++ b/Classes/Domain/Model/Frontend/Place.php @@ -38,6 +38,11 @@ abstract class Place extends Base */ protected $openingHours = null; + /** + * @var OpeningHours|null + */ + protected $specialOpeningHours = null; + /** * @var ObjectStorage */ @@ -88,6 +93,11 @@ abstract class Place extends Base return $this->openingHours; } + public function getSpecialOpeningHours(): ?OpeningHours + { + return $this->specialOpeningHours; + } + public function getParkingFacilitiesNearBy(): ObjectStorage { return $this->parkingFacilityNearBy; diff --git a/Configuration/TCA/tx_thuecat_parking_facility.php b/Configuration/TCA/tx_thuecat_parking_facility.php index 06b0797..b520a55 100644 --- a/Configuration/TCA/tx_thuecat_parking_facility.php +++ b/Configuration/TCA/tx_thuecat_parking_facility.php @@ -125,6 +125,14 @@ return (static function (string $extensionKey, string $tableName) { 'readOnly' => true, ], ], + 'special_opening_hours' => [ + 'label' => $languagePath . '.special_opening_hours', + 'l10n_mode' => 'exclude', + 'config' => [ + 'type' => 'text', + 'readOnly' => true, + ], + ], 'address' => [ 'label' => $languagePath . '.address', 'l10n_mode' => 'exclude', @@ -200,7 +208,7 @@ return (static function (string $extensionKey, string $tableName) { ], 'types' => [ '0' => [ - 'showitem' => '--palette--;;language, title, description, sanitation, other_service, traffic_infrastructure, payment_accepted, distance_to_public_transport, opening_hours, offers, address, media, remote_id, --div--;' . $languagePath . '.tab.relations, town, managed_by', + 'showitem' => '--palette--;;language, title, description, sanitation, other_service, traffic_infrastructure, payment_accepted, distance_to_public_transport, opening_hours, special_opening_hours, offers, address, media, remote_id, --div--;' . $languagePath . '.tab.relations, town, managed_by', ], ], ]; diff --git a/Configuration/TCA/tx_thuecat_tourist_attraction.php b/Configuration/TCA/tx_thuecat_tourist_attraction.php index 638ef40..aae33ea 100644 --- a/Configuration/TCA/tx_thuecat_tourist_attraction.php +++ b/Configuration/TCA/tx_thuecat_tourist_attraction.php @@ -205,6 +205,14 @@ return (static function (string $extensionKey, string $tableName) { 'readOnly' => true, ], ], + 'special_opening_hours' => [ + 'label' => $languagePath . '.special_opening_hours', + 'l10n_mode' => 'exclude', + 'config' => [ + 'type' => 'text', + 'readOnly' => true, + ], + ], 'address' => [ 'label' => $languagePath . '.address', 'l10n_mode' => 'exclude', @@ -303,7 +311,7 @@ return (static function (string $extensionKey, string $tableName) { ], 'types' => [ '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, offers, accessibility_specification, address, 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, media, remote_id, --div--;' . $languagePath . '.tab.relations, town, managed_by, parking_facility_near_by', ], ], ]; diff --git a/Documentation/Changelog/1.3.0.rst b/Documentation/Changelog/1.3.0.rst index 1155925..58c5ce1 100644 --- a/Documentation/Changelog/1.3.0.rst +++ b/Documentation/Changelog/1.3.0.rst @@ -22,6 +22,10 @@ Features This should improve the UX of website visitors. It is not possible yet to sort opening hours by hand within the thuecat backend. +* Support special opening hours. + It is possible to define special opening hours, e.g. for holidays. + Those are now also imported and provided to the templates. + Fixes ----- diff --git a/Resources/Private/Language/de.locallang.xlf b/Resources/Private/Language/de.locallang.xlf index 1f3edd9..2ba0f30 100644 --- a/Resources/Private/Language/de.locallang.xlf +++ b/Resources/Private/Language/de.locallang.xlf @@ -99,6 +99,10 @@ Address Anschrift + + Special Opening hours + Sonderöffnungszeiten + Opening hours Öffnungszeiten diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index 9775c60..6bb100d 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -149,6 +149,9 @@ Address + + Special Opening hours + Opening hours diff --git a/Resources/Private/Language/locallang_tca.xlf b/Resources/Private/Language/locallang_tca.xlf index 550b63a..8af3537 100644 --- a/Resources/Private/Language/locallang_tca.xlf +++ b/Resources/Private/Language/locallang_tca.xlf @@ -141,6 +141,9 @@ Opening Hours + + Special Opening Hours + Address @@ -193,6 +196,9 @@ Opening Hours + + Special Opening Hours + Address diff --git a/Resources/Private/Templates/Frontend/ContentElement/TouristAttraction.html b/Resources/Private/Templates/Frontend/ContentElement/TouristAttraction.html index e7da633..6fb5157 100644 --- a/Resources/Private/Templates/Frontend/ContentElement/TouristAttraction.html +++ b/Resources/Private/Templates/Frontend/ContentElement/TouristAttraction.html @@ -137,6 +137,12 @@ {f:render(partial: 'Opening', arguments: {openingHours: entity.openingHours})} + +
+

{f:translate(id: 'content.specialOpeningHours', extensionName: 'Thuecat')}

+ {f:render(partial: 'Opening', arguments: {openingHours: entity.specialOpeningHours})} +
+
diff --git a/Tests/Functional/Fixtures/Import/Guzzle/thuecat.org/resources/special-opening-hours.json b/Tests/Functional/Fixtures/Import/Guzzle/thuecat.org/resources/special-opening-hours.json new file mode 100644 index 0000000..648b8af --- /dev/null +++ b/Tests/Functional/Fixtures/Import/Guzzle/thuecat.org/resources/special-opening-hours.json @@ -0,0 +1,120 @@ +{ + "@context": { + "cdb": "https://thuecat.org/ontology/cdb/1.0/", + "dachkg": "https://thuecat.org/ontology/dachkg/1.0/", + "dbo": "http://dbpedia.org/ontology/", + "dsv": "http://ontologies.sti-innsbruck.at/dsv/", + "foaf": "http://xmlns.com/foaf/0.1/", + "owl": "http://www.w3.org/2002/07/owl#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "schema": "http://schema.org/", + "sh": "http://www.w3.org/ns/shacl#", + "thuecat": "https://thuecat.org/ontology/thuecat/1.0/", + "ttgds": "https://thuecat.org/ontology/ttgds/1.0/", + "xsd": "http://www.w3.org/2001/XMLSchema#" + }, + "@graph": [ + { + "@id": "https://thuecat.org/resources/835224016581-dara", + "@type": [ + "schema:Place", + "schema:Thing", + "schema:TouristAttraction", + "ttgds:PointOfInterest", + "thuecat:Building", + "thuecat:ReligiousBuilding", + "thuecat:Cathedral", + "thuecat:CatholicChurch", + "thuecat:Dome" + ], + "thuecat:contentResponsible": { + "@id": "https://thuecat.org/resources/018132452787-ngbe" + }, + "schema:availableLanguage": [ + { + "@type": "thuecat:Language", + "@value": "thuecat:German" + }, + { + "@type": "thuecat:Language", + "@value": "thuecat:English" + } + ], + "schema:name": [ + { + "@language": "de", + "@value": "Dom St. Marien" + }, + { + "@language": "en", + "@value": "Cathedral of St. Mary" + } + ], + "schema:specialOpeningHoursSpecification": [ + { + "@id": "genid-6b61946f64bc4b518eac7d499bb5375f-b10", + "@type": [ + "schema:Thing", + "schema:Intangible", + "schema:StructuredValue", + "schema:OpeningHoursSpecification" + ], + "schema:closes": { + "@type": "schema:Time", + "@value": "14:00:00" + }, + "schema:dayOfWeek": { + "@type": "schema:DayOfWeek", + "@value": "schema:Saturday" + }, + "schema:opens": { + "@type": "schema:Time", + "@value": "10:00:00" + }, + "schema:validFrom": { + "@type": "schema:Date", + "@value": "2050-12-31" + }, + "schema:validThrough": { + "@type": "schema:Date", + "@value": "2050-12-31" + } + }, + { + "@id": "genid-6b61946f64bc4b518eac7d499bb5375f-b11", + "@type": [ + "schema:Thing", + "schema:Intangible", + "schema:StructuredValue", + "schema:OpeningHoursSpecification" + ], + "schema:closes": { + "@type": "schema:Time", + "@value": "14:00:00" + }, + "schema:dayOfWeek": { + "@type": "schema:DayOfWeek", + "@value": "schema:Saturday" + }, + "schema:opens": { + "@type": "schema:Time", + "@value": "10:00:00" + }, + "schema:validFrom": { + "@type": "schema:Date", + "@value": "2021-12-31" + }, + "schema:validThrough": { + "@type": "schema:Date", + "@value": "2021-12-31" + } + } + ], + "schema:url": { + "@type": "schema:URL", + "@value": "http://www.dom-erfurt.de" + } + } + ] +} diff --git a/Tests/Functional/Fixtures/Import/ImportsTouristAttractionWithSpecialOpeningHours.xml b/Tests/Functional/Fixtures/Import/ImportsTouristAttractionWithSpecialOpeningHours.xml new file mode 100644 index 0000000..50c2234 --- /dev/null +++ b/Tests/Functional/Fixtures/Import/ImportsTouristAttractionWithSpecialOpeningHours.xml @@ -0,0 +1,76 @@ + + + + 1 + 0 + 1613400587 + 1613400558 + 1 + 4 + Rootpage + 1 + + + 10 + 1 + 1613400587 + 1613400558 + 1 + 254 + Storage folder + + + + 1 + 0 + English + en-us-gb + en + + + + 2 + 0 + French + fr + fr + + + + 1 + 0 + 1613400587 + 1613400558 + 1 + 0 + The tourist attraction + static + + + + + + + 10 + + + + + + + + https://thuecat.org/resources/special-opening-hours + + + + 0 + + + + + + + + ]]> + + diff --git a/Tests/Functional/Fixtures/Import/ImportsTouristAttractionsWithSpecialOpeningHours.csv b/Tests/Functional/Fixtures/Import/ImportsTouristAttractionsWithSpecialOpeningHours.csv new file mode 100644 index 0000000..8e2b176 --- /dev/null +++ b/Tests/Functional/Fixtures/Import/ImportsTouristAttractionsWithSpecialOpeningHours.csv @@ -0,0 +1,4 @@ +"tx_thuecat_tourist_attraction",,,,,,,, +,"uid","pid","sys_language_uid","l18n_parent","l10n_source","remote_id","title","special_opening_hours" +,1,10,0,0,0,"https://thuecat.org/resources/835224016581-dara","Dom St. Marien","[{""opens"":""10:00:00"",""closes"":""14:00:00"",""from"":{""date"":""2050-12-31 00:00:00.000000"",""timezone_type"":3,""timezone"":""UTC""},""through"":{""date"":""2050-12-31 00:00:00.000000"",""timezone_type"":3,""timezone"":""UTC""},""daysOfWeek"":[""Saturday""]}]" +,2,10,1,1,1,"https://thuecat.org/resources/835224016581-dara","Cathedral of St. Mary","[{""opens"":""10:00:00"",""closes"":""14:00:00"",""from"":{""date"":""2050-12-31 00:00:00.000000"",""timezone_type"":3,""timezone"":""UTC""},""through"":{""date"":""2050-12-31 00:00:00.000000"",""timezone_type"":3,""timezone"":""UTC""},""daysOfWeek"":[""Saturday""]}]" diff --git a/Tests/Functional/FrontendTest.php b/Tests/Functional/FrontendTest.php index 5f3f2a9..2031e78 100644 --- a/Tests/Functional/FrontendTest.php +++ b/Tests/Functional/FrontendTest.php @@ -561,4 +561,87 @@ class FrontendTest extends FunctionalTestCase self::assertLessThan($positionThirdHour, $positionSecondHour, 'Third hour does not come after second hour.'); self::assertLessThan($positionSecondHour, $positionFirstHour, 'Second hour does not come after first hour.'); } + + /** + * @test + */ + public function specialOpeningHoursAreRendered(): void + { + $this->importDataSet('EXT:thuecat/Tests/Functional/Fixtures/Frontend/TouristAttractionsOpeningHours.xml'); + + $hidden = new \DateTimeImmutable('yesterday'); + $available = new \DateTimeImmutable('tomorrow'); + $available2 = new \DateTimeImmutable('+3 days'); + + $this->getConnectionPool() + ->getConnectionForTable('tx_thuecat_tourist_attraction') + ->update( + 'tx_thuecat_tourist_attraction', + ['special_opening_hours' => json_encode([ + [ + 'closes' => '17:00:00', + 'opens' => '13:00:00', + 'daysOfWeek' => ['Sunday'], + 'from' => [ + 'date' => $hidden->modify('-1 day')->format('Y-m-d') . ' 00:00:00.000000', + 'timezone' => 'UTC', + 'timezone_type' => 3, + ], + 'through' => [ + 'date' => $hidden->format('Y-m-d') . ' 00:00:00.000000', + 'timezone' => 'UTC', + 'timezone_type' => 3, + ], + ], + [ + 'closes' => '17:00:00', + 'opens' => '13:00:00', + 'daysOfWeek' => ['Sunday'], + 'from' => [ + 'date' => $available2->modify('-1 day')->format('Y-m-d') . ' 00:00:00.000000', + 'timezone' => 'UTC', + 'timezone_type' => 3, + ], + 'through' => [ + 'date' => $available2->format('Y-m-d') . ' 00:00:00.000000', + 'timezone' => 'UTC', + 'timezone_type' => 3, + ], + ], + [ + 'closes' => '17:00:00', + 'opens' => '13:00:00', + 'daysOfWeek' => ['Sunday'], + 'from' => [ + 'date' => $available->modify('-1 day')->format('Y-m-d') . ' 00:00:00.000000', + 'timezone' => 'UTC', + 'timezone_type' => 3, + ], + 'through' => [ + 'date' => $available->format('Y-m-d') . ' 00:00:00.000000', + 'timezone' => 'UTC', + 'timezone_type' => 3, + ], + ], + ])], + ['uid' => 1] + ); + + $request = new InternalRequest(); + $request = $request->withPageId(2); + + $result = (string)$this->executeFrontendRequest($request)->getBody(); + + self::assertStringNotContainsString($hidden->modify('-1 day')->format('d.m.Y'), $result, 'Filtered date is shown'); + self::assertStringNotContainsString($hidden->format('d.m.Y'), $result, 'Filtered date is shown'); + self::assertStringContainsString($available->modify('-1 day')->format('d.m.Y'), $result, 'First special opening hour is missing'); + self::assertStringContainsString($available->format('d.m.Y'), $result, 'First special opening hour is missing'); + self::assertStringContainsString($available2->modify('-1 day')->format('d.m.Y'), $result, 'Second special opening hour is missing'); + self::assertStringContainsString($available2->format('d.m.Y'), $result, 'Second special opening hour is missing'); + + $positionFirstHour = mb_strpos($result, $available->format('d.m.Y')); + $positionSecondHour = mb_strpos($result, $available2->format('d.m.Y')); + + self::assertLessThan($positionSecondHour, $positionFirstHour, 'Second hour does not come after first hour.'); + } } diff --git a/Tests/Functional/ImportTest.php b/Tests/Functional/ImportTest.php index af0dd74..cd1be3b 100644 --- a/Tests/Functional/ImportTest.php +++ b/Tests/Functional/ImportTest.php @@ -230,6 +230,21 @@ class ImportTest extends TestCase $this->assertCSVDataSet('EXT:thuecat/Tests/Functional/Fixtures/Import/ImportsTouristAttractionsWithFilteredOpeningHours.csv'); } + /** + * @test + */ + public function importsTouristAttractionsWithSpecialOpeningHours(): void + { + $this->importDataSet(__DIR__ . '/Fixtures/Import/ImportsTouristAttractionWithSpecialOpeningHours.xml'); + GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/special-opening-hours.json'); + GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/018132452787-ngbe.json'); + + $configuration = $this->get(ImportConfigurationRepository::class)->findByUid(1); + $this->get(Importer::class)->importConfiguration($configuration); + + $this->assertCSVDataSet('EXT:thuecat/Tests/Functional/Fixtures/Import/ImportsTouristAttractionsWithSpecialOpeningHours.csv'); + } + /** * @test */ diff --git a/ext_tables.sql b/ext_tables.sql index daea5ab..9ba00c5 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -55,6 +55,7 @@ CREATE TABLE tx_thuecat_tourist_attraction ( title varchar(255) DEFAULT '' NOT NULL, description text, opening_hours text, + special_opening_hours text, address text, media text, offers text, @@ -83,6 +84,7 @@ CREATE TABLE tx_thuecat_parking_facility ( title varchar(255) DEFAULT '' NOT NULL, description text, opening_hours text, + special_opening_hours text, address text, media text, offers text, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 304c995..8fed8ec 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -351,11 +351,11 @@ parameters: - message: "#^Cannot call method findByUid\\(\\) on mixed\\.$#" - count: 9 + count: 10 path: Tests/Functional/ImportTest.php - message: "#^Cannot call method importConfiguration\\(\\) on mixed\\.$#" - count: 9 + count: 10 path: Tests/Functional/ImportTest.php