From 72b1ea58fc944aed63b2f61e1629b17ee053c4e4 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Tue, 10 Nov 2026 08:58:09 +0100 Subject: [PATCH 1/2] fix(analytics): do not fail on forms without answers The data of a form without answers was never initialized, so sorting it failed. Answers of deleted questions were counted for a question without text. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Andy Scherzinger --- lib/Analytics/AnalyticsDatasource.php | 6 +++++- .../Analytics/AnalyticsDatasourceTest.php | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/Analytics/AnalyticsDatasource.php b/lib/Analytics/AnalyticsDatasource.php index 62f520b4b4..37b493a30e 100644 --- a/lib/Analytics/AnalyticsDatasource.php +++ b/lib/Analytics/AnalyticsDatasource.php @@ -98,6 +98,7 @@ public function getTemplate(): array { public function readData($option): array { $questionMap = []; $answerCounts = []; + $results = []; $formId = $option['formId']; @@ -109,7 +110,10 @@ public function readData($option): array { $submissions = $this->submissionService->getSubmissions($formId); foreach ($submissions as $submission) { foreach ($submission['answers'] as $answer) { - $questionText = $questionMap[$answer['questionId']]; + $questionText = $questionMap[$answer['questionId']] ?? null; + if ($questionText === null) { + continue; + } $key = $questionText . '|' . $answer['text']; if (isset($answerCounts[$key])) { $answerCounts[$key]++; diff --git a/tests/Unit/Analytics/AnalyticsDatasourceTest.php b/tests/Unit/Analytics/AnalyticsDatasourceTest.php index c01330a684..7f470ef9d8 100644 --- a/tests/Unit/Analytics/AnalyticsDatasourceTest.php +++ b/tests/Unit/Analytics/AnalyticsDatasourceTest.php @@ -123,4 +123,25 @@ public function testReadData() { $this->assertEquals($expectedData, $data); } + + public function testReadDataWithoutAnswers(): void { + $this->formsService->method('getQuestions')->willReturn([['id' => 1, 'text' => 'Question 1']]); + $this->submissionService->method('getSubmissions')->willReturn([]); + $this->l10n->method('t')->willReturnArgument(0); + + $this->assertSame([], $this->analyticsDatasource->readData(['formId' => 123])['data']); + } + + public function testReadDataSkipsAnswersOfDeletedQuestions(): void { + $this->formsService->method('getQuestions')->willReturn([['id' => 1, 'text' => 'Question 1']]); + $this->submissionService->method('getSubmissions')->willReturn([ + ['answers' => [ + ['questionId' => 1, 'text' => 'Answer 1'], + ['questionId' => 2, 'text' => 'Answer of a deleted question'], + ]], + ]); + $this->l10n->method('t')->willReturnArgument(0); + + $this->assertSame([['Question 1', 'Answer 1', 1]], $this->analyticsDatasource->readData(['formId' => 123])['data']); + } } From fab1423f27a266c330de6ae590685ac6373bd974 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Tue, 10 Nov 2026 21:22:13 +0100 Subject: [PATCH 2/2] fix(analytics): count the answers of each question separately The Analytics data source counted answers under the key "question text|answer text" and split that key at "|" again. A "|" in a question title or an answer cut the texts apart, and questions with the same title shared one count. Count the answers per question id and answer text instead. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Andy Scherzinger --- lib/Analytics/AnalyticsDatasource.php | 17 +++++--------- .../Analytics/AnalyticsDatasourceTest.php | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/Analytics/AnalyticsDatasource.php b/lib/Analytics/AnalyticsDatasource.php index 37b493a30e..2bf8f6e516 100644 --- a/lib/Analytics/AnalyticsDatasource.php +++ b/lib/Analytics/AnalyticsDatasource.php @@ -110,22 +110,17 @@ public function readData($option): array { $submissions = $this->submissionService->getSubmissions($formId); foreach ($submissions as $submission) { foreach ($submission['answers'] as $answer) { - $questionText = $questionMap[$answer['questionId']] ?? null; - if ($questionText === null) { + if (!isset($questionMap[$answer['questionId']])) { continue; } - $key = $questionText . '|' . $answer['text']; - if (isset($answerCounts[$key])) { - $answerCounts[$key]++; - } else { - $answerCounts[$key] = 1; - } + $answerCounts[$answer['questionId']][$answer['text']] = ($answerCounts[$answer['questionId']][$answer['text']] ?? 0) + 1; } } - foreach ($answerCounts as $key => $count) { - [$questionText, $text] = explode('|', $key); - $results[] = [$questionText, $text, $count]; + foreach ($answerCounts as $questionId => $counts) { + foreach ($counts as $text => $count) { + $results[] = [$questionMap[$questionId], (string)$text, $count]; + } } // Sort by questionText ascending and count descending diff --git a/tests/Unit/Analytics/AnalyticsDatasourceTest.php b/tests/Unit/Analytics/AnalyticsDatasourceTest.php index 7f470ef9d8..1598d8b187 100644 --- a/tests/Unit/Analytics/AnalyticsDatasourceTest.php +++ b/tests/Unit/Analytics/AnalyticsDatasourceTest.php @@ -144,4 +144,26 @@ public function testReadDataSkipsAnswersOfDeletedQuestions(): void { $this->assertSame([['Question 1', 'Answer 1', 1]], $this->analyticsDatasource->readData(['formId' => 123])['data']); } + + public function testReadDataKeepsSeparatorsAndQuestionsWithTheSameTitle(): void { + $this->formsService->method('getQuestions')->willReturn([ + ['id' => 1, 'text' => 'Pick A|B'], + ['id' => 2, 'text' => 'Comment'], + ['id' => 3, 'text' => 'Comment'], + ]); + $this->submissionService->method('getSubmissions')->willReturn([ + ['answers' => [ + ['questionId' => 1, 'text' => 'a|b'], + ['questionId' => 2, 'text' => '5'], + ['questionId' => 3, 'text' => '5'], + ]], + ]); + $this->l10n->method('t')->willReturnArgument(0); + + $this->assertSame([ + ['Comment', '5', 1], + ['Comment', '5', 1], + ['Pick A|B', 'a|b', 1], + ], $this->analyticsDatasource->readData(['formId' => 123])['data']); + } }