Commit 3e0b7674 authored by Andrés Moreno Rubio's avatar Andrés Moreno Rubio
Browse files

fix: refactor how CSV distributions are read and rendered

parent ae2b5b11
Loading
Loading
Loading
Loading
Loading
+1 −1
Changes for README.md: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -59,7 +59,7 @@ over each, on every push, in a job that blocks the pipeline. The numbers and wha
| `templates/` | **19 Twig files: 14 overrides of Drupal templates and five for this theme's own hooks.** The overrides are page, region, node, field, table, Views table, form element, pager, three menus (generic, footer, quick access), breadcrumb, the site-branding block and the file link. The five own hooks are the masthead, the header search, the award total, the sign-in panel and the CSV-distribution table, registered in `agora_theme.theme`. The last is the shortest file here and deliberately so: it prints a `#type: table` render array, which means the actual table markup is the override two rows up, already under the accessibility gate, rather than a second convention beside it. The table is still the centrepiece: it adds the `scope="col"`/`scope="row"` that Drupal's own table preprocess never sets, and renders the caption as the table's first child. ⚠️ **The sign-in template is not what a stock Ágora install shows.** `gin_login` takes `user.login` and four neighbouring routes and hands them to the site's admin theme, so this panel is reached on a standalone install of `agora_theme` and not on a site built from the Ágora site template. That is D-052, and it is recorded rather than fixed here |
| `components/` | **Four single-directory components — heading, text, section and group** — the pieces a site owner places on a page in Drupal Canvas without touching code. Layout and type only: no component here fetches data, because a component that fetches data is a block and the eight blocks on the front page are correct where they are (D-049). **No prop accepts a free colour or a free size.** Every one is an enum over a step `css/tokens.css` already declares and `tests/bin/contrast-check` has already measured, because that checker runs at build time and can say nothing about a value typed into a form afterwards. `tests/bin/component-metadata` is what stops one of them silently disappearing from the palette |
| `fonts/` | Public Sans, two weights, self-hosted, plus its `OFL.txt`. See "Third-party assets" |
| `agora_theme.theme` | **The theme's PHP — 3,806 lines, 47 functions** (it read 3,665 and 45 until 2026-09-25, when the header mark gained two functions of its own - `_agora_theme_logo_dimensions()` reads a site's own logo file's real width and height, falling back to the theme's known 120 by 120 grid when it is the shipped default or cannot be measured, and `_agora_theme_svg_dimensions()` is the pure parser beside it that reads a `viewBox` or a `width`/`height` pair out of an SVG document, which `getimagesize()` cannot do at all - 141 lines net, T-0721; it read 3,626 and 43 until 2026-09-24, when the two table hooks that attach `js/table-scroll.js` arrived; ⚠️ it read 2,528 and 28 until 2026-09-20; the count was already 32 before that day's eleven new functions, so it was stale by four and nobody re-ran `grep -c`), and it was missing from this table until 2026-09-06 despite being the largest source file here. It registers the four theme hooks named under `templates/`, preprocesses the page, the blocks and the footer menu, reads each setting through a `?? default` so an upgraded site behaves like a fresh one, and holds the money and award-total helpers the unit tests cover. Since 2026-09-20 it also **reads a dataset's CSV distribution and hands Twig a rows/header array** — D-038 option A — bounded by three ceilings the `phpunit` job prints, and refusing whole rather than drawing part of a file |
| `agora_theme.theme` | **The theme's PHP — 3,893 lines, 49 functions** (it read 3,806 and 47 until 2026-09-27, when the CSV preview gained two helpers, `_agora_theme_csv_download_allowed()` and `_agora_theme_csv_no_reuse()` - 87 lines net; it read 3,665 and 45 until 2026-09-25, when the header mark gained two functions of its own - `_agora_theme_logo_dimensions()` reads a site's own logo file's real width and height, falling back to the theme's known 120 by 120 grid when it is the shipped default or cannot be measured, and `_agora_theme_svg_dimensions()` is the pure parser beside it that reads a `viewBox` or a `width`/`height` pair out of an SVG document, which `getimagesize()` cannot do at all - 141 lines net, T-0721; it read 3,626 and 43 until 2026-09-24, when the two table hooks that attach `js/table-scroll.js` arrived; ⚠️ it read 2,528 and 28 until 2026-09-20; the count was already 32 before that day's eleven new functions, so it was stale by four and nobody re-ran `grep -c`), and it was missing from this table until 2026-09-06 despite being the largest source file here. It registers the four theme hooks named under `templates/`, preprocesses the page, the blocks and the footer menu, reads each setting through a `?? default` so an upgraded site behaves like a fresh one, and holds the money and award-total helpers the unit tests cover. Since 2026-09-20 it also **reads a dataset's CSV distribution and hands Twig a rows/header array** — D-038 option A — bounded by three ceilings the `phpunit` job prints, and refusing whole rather than drawing part of a file |
| `composer.json` | Package metadata. No dependencies, so there is nothing to justify |
| `LICENSE.txt` | GPL-2.0-or-later, byte-identical to the site template's copy — **verified as committed**, 2026-09-06: both repositories store the same 18,002-byte blob, `94fb846`. ⚠️ Compared in a Windows working copy the two files differ at byte 35, because this checkout carries CRLF; the difference is a checkout attribute and not a difference in what ships |
| `.gitlab-ci.yml` | The shared Drupal Association pipeline, plus the variables this project sets on it — see [Continuous integration](#continuous-integration) |
+89 −2
Changes for agora_theme.theme: 89 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -70,6 +70,7 @@ use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Render\Element;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
use Drupal\Core\StringTranslation\ByteSizeMarkup;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Template\Attribute;
@@ -3177,6 +3178,16 @@ function agora_theme_preprocess_node(array &$variables): void {
      (string) $distribution['file']->getFilename(),
      count($found)
    );
    // Set before the element is built: _agora_theme_csv_element() sets the
    // same metadata on the table it returns, but it can also return NULL, which
    // carries no cacheability metadata. Setting it here covers both outcomes.
    // `#cache` set on a preprocess variable bubbles into the render array being
    // built.
    if (!isset($variables['#cache']) || !is_array($variables['#cache'])) {
      $variables['#cache'] = [];
    }
    _agora_theme_csv_no_reuse((string) $distribution['file']->getFileUri(), $variables['#cache']);

    $element = _agora_theme_csv_element($distribution['file'], $caption);
    if ($element === NULL) {
      continue;
@@ -3707,6 +3718,65 @@ function _agora_theme_csv_note(array $refusal, string $filename): TranslatableMa
  }
}

/**
 * Whether a distribution's bytes may be read for this request.
 *
 * Decided by the URI's scheme and by what hook_file_download() answered for it.
 *
 * Kept free of \Drupal:: like the other decisions in this file, so a test can
 * drive every shape of answer a module can give without a container.
 *
 * The rule is the one core's FileDownloadController applies, so a file shown
 * as a table is one the same visitor could also download from the link above
 * it: a `-1` from any module means no, and for a scheme other than `public` no
 * answer at all also means no. A `public://` file is served by the webserver
 * directly, so it is always readable here.
 *
 * @param string|false $scheme
 *   The URI's stream wrapper scheme, as StreamWrapperManager::getScheme()
 *   returns it.
 * @param array $file_download_headers
 *   Every result of invoking hook_file_download() for the file's URI, as
 *   ModuleHandlerInterface::invokeAll() collects them.
 *
 * @return bool
 *   TRUE when the file's bytes may be read.
 */
function _agora_theme_csv_download_allowed($scheme, array $file_download_headers): bool {
  if ($scheme === 'public') {
    return TRUE;
  }
  if (in_array(-1, $file_download_headers, TRUE)) {
    return FALSE;
  }
  return $file_download_headers !== [];
}

/**
 * Makes the preview of a distribution outside `public://` uncacheable.
 *
 * Whether such a preview is drawn depends on what hook_file_download()
 * answered for this request, and those answers carry no cacheability metadata,
 * so the result is not cached at all. `max-age` 0 covers the render cache and
 * the Dynamic Page Cache, and also means this function runs again on the next
 * request. The internal Page Cache does not read `max-age`, so its kill switch
 * is triggered as well.
 *
 * A `public://` distribution's cacheability is left as it was.
 *
 * @param string $uri
 *   The distribution file's URI.
 * @param array $cache
 *   A render array's `#cache` property, modified in place.
 */
function _agora_theme_csv_no_reuse(string $uri, array &$cache): void {
  if (StreamWrapperManager::getScheme($uri) === 'public') {
    return;
  }
  $cache['max-age'] = 0;
  \Drupal::service('page_cache_kill_switch')->trigger();
}

/**
 * Builds the render array for one distribution.
 *
@@ -3731,16 +3801,30 @@ function _agora_theme_csv_note(array $refusal, string $filename): TranslatableMa
 * renders as the text it is, which is the only correct reading of a value in a
 * file a public body published.
 *
 * The file is read only when _agora_theme_csv_download_allowed() says it may
 * be, from what hook_file_download() answers for its URI, and the element's
 * cacheability is set by _agora_theme_csv_no_reuse().
 *
 * @param \Drupal\file\FileInterface $file
 *   The distribution file.
 * @param string $caption
 *   The table's name.
 *
 * @return array|null
 *   A render array, or NULL when the file could not be read at all.
 *   A render array, or NULL when the file could not be read at all, or may
 *   not be read for this request.
 */
function _agora_theme_csv_element(FileInterface $file, string $caption): ?array {
  $read = _agora_theme_csv_read((string) $file->getFileUri(), AGORA_THEME_CSV_BYTE_CEILING);
  $uri = (string) $file->getFileUri();
  $allowed = _agora_theme_csv_download_allowed(
    StreamWrapperManager::getScheme($uri),
    \Drupal::moduleHandler()->invokeAll('file_download', [$uri])
  );
  if (!$allowed) {
    return NULL;
  }

  $read = _agora_theme_csv_read($uri, AGORA_THEME_CSV_BYTE_CEILING);
  if ($read['text'] === NULL && $read['refusal'] === NULL) {
    return NULL;
  }
@@ -3749,6 +3833,9 @@ function _agora_theme_csv_element(FileInterface $file, string $caption): ?array
    '#theme' => 'agora_csv_table',
    '#cache' => ['tags' => $file->getCacheTags()],
  ];
  // agora_theme_preprocess_node() calls the same helper for every distribution
  // it finds, which covers the case where NULL is returned above.
  _agora_theme_csv_no_reuse($uri, $element['#cache']);

  if ($read['refusal'] !== NULL) {
    $element['#note'] = _agora_theme_csv_note($read['refusal'], (string) $file->getFilename());