From 9b2833f733d1bca7c40d3a6e3e079f595189792b Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 14 Aug 2026 15:20:33 +0200 Subject: [PATCH 1/2] Add a topic field to initiatives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The title names the project, the topic places it in the wider programme it belongs to — "DS4SSCC" against "Digital Europe Blueprint for Data Space for smart and sustainable cities and communities". Sits under the title on the form and on the initiative page, is covered by the free-text filter, and is included in the CSV export. --- CHANGELOG.md | 5 +++ migrations/Version20260814131716.php | 26 ++++++++++++++++ src/Controller/InitiativeController.php | 4 ++- src/DataFixtures/AppFixtures.php | 11 +++++++ src/Entity/Initiative.php | 20 ++++++++++++ src/Form/InitiativeType.php | 6 ++++ src/Repository/InitiativeRepository.php | 1 + templates/initiative/_form.html.twig | 1 + templates/initiative/show.html.twig | 4 +++ tests/Controller/InitiativeControllerTest.php | 31 +++++++++++++++++++ tests/Unit/Entity/InitiativeTest.php | 2 ++ translations/messages.da.yaml | 2 ++ translations/messages.en.yaml | 2 ++ 13 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 migrations/Version20260814131716.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b75412..4bf3a21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* [PR-25](https://github.com/itk-dev/itk-projects/pull/25) + Add a Topic field to initiatives, naming the wider programme an initiative is + part of. Shown under the title on the form and on the initiative page, covered + by the free-text filter, and included in the CSV export. + ## [0.2.0] - 2026-06-30 * [PR-23](https://github.com/itk-dev/itk-projects/pull/23) diff --git a/migrations/Version20260814131716.php b/migrations/Version20260814131716.php new file mode 100644 index 0000000..2347cbb --- /dev/null +++ b/migrations/Version20260814131716.php @@ -0,0 +1,26 @@ +addSql('ALTER TABLE initiative ADD topic LONGTEXT DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE initiative DROP topic'); + } +} diff --git a/src/Controller/InitiativeController.php b/src/Controller/InitiativeController.php index 7b2f8d0..8615e1c 100644 --- a/src/Controller/InitiativeController.php +++ b/src/Controller/InitiativeController.php @@ -64,7 +64,8 @@ public function export(Request $request, InitiativeRepository $initiatives, Tran $response = new StreamedResponse(function () use ($rows, $translator, $translate): void { $csv = Writer::createFromStream(fopen('php://output', 'w')); $csv->insertOne([ - 'id', $translator->trans('initiative.title'), $translator->trans('initiative.status'), + 'id', $translator->trans('initiative.title'), $translator->trans('initiative.topic'), + $translator->trans('initiative.status'), $translator->trans('initiative.area'), $translator->trans('initiative.initiative_type'), $translator->trans('initiative.organizational_anchoring'), $translator->trans('initiative.endorsement'), $translator->trans('initiative.endorsement_author'), $translator->trans('initiative.budget'), @@ -80,6 +81,7 @@ public function export(Request $request, InitiativeRepository $initiatives, Tran $csv->insertOne([ (string) $row->getId(), $row->getTitle(), + $row->getTopic(), $translate($row->getStatus()), $row->getArea()?->getName(), $translate($row->getInitiativeType()), diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php index aa04607..8cd35ce 100644 --- a/src/DataFixtures/AppFixtures.php +++ b/src/DataFixtures/AppFixtures.php @@ -26,6 +26,12 @@ class AppFixtures extends Fixture private const array STRATEGIES = ['Klimaplan 2030', 'Erhvervsplan', 'Børn- og ungepolitik', 'Mobilitetsplan', 'Digitaliseringsstrategi', 'Sundhedspolitik']; private const array DEPARTMENTS = ['ITK Development', 'CFIA', 'Aarhus CityLab', 'Stab', 'OS2', 'AI Lab', 'IOT Lab', 'GTM', 'Fut Lab']; private const array AREAS = ['Klima og miljø', 'Mobilitet', 'Velfærd', 'Kultur og fritid', 'Uddannelse', 'Erhverv', 'Digitalisering', 'Byudvikling']; + private const array TOPICS = [ + 'Digital Europe Blueprint for Data Space for smart and sustainable cities and communities.', + 'Horizon Europe — Climate-neutral and smart cities mission.', + 'Den fællesoffentlige digitaliseringsstrategi 2022–2026.', + 'Interreg Øresund-Kattegat-Skagerrak.', + ]; public function __construct(private readonly UserPasswordHasherInterface $hasher) { @@ -127,6 +133,11 @@ public function load(ObjectManager $manager): void ->setBudget(mt_rand(1, 40) * 50000); $initiative->setCreatedBy($users[array_rand($users)]); + // Not every initiative belongs to a wider programme. + if (0 !== $index % 3) { + $initiative->setTopic(self::TOPICS[$index % \count(self::TOPICS)]); + } + if (0 !== $index % 4) { $initiative->setEndorsementAuthor($endorsers[array_rand($endorsers)]); } diff --git a/src/Entity/Initiative.php b/src/Entity/Initiative.php index a274d9c..8f56cf6 100644 --- a/src/Entity/Initiative.php +++ b/src/Entity/Initiative.php @@ -35,6 +35,14 @@ class Initiative extends AbstractEntity #[ORM\Column(length: 255)] private ?string $title = null; + /** + * Which wider programme the initiative is a part of — the title names this + * project, the topic places it ("DS4SSCC" → "Digital Europe Blueprint for + * Data Space for smart and sustainable cities and communities"). + */ + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $topic = null; + #[ORM\ManyToOne(targetEntity: Area::class)] #[ORM\JoinColumn(onDelete: 'SET NULL')] private ?Area $area = null; @@ -134,6 +142,18 @@ public function setTitle(?string $title): static return $this; } + public function getTopic(): ?string + { + return $this->topic; + } + + public function setTopic(?string $topic): static + { + $this->topic = $topic; + + return $this; + } + public function getArea(): ?Area { return $this->area; diff --git a/src/Form/InitiativeType.php b/src/Form/InitiativeType.php index 29a8d35..a0e0e4e 100644 --- a/src/Form/InitiativeType.php +++ b/src/Form/InitiativeType.php @@ -40,6 +40,12 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => 'initiative.title', 'help' => 'initiative.title_help', ]) + ->add('topic', TextareaType::class, [ + 'label' => 'initiative.topic', + 'required' => false, + 'attr' => ['rows' => 4], + 'help' => 'initiative.topic_help', + ]) ->add('area', EntityType::class, [ 'label' => 'initiative.area', 'class' => Area::class, diff --git a/src/Repository/InitiativeRepository.php b/src/Repository/InitiativeRepository.php index 18ce917..245dd1b 100644 --- a/src/Repository/InitiativeRepository.php +++ b/src/Repository/InitiativeRepository.php @@ -42,6 +42,7 @@ public function search(InitiativeFilter $filter): QueryBuilder $ors = [ 'LOWER(i.title) LIKE :q', + 'LOWER(i.topic) LIKE :q', 'LOWER(i.description) LIKE :q', 'LOWER(i.statusAdditional) LIKE :q', // Related names, matched without joining the root query so the diff --git a/templates/initiative/_form.html.twig b/templates/initiative/_form.html.twig index 3cecdae..447f765 100644 --- a/templates/initiative/_form.html.twig +++ b/templates/initiative/_form.html.twig @@ -46,6 +46,7 @@ {% set title_attr = {'data-action': 'input->title-mirror#update', 'data-autosave-required': true} %} {% if not initiative.id %}{% set title_attr = title_attr|merge({autofocus: true}) %}{% endif %} {{ form_row(form.title, {attr: title_attr}) }} + {{ form_row(form.topic) }}
{{ form_row(form.area) }} {{ form_row(form.initiativeType) }} diff --git a/templates/initiative/show.html.twig b/templates/initiative/show.html.twig index 1102366..778b22b 100644 --- a/templates/initiative/show.html.twig +++ b/templates/initiative/show.html.twig @@ -29,6 +29,10 @@
{% if initiative.description %}

{{ initiative.description }}

{% else %}

{% endif %} + {% if initiative.topic %} +

{{ 'initiative.topic'|trans }}

+

{{ initiative.topic }}

+ {% endif %} {% if initiative.statusAdditional %}

{{ 'initiative.status_additional'|trans }}

{{ initiative.statusAdditional }}

diff --git a/tests/Controller/InitiativeControllerTest.php b/tests/Controller/InitiativeControllerTest.php index f3b1f4f..57e35de 100644 --- a/tests/Controller/InitiativeControllerTest.php +++ b/tests/Controller/InitiativeControllerTest.php @@ -44,6 +44,37 @@ public function testNewPersistsInitiativeWithInlineContactAndDropsEmptyMedia(): $em->flush(); } + public function testTopicIsSavedShownSearchableAndExported(): void + { + $this->loginAsAdmin(); + $topic = 'Digital Europe Blueprint '.uniqid(); + + $crawler = $this->client->request('GET', '/initiatives/new'); + $token = (string) $crawler->filter('input[name="initiative[_token]"]')->attr('value'); + $this->client->request('POST', '/initiatives/new', [ + 'initiative' => ['title' => 'Topic initiative', 'topic' => $topic, '_token' => $token], + ]); + $this->assertResponseRedirects(); + + $initiative = $this->initiatives()->findOneBy(['title' => 'Topic initiative']); + self::assertInstanceOf(Initiative::class, $initiative); + self::assertSame($topic, $initiative->getTopic()); + $id = (string) $initiative->getId(); + + $crawler = $this->client->request('GET', '/initiatives/'.$id); + self::assertStringContainsString($topic, $crawler->filter('.card__body')->first()->text()); + + // The free-text filter searches the topic alongside title and description. + $crawler = $this->client->request('GET', '/initiatives?q='.urlencode($topic)); + self::assertStringContainsString('Topic initiative', $crawler->filter('#initiative-results')->text()); + + $this->client->request('GET', '/initiatives/export?q='.urlencode($topic)); + $csv = (string) $this->client->getInternalResponse()->getContent(); + self::assertStringContainsString($topic, $csv); + + $this->removeInitiative($id); + } + public function testEditUpdatesInitiative(): void { $this->loginAsAdmin(); diff --git a/tests/Unit/Entity/InitiativeTest.php b/tests/Unit/Entity/InitiativeTest.php index 9613b3f..65ec08f 100644 --- a/tests/Unit/Entity/InitiativeTest.php +++ b/tests/Unit/Entity/InitiativeTest.php @@ -48,6 +48,7 @@ public function testScalarAccessors(): void $initiative = (new Initiative()) ->setTitle('Grøn omstilling') + ->setTopic('Digital Europe Blueprint for Data Space') ->setArea($area) ->setDescription('Beskrivelse') ->setInitiativeType(InitiativeType::Project) @@ -61,6 +62,7 @@ public function testScalarAccessors(): void ->setTimePeriodEnd($end); self::assertSame('Grøn omstilling', $initiative->getTitle()); + self::assertSame('Digital Europe Blueprint for Data Space', $initiative->getTopic()); self::assertSame($area, $initiative->getArea()); self::assertSame('Beskrivelse', $initiative->getDescription()); self::assertSame(InitiativeType::Project, $initiative->getInitiativeType()); diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index e1cdcbe..b9f411c 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -184,6 +184,8 @@ dashboard: initiative: title: Titel title_help: Et kort, sigende navn på initiativet. + topic: Emne + topic_help: "Det overordnede program, initiativet er en del af, fx “Digital Europe Blueprint for Data Space for smart and sustainable cities and communities”." area: Område area_help: Det tematiske område initiativet hører under. description: Kort beskrivelse diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index 607bf6d..dc7351a 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -184,6 +184,8 @@ dashboard: initiative: title: Title title_help: A short, descriptive name for the initiative. + topic: Topic + topic_help: "The wider programme this initiative is part of, e.g. “Digital Europe Blueprint for Data Space for smart and sustainable cities and communities”." area: Area area_help: The thematic area the initiative belongs to. description: Short description From cac9f21ef0bc6a2da291e9dcb812ba3f4bb0b936 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 17 Aug 2026 11:10:02 +0200 Subject: [PATCH 2/2] Drop the example from the topic help text --- translations/messages.da.yaml | 2 +- translations/messages.en.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index b9f411c..ba3928f 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -185,7 +185,7 @@ initiative: title: Titel title_help: Et kort, sigende navn på initiativet. topic: Emne - topic_help: "Det overordnede program, initiativet er en del af, fx “Digital Europe Blueprint for Data Space for smart and sustainable cities and communities”." + topic_help: Det overordnede program, initiativet er en del af. area: Område area_help: Det tematiske område initiativet hører under. description: Kort beskrivelse diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index dc7351a..797a4c9 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -185,7 +185,7 @@ initiative: title: Title title_help: A short, descriptive name for the initiative. topic: Topic - topic_help: "The wider programme this initiative is part of, e.g. “Digital Europe Blueprint for Data Space for smart and sustainable cities and communities”." + topic_help: The wider programme this initiative is part of. area: Area area_help: The thematic area the initiative belongs to. description: Short description