|
32 | 32 | use OCP\IUserManager; |
33 | 33 | use OCP\IUserSession; |
34 | 34 | use OCP\Mail\IMailer; |
| 35 | +use PhpOffice\PhpSpreadsheet\Cell\DataType; |
| 36 | +use PhpOffice\PhpSpreadsheet\IOFactory; |
35 | 37 | use PHPUnit\Framework\MockObject\MockObject; |
36 | 38 | use Psr\Log\LoggerInterface; |
37 | 39 |
|
@@ -565,6 +567,63 @@ public function testGetSubmissionsDataThrowsExceptionOnInvalidFormat() { |
565 | 567 | $this->submissionService->getSubmissionsData($form, 'invalid'); |
566 | 568 | } |
567 | 569 |
|
| 570 | + public function dataExportNumericTyping() { |
| 571 | + return [ |
| 572 | + // Radio (and similar choice) answers are always strings, even when the option labels are |
| 573 | + // pure numbers. For spreadsheet formats they must be written as numbers so they can be |
| 574 | + // aggregated (e.g. averaged) in the spreadsheet application. |
| 575 | + 'radio-numeric-label-xlsx' => ['3', 'xlsx', DataType::TYPE_NUMERIC, 3], |
| 576 | + 'radio-numeric-label-ods' => ['5', 'ods', DataType::TYPE_NUMERIC, 5], |
| 577 | + 'numeric-zero' => ['0', 'xlsx', DataType::TYPE_NUMERIC, 0], |
| 578 | + 'numeric-decimal' => ['3.5', 'xlsx', DataType::TYPE_NUMERIC, 3.5], |
| 579 | + // Values starting with a formula-trigger character keep the string typing so they are |
| 580 | + // never interpreted as formulas. |
| 581 | + 'formula' => ['=SUM(A1:A2)', 'xlsx', DataType::TYPE_STRING, '=SUM(A1:A2)'], |
| 582 | + 'negative-number' => ['-5', 'xlsx', DataType::TYPE_STRING, '-5'], |
| 583 | + // Strings whose canonical numeric form would differ must stay text to avoid data loss |
| 584 | + // (leading-zero phone numbers, scientific notation, ...). |
| 585 | + 'leading-zero' => ['0123456789', 'xlsx', DataType::TYPE_STRING, '0123456789'], |
| 586 | + 'scientific' => ['1e5', 'xlsx', DataType::TYPE_STRING, '1e5'], |
| 587 | + 'text' => ['Option A', 'xlsx', DataType::TYPE_STRING, 'Option A'], |
| 588 | + ]; |
| 589 | + } |
| 590 | + |
| 591 | + /** |
| 592 | + * @dataProvider dataExportNumericTyping |
| 593 | + * |
| 594 | + * @param string $value the raw (string) answer as stored in the database |
| 595 | + * @param string $fileFormat spreadsheet format to export to |
| 596 | + * @param string $expectedType the expected PhpSpreadsheet cell data type |
| 597 | + * @param mixed $expectedValue the expected cell value after a write/read round-trip |
| 598 | + */ |
| 599 | + public function testExportNumericTyping(string $value, string $fileFormat, string $expectedType, mixed $expectedValue) { |
| 600 | + // Hand out real temporary files so the spreadsheet can actually be written and reloaded. |
| 601 | + $this->tempManager->expects($this->any()) |
| 602 | + ->method('getTemporaryFile') |
| 603 | + ->willReturnCallback(fn () => tempnam(sys_get_temp_dir(), 'forms-export-')); |
| 604 | + |
| 605 | + $content = self::invokePrivate( |
| 606 | + $this->submissionService, |
| 607 | + 'exportData', |
| 608 | + [['Question 1'], [[$value]], $fileFormat, null], |
| 609 | + ); |
| 610 | + |
| 611 | + // Reload the produced file and inspect the actual data type of the answer cell. |
| 612 | + $reloadPath = tempnam(sys_get_temp_dir(), 'forms-reload-'); |
| 613 | + file_put_contents($reloadPath, $content); |
| 614 | + $cell = IOFactory::load($reloadPath)->getSheet(0)->getCell([1, 2]); |
| 615 | + |
| 616 | + $this->assertSame($expectedType, $cell->getDataType()); |
| 617 | + if ($expectedType === DataType::TYPE_NUMERIC) { |
| 618 | + $this->assertEquals($expectedValue, $cell->getValue()); |
| 619 | + } else { |
| 620 | + // assertSame guards against a numeric-looking string being silently coerced. |
| 621 | + $this->assertSame($expectedValue, $cell->getValue()); |
| 622 | + } |
| 623 | + |
| 624 | + unlink($reloadPath); |
| 625 | + } |
| 626 | + |
568 | 627 | /** |
569 | 628 | * Setting up a very simple default CsvTest |
570 | 629 | */ |
|
0 commit comments