Skip to content

Commit

Permalink
Merge pull request #1725 from danskernesdigitalebibliotek/redia-xml-f…
Browse files Browse the repository at this point in the history
…ixes

Redia XML fixes
  • Loading branch information
kasperg authored Dec 2, 2024
2 parents 639eaeb + 7451c6f commit 3476668
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 54 deletions.
45 changes: 45 additions & 0 deletions web/modules/custom/dpl_event/src/EventWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

namespace Drupal\dpl_event;

use Brick\Math\BigDecimal;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\drupal_typed\DrupalTyped;
use Drupal\paragraphs\ParagraphInterface;
use Drupal\recurring_events\Entity\EventInstance;
use Psr\Log\LoggerInterface;
use Safe\DateTime;
Expand Down Expand Up @@ -148,6 +150,49 @@ public function getState(): ?EventState {
return NULL;
}

/**
* Get the url of the event if available.
*
* The url will usually be the place where visitors can by tickets for the
* event.
*/
public function getLink() : ?string {
$linkField = $this->getField('event_link');
return $linkField?->getString();
}

/**
* Get the price(s) for the event.
*
* @return int[]|float[]
* Price(s) for the available ticket categories.
*/
public function getTicketPrices(): array {
$field = $this->getField('event_ticket_categories');
if (!$field instanceof FieldItemListInterface) {
return [];
}

$ticketCategories = $field->referencedEntities();
return array_map(function (ParagraphInterface $ticketCategory) {
return $ticketCategory->get('field_ticket_category_price')->value;
}, $ticketCategories);
}

/**
* Returns whether the event can be freely attended.
*
* This means that the event does not require ticketing or that all ticket
* categories are free.
*/
public function isFreeToAttend(): bool {
$nonFreePrice = array_filter($this->getTicketPrices(), function (int|float $price) {
$price = BigDecimal::of($price);
return !$price->isZero();
});
return empty($nonFreePrice);
}

/**
* Getting relevant updated date - either the series or instance.
*
Expand Down
27 changes: 27 additions & 0 deletions web/modules/custom/dpl_event/src/PriceFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,31 @@ public function formatRawPrice(string $price_string): string {
}
}

/**
* Formats a range of numeric prices into a string.
*
* Sorts and formats raw prices without currency pre/suffixes or rewriting 0
* to "Free".
*
* @param float[]|int[] $prices
* Array of price values (numbers).
*
* @return string
* Formatted price range string.
*/
public function formatRawPriceRange(array $prices): string {
sort($prices);
$lowest_price = min($prices);
$highest_price = max($prices);

if ($lowest_price != $highest_price) {
$lowest_price = $this->formatRawPrice((string) $lowest_price);
$highest_price = $this->formatRawPrice((string) $highest_price);

return "$lowest_price - $highest_price";
}

return $this->formatRawPrice((string) $lowest_price);
}

}
49 changes: 49 additions & 0 deletions web/modules/custom/dpl_event/tests/src/Unit/PriceFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,53 @@ public function testPriceRangeFormatting(
);
}

/**
* Provides examples of price arrays and their expected range formatting.
*
* @return array<array{array<int>, string}>
* Array of examples. Each example contains an array of prices and how
* they should be formatted. This matches signature of
* testPriceRangeFormatting().
*/
public function rawPriceRangeProvider(): array {
return [
// Only free prices.
[[0], "0"],
// Free and a single price.
[[0, 20], "0 - 20"],
// Range of prices.
[[20, 30], "20 - 30"],
// Single price.
[[20], "20"],
// Multiple prices.
[[10, 20, 30], "10 - 30"],
// Free with multiple prices.
[[0, 10, 20], "0 - 20"],
// Larger range of prices.
[[50, 100, 150], "50 - 150"],
[[0, 1000], "0 - 1000"],
];
}

/**
* Test raw price range formatting.
*
* @param int[] $prices
* Array of integers representing prices.
* @param string $expected
* Expected formatted string.
*
* @dataProvider rawPriceRangeProvider
*/
public function testRawPriceRangeFormatting(
array $prices,
string $expected,
): void {
$priceFormatter = new PriceFormatter($this->getStringTranslationStub(), $this->getConfigFactoryStub($this->mockConfig));
$this->assertSame(
$expected,
$priceFormatter->formatRawPriceRange($prices)
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

namespace Drupal\dpl_redia_legacy\Controller\RssFeeds;

use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\CacheableResponse;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Url;
use Drupal\dpl_event\PriceFormatter;
use Drupal\dpl_redia_legacy\RediaEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use function Safe\strtotime;

/**
Expand All @@ -24,6 +25,7 @@ class EventsController extends ControllerBase {
public function __construct(
protected FileUrlGeneratorInterface $fileUrlGenerator,
protected DateFormatterInterface $dateFormatter,
protected PriceFormatter $priceFormatter,
) {}

/**
Expand All @@ -33,16 +35,14 @@ public static function create(ContainerInterface $container): static {
return new static(
$container->get('file_url_generator'),
$container->get('date.formatter'),
$container->get('dpl_event.price_formatter'),
);
}

/**
* Getting the RSS/XML feed of the items.
*/
public function getFeed(Request $request): CacheableResponse {
// Disable the feed while we wait for validation by Redia.
return new CacheableResponse("Feed disabled temporarily", Response::HTTP_NOT_FOUND);
/*
$items = $this->getItems();

$rss_content = $this->buildRss($items, $request);
Expand All @@ -59,7 +59,6 @@ public function getFeed(Request $request): CacheableResponse {

$response->headers->set('Content-Type', 'application/rss+xml');
return $response;
*/
}

/**
Expand All @@ -68,7 +67,7 @@ public function getFeed(Request $request): CacheableResponse {
* @return \Drupal\dpl_redia_legacy\RediaEvent[]
* An array of necessary item fields, used in buildRss().
*/
protected function getItems(): array {
private function getItems(): array {

$storage = $this->entityTypeManager()->getStorage('eventinstance');
$query = $storage->getQuery()
Expand All @@ -84,7 +83,7 @@ protected function getItems(): array {
$items = [];

foreach ($events as $event) {
$items[] = new RediaEvent($event);
$items[] = new RediaEvent($event, $this->priceFormatter);
}

return $items;
Expand All @@ -98,7 +97,7 @@ protected function getItems(): array {
* @param \Symfony\Component\HttpFoundation\Request $request
* The request, for looking up the current site info.
*/
protected function buildRss(array $items, Request $request): string {
private function buildRss(array $items, Request $request): string {
$config = $this->config('system.site');
$site_title = $config->get('name');
$site_url = $request->getSchemeAndHttpHost();
Expand All @@ -108,48 +107,101 @@ protected function buildRss(array $items, Request $request): string {

$date = $this->dateFormatter->format(time(), 'custom', 'r');

$rss_feed = <<<RSS
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xml:base="$site_url" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:content-rss="http://xml.redia.dk/rss">
<channel>
<title>$site_title</title>
<link>$site_url</link>
<atom:link rel="self" href="$feed_url" />
<language>da</language>
<pubDate>$date</pubDate>
<lastBuildDate>$date</lastBuildDate>
RSS;

foreach ($items as $item) {
$rss_feed .= <<<ITEM
<item>
<title>{$item->title}</title>
<description>{$item->description}</description>
<author>{$item->author}</author>
<guid isPermaLink="false">{$item->id}</guid>
<pubDate>{$item->date}</pubDate>
<source url="$feed_url">$site_title</source>
<media:content url="{$item->media?->url}" fileSize="{$item->media?->size}" type="{$item->media?->type}" contentmedium="{$item->media?->medium}" width="{$item->media?->width}" height="{$item->media?->height}">
<media:hash algo="md5">{$item->media?->md5}</media:hash>
</media:content>
<media:thumbnail url="{$item->mediaThumbnail?->url}" width="{$item->mediaThumbnail?->width}" height="{$item->mediaThumbnail?->height}" />
<content-rss:subheadline>{$item->subtitle}</content-rss:subheadline>
<content-rss:arrangement-starttime>{$item->startTime}</content-rss:arrangement-starttime>
<content-rss:arrangement-endtime>{$item->endTime}</content-rss:arrangement-endtime>
<content-rss:arrangement-location>{$item->branch?->label()}</content-rss:arrangement-location>
<content-rss:library-id>{$item->branch?->id()}</content-rss:library-id>
<content-rss:promoted>{$item->promoted}</content-rss:promoted>
</item>
ITEM;
}

$rss_feed .= <<<RSS
</channel>
</rss>
RSS;

return $rss_feed;

// Disable formatting rules. We use indentation to mark start/end elements.
// phpcs:disable Drupal.WhiteSpace.ScopeIndent.IncorrectExact
// @formatter:off
$xml = new \XMLWriter();
$xml->openMemory();
$xml->startDocument('1.0', 'UTF-8');
$xml->startElement('rss');
$xml->writeAttribute('version', '2.0');
$xml->writeAttribute('xml:base', $site_url);
// We intentionally do not use the built-in XML Writer namespace handling.
// This allows us to produce output that matches the existing
// implementation as closely as possible.
$xml->writeAttribute('xmlns:atom', 'http://www.w3.org/2005/Atom');
$xml->writeAttribute('xmlns:media', 'http://search.yahoo.com/mrss/');
$xml->writeAttribute('xmlns:content-rss', 'http://xml.redia.dk/rss');

$xml->startElement('channel');
$xml->writeElement('title', $site_title);
$xml->writeElement('link', $site_url);
$xml->startElement('atom:link');
$xml->writeAttribute('rel', 'self');
$xml->writeAttribute('href', $feed_url);
$xml->endElement();
$xml->writeElement('language', 'da');
$xml->writeElement('pubDate', $date);
$xml->writeElement('lastBuildDate', $date);

foreach ($items as $item) {
$xml->startElement('item');
$xml->writeElement('title', $item->title);
$xml->writeElement('description', $item->description);
$xml->writeElement('author', $item->author);
$xml->startElement('guid');
$xml->writeAttribute('isPermaLink', 'false');
$xml->text((string) $item->id);
$xml->endElement();
$xml->writeElement('pubDate', $item->date);
$xml->startElement('source');
$xml->writeAttribute('url', $feed_url);
$xml->text($site_title);
$xml->endElement();

if ($item->media && $item->media->url) {
$xml->startElement('media:content');
$xml->writeAttribute('url', $item->media->url);
$xml->writeAttribute('fileSize', (string) $item->media->size);
$xml->writeAttribute('type', (string) $item->media->type);
$xml->writeAttribute('medium', $item->media->medium);
$xml->writeAttribute('width', (string) $item->media->width);
$xml->writeAttribute('height', (string) $item->media->height);
if ($item->media->md5) {
$xml->startElement('media:hash');
$xml->writeAttribute('algo', 'md5');
$xml->text($item->media->md5);
$xml->endElement();
}
$xml->endElement();
}

if ($item->mediaThumbnail && $item->mediaThumbnail->url) {
$xml->startElement('media:thumbnail');
$xml->writeAttribute('url', $item->mediaThumbnail->url);
$xml->writeAttribute('width', (string) $item->mediaThumbnail->width);
$xml->writeAttribute('height', (string) $item->mediaThumbnail->height);
$xml->endElement();
}

$xml->writeElement('content-rss:subheadline', $item->subtitle);
$xml->writeElement('content-rss:arrangement-starttime', $item->startTime);
$xml->writeElement('content-rss:arrangement-endtime', $item->endTime);

if ($item->branch) {
$xml->writeElement('content-rss:arrangement-location', $item->branch->label());
$xml->writeElement('content-rss:library-id', (string) $item->branch->id());
}

if ($item->bookingUrl) {
$xml->writeElement('content-rss:booking-url', $item->bookingUrl);
}

// Events without a price element are interpreted as free.
if ($item->prices) {
$xml->writeElement('content-rss:arrangement-price', $item->prices);
}

$xml->writeElement('content-rss:promoted', $item->promoted);
$xml->endElement();
}
$xml->endElement();

$xml->endElement();
$xml->endDocument();
return $xml->outputMemory();
// @formatter:on
// phpcs:enable Drupal.WhiteSpace.ScopeIndent.IncorrectExact
}

}
Loading

0 comments on commit 3476668

Please sign in to comment.