From a796460998359825661bfd0e86834328383c7711 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 8 Nov 2024 20:03:29 +0100 Subject: [PATCH 01/11] test(Integration): extends csv import test data Signed-off-by: Arthur Schiwon --- tests/integration/features/APIv1.feature | 34 ++++++++++--------- .../features/bootstrap/FeatureContext.php | 30 ++++++++++++++++ 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/tests/integration/features/APIv1.feature b/tests/integration/features/APIv1.feature index b70fc3c98..333d225ee 100644 --- a/tests/integration/features/APIv1.feature +++ b/tests/integration/features/APIv1.feature @@ -188,30 +188,32 @@ Feature: APIv1 Then user "participant1" deletes table with keyword "Rows check" - @api1 + @api1 @import Scenario: Import csv table Given file "/import.csv" exists for user "participant1" with following data - | Col1 | Col2 | Col3 | num | emoji | special | - | Val1 | Val2 | Val3 | 1 | 💙 | Ä | - | great | news | here | 99 | ⚠️ | Ö | + | Col1 | Col2 | Col3 | num | emoji | special | date | truth | + | Val1 | Val2 | Val3 | 1 | 💙 | Ä | 2024-02-24 | false | + | great | news | here | 99 | ⚠️ | Ö | 2016-06-01 | true | Given table "Import test" with emoji "👨🏻‍💻" exists for user "participant1" as "base1" When user imports file "/import.csv" into last created table Then import results have the following data - | found_columns_count | 6 | - | created_columns_count | 6 | + | found_columns_count | 8 | + | created_columns_count | 8 | | inserted_rows_count | 2 | | errors_count | 0 | - Then table has at least following columns - | Col1 | - | Col2 | - | Col3 | - | num | - | emoji | - | special | + Then table has at least following typed columns + | Col1 | text | + | Col2 | text | + | Col3 | text | + | num | number | + | emoji | text | + | special | text | + | date | datetime | + | truth | selection | Then table contains at least following rows - | Col1 | Col2 | Col3 | num | emoji | special | - | Val1 | Val2 | Val3 | 1 | 💙 | Ä | - | great | news | here | 99 | ⚠️ | Ö | + | Col1 | Col2 | Col3 | num | emoji | special | date | truth | + | Val1 | Val2 | Val3 | 1 | 💙 | Ä | 2024-02-24 | false | + | great | news | here | 99 | ⚠️ | Ö | 2016-06-01 | true | @api1 Scenario: Create, edit and delete views diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index e3b2f1128..725a93be1 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -1190,6 +1190,36 @@ public function tableColumns(?TableNode $body = null): void { } } + /** + * @Then table has at least following typed columns + * + * @param TableNode|null $body + */ + public function tableTypedColumns(?TableNode $body = null): void { + $this->sendRequest( + 'GET', + '/apps/tables/api/1/tables/'.$this->tableId.'/columns' + ); + + $data = $this->getDataFromResponse($this->response); + Assert::assertEquals(200, $this->response->getStatusCode()); + + // check if no columns exists + if ($body === null) { + Assert::assertCount(0, $data); + return; + } + + $colByTitle = []; + foreach ($data as $d) { + $colByTitle[$d['title']] = $d['type']; + } + foreach ($body->getRows() as $columnData) { + Assert::assertArrayHasKey($columnData[0], $colByTitle); + Assert::assertSame($columnData[1], $colByTitle[$columnData[0]], sprintf('Column "%s" has unexpected type "%s"', $columnData[0], $colByTitle[$columnData[0]])); + } + } + /** * @Then user deletes last created column */ From 126fa47db0226d53a09dd50ccba34381066dd581 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Tue, 12 Nov 2024 23:46:20 +0100 Subject: [PATCH 02/11] test(Integration): extend import test with xlsx data sets Signed-off-by: Arthur Schiwon --- lib/Service/ImportService.php | 10 +++- tests/integration/features/APIv1.feature | 48 ++++++++++++++++++ .../features/bootstrap/FeatureContext.php | 19 ++++++- .../resources/import-from-libreoffice.xlsx | Bin 0 -> 6498 bytes .../resources/import-from-ms365.xlsx | Bin 0 -> 13695 bytes 5 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 tests/integration/resources/import-from-libreoffice.xlsx create mode 100644 tests/integration/resources/import-from-ms365.xlsx diff --git a/lib/Service/ImportService.php b/lib/Service/ImportService.php index a30725019..cf701cdd6 100644 --- a/lib/Service/ImportService.php +++ b/lib/Service/ImportService.php @@ -285,8 +285,14 @@ public function import(?int $tableId, ?int $viewId, string $path, bool $createMi * @throws PermissionError */ private function loop(Worksheet $worksheet): void { - $firstRow = $worksheet->getRowIterator()->current(); - $secondRow = $worksheet->getRowIterator()->seek(2)->current(); + $rowIterator = $worksheet->getRowIterator(); + $firstRow = $rowIterator->current(); + $rowIterator->next(); + if (!$rowIterator->valid()) { + return; + } + $secondRow = $rowIterator->current(); + unset($rowIterator); $this->getColumns($firstRow, $secondRow); if (empty(array_filter($this->columns))) { diff --git a/tests/integration/features/APIv1.feature b/tests/integration/features/APIv1.feature index 333d225ee..77c45be4b 100644 --- a/tests/integration/features/APIv1.feature +++ b/tests/integration/features/APIv1.feature @@ -215,6 +215,54 @@ Feature: APIv1 | Val1 | Val2 | Val3 | 1 | 💙 | Ä | 2024-02-24 | false | | great | news | here | 99 | ⚠️ | Ö | 2016-06-01 | true | + @api1 @import + Scenario: Import xlsx table generated by 365 + Given user "participant1" uploads file "import-from-ms365.xlsx" + And table "Import test" with emoji "👨🏻‍💻" exists for user "participant1" as "base1" + When user imports file "/import-from-ms365.xlsx" into last created table + Then import results have the following data + | found_columns_count | 8 | + | created_columns_count | 8 | + | inserted_rows_count | 2 | + | errors_count | 0 | + Then table has at least following typed columns + | Col1 | text | + | Col2 | text | + | Col3 | text | + | num | number | + | emoji | text | + | special | text | + | date | datetime | + | truth | selection | + Then table contains at least following rows + | Col1 | Col2 | Col3 | num | emoji | special | date | truth | + | Val1 | Val2 | Val3 | 1 | 💙 | Ä | 2024-02-24 | false | + | great | news | here | 99 | ⚠ | Ö | 2016-06-01 | true | + + @api1 @import + Scenario: Import xlsx table generated by LibreOffice + Given user "participant1" uploads file "import-from-libreoffice.xlsx" + And table "Import test" with emoji "👨🏻‍💻" exists for user "participant1" as "base1" + When user imports file "/import-from-libreoffice.xlsx" into last created table + Then import results have the following data + | found_columns_count | 8 | + | created_columns_count | 8 | + | inserted_rows_count | 2 | + | errors_count | 0 | + Then table has at least following typed columns + | Col1 | text | + | Col2 | text | + | Col3 | text | + | num | number | + | emoji | text | + | special | text | + | date | datetime | + | truth | selection | + Then table contains at least following rows + | Col1 | Col2 | Col3 | num | emoji | special | date | truth | + | Val1 | Val2 | Val3 | 1 | 💙 | Ä | 2024-02-24 | false | + | great | news | here | 99 | ⚠ | Ö | 2016-06-01 | true | + @api1 Scenario: Create, edit and delete views Given table "View test" with emoji "👨🏻‍💻" exists for user "participant1" as "view-test" diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 725a93be1..0be0cec0e 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -13,6 +13,7 @@ use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Psr7\Utils; use PHPUnit\Framework\Assert; use PHPUnit\Framework\ExpectationFailedException; use Psr\Http\Message\ResponseInterface; @@ -64,6 +65,8 @@ class FeatureContext implements Context { private array $tableData = []; private array $viewData = []; + private $importColumnData = null; + // use CommandLineTrait; private CollectionManager $collectionManager; @@ -89,6 +92,7 @@ public function setUp() { * @AfterScenario */ public function cleanupUsers() { + $this->importColumnData = null; $this->collectionManager->cleanUp(); foreach ($this->createdUsers as $user) { $this->deleteUser($user); @@ -467,8 +471,21 @@ public function columnsForNodeV2(string $nodeType, string $nodeName, ?TableNode // (((((((((((((((((((((((((((( END API v2 ))))))))))))))))))))))))))))))))))) + /** + * @Given user :user uploads file :file + */ + public function uploadFile(string $user, string $file): void { + $this->setCurrentUser($user); + + $localFilePath = __DIR__ . '/../../resources/' . $file; + + $url = sprintf('%sremote.php/dav/files/%s/%s', $this->baseUrl, $user, $file); + $body = Utils::streamFor(fopen($localFilePath, 'rb')); + $this->sendRequestFullUrl('PUT', $url, $body); + Assert::assertEquals(201, $this->response->getStatusCode()); + } // IMPORT -------------------------- @@ -574,7 +591,7 @@ public function checkRowsExists(TableNode $table): void { $allValuesForColumn[] = $row[$indexForCol]; } foreach ($table->getColumn($key) as $item) { - Assert::assertTrue(in_array($item, $allValuesForColumn)); + Assert::assertTrue(in_array($item, $allValuesForColumn), sprintf('%s not in %s', $item, implode(', ', $allValuesForColumn))); } } } diff --git a/tests/integration/resources/import-from-libreoffice.xlsx b/tests/integration/resources/import-from-libreoffice.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6cdff60f7f2d666e22dc5b2b958c7aff15ef27ca GIT binary patch literal 6498 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 literal 0 HcmV?d00001 diff --git a/tests/integration/resources/import-from-ms365.xlsx b/tests/integration/resources/import-from-ms365.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..896b4276c445755efe28305ce81b702b5a9709fa GIT binary patch literal 13695 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 Date: Thu, 14 Nov 2024 13:54:59 +0100 Subject: [PATCH 03/11] fix(Import): distinguish between date only and date time on xlsx imports Signed-off-by: Arthur Schiwon --- lib/Service/ColumnTypes/DatetimeDateBusiness.php | 4 ++-- lib/Service/ImportService.php | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/Service/ColumnTypes/DatetimeDateBusiness.php b/lib/Service/ColumnTypes/DatetimeDateBusiness.php index 70b5b6922..5cbc8abaf 100644 --- a/lib/Service/ColumnTypes/DatetimeDateBusiness.php +++ b/lib/Service/ColumnTypes/DatetimeDateBusiness.php @@ -17,7 +17,7 @@ class DatetimeDateBusiness extends SuperBusiness implements IColumnTypeBusiness * @return string */ public function parseValue($value, ?Column $column = null): string { - return json_encode($this->isValidDate($value, 'Y-m-d') ? $value : ''); + return json_encode($this->isValidDate((string)$value, 'Y-m-d') ? (string)$value : ''); } /** @@ -26,7 +26,7 @@ public function parseValue($value, ?Column $column = null): string { * @return bool */ public function canBeParsed($value, ?Column $column = null): bool { - return $this->isValidDate($value, 'Y-m-d'); + return $this->isValidDate((string)$value, 'Y-m-d'); } } diff --git a/lib/Service/ImportService.php b/lib/Service/ImportService.php index cf701cdd6..d7c1fbd98 100644 --- a/lib/Service/ImportService.php +++ b/lib/Service/ImportService.php @@ -136,7 +136,7 @@ private function getPreviewData(Worksheet $worksheet): array { $columns[] = [ 'title' => $title, 'type' => $this->rawColumnDataTypes[$colIndex]['type'], - 'subtype' => $this->rawColumnDataTypes[$colIndex]['subtype'], + 'subtype' => $this->rawColumnDataTypes[$colIndex]['subtype'] ?? null, 'numberDecimals' => $this->rawColumnDataTypes[$colIndex]['number_decimals'] ?? 0, 'numberPrefix' => $this->rawColumnDataTypes[$colIndex]['number_prefix'] ?? '', 'numberSuffix' => $this->rawColumnDataTypes[$colIndex]['number_suffix'] ?? '', @@ -367,8 +367,10 @@ private function createRow(Row $row): void { $value = $cell->getValue(); $hasData = $hasData || !empty($value); - if ($column->getType() === 'datetime') { + if ($column->getType() === 'datetime' && $column->getSubtype() === '') { $value = Date::excelToDateTimeObject($value)->format('Y-m-d H:i'); + } elseif ($column->getType() === 'datetime' && $column->getSubtype() === 'date') { + $value = Date::excelToDateTimeObject($value)->format('Y-m-d'); } elseif ($column->getType() === 'number' && $column->getNumberSuffix() === '%') { $value = $value * 100; } elseif ($column->getType() === 'selection' && $column->getSubtype() === 'check') { @@ -477,6 +479,7 @@ private function parseColumnDataType(Cell $cell): array { if (Date::isDateTime($cell) || $originDataType === DataType::TYPE_ISO_DATE) { $dataType = [ 'type' => 'datetime', + 'subtype' => $cell->getCalculateDateTimeType() === Cell::CALCULATE_DATE_TIME_ASIS ? 'date' : '', ]; } elseif ($originDataType === DataType::TYPE_NUMERIC) { if (str_contains($formattedValue, '%')) { From 9430d24473b94fe73d54ebfb8e1789c6e5006e4e Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Thu, 14 Nov 2024 15:21:04 +0100 Subject: [PATCH 04/11] fix(Import): be tolerant to alternative ways of XLSX file content improved handling of LibreOffice-generated XLSX documents Signed-off-by: Arthur Schiwon --- lib/Service/ColumnTypes/SelectionCheckBusiness.php | 4 ++-- lib/Service/ImportService.php | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/Service/ColumnTypes/SelectionCheckBusiness.php b/lib/Service/ColumnTypes/SelectionCheckBusiness.php index 71c1f6fb7..87f57ec76 100644 --- a/lib/Service/ColumnTypes/SelectionCheckBusiness.php +++ b/lib/Service/ColumnTypes/SelectionCheckBusiness.php @@ -10,8 +10,8 @@ use OCA\Tables\Db\Column; class SelectionCheckBusiness extends SuperBusiness implements IColumnTypeBusiness { - public const PATTERN_POSITIVE = ['yes', '1', true, 1, 'true']; - public const PATTERN_NEGATIVE = ['no', '0', false, 0, 'false']; + public const PATTERN_POSITIVE = ['yes', '1', true, 1, 'true', 'TRUE']; + public const PATTERN_NEGATIVE = ['no', '0', false, 0, 'false', 'FALSE']; /** * @param mixed $value diff --git a/lib/Service/ImportService.php b/lib/Service/ImportService.php index d7c1fbd98..faf288754 100644 --- a/lib/Service/ImportService.php +++ b/lib/Service/ImportService.php @@ -447,6 +447,12 @@ private function getColumns(Row $firstRow, Row $secondRow): void { $dataTypes[] = $this->parseColumnDataType($secondRowCellIterator->current()); } else { $this->logger->debug('No cell given or cellValue is empty while loading columns for importing'); + if ($cell->getDataType() === 'null') { + // LibreOffice generated XLSX doc may have more empty columns in the first row. + // Continue without increasing error count. + // Question: What about tables where a column does not have a heading? + continue; + } $this->countErrors++; } $secondRowCellIterator->next(); @@ -523,7 +529,10 @@ private function parseColumnDataType(Cell $cell): array { 'type' => 'number', ]; } - } elseif ($originDataType === DataType::TYPE_BOOL) { + } elseif ($originDataType === DataType::TYPE_BOOL + || ($originDataType === DataType::TYPE_FORMULA + && in_array($formattedValue, ['FALSE', 'TRUE'], true)) + ) { $dataType = [ 'type' => 'selection', 'subtype' => 'check', From 74e1becb8c093e502dc59349e49b3ce63ecfc3e8 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Thu, 14 Nov 2024 15:34:54 +0100 Subject: [PATCH 05/11] fix(Import): accept Y-m-d formatted dates, as from CSV Signed-off-by: Arthur Schiwon --- lib/Service/ImportService.php | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/Service/ImportService.php b/lib/Service/ImportService.php index faf288754..78aa70b4b 100644 --- a/lib/Service/ImportService.php +++ b/lib/Service/ImportService.php @@ -368,9 +368,17 @@ private function createRow(Row $row): void { $value = $cell->getValue(); $hasData = $hasData || !empty($value); if ($column->getType() === 'datetime' && $column->getSubtype() === '') { - $value = Date::excelToDateTimeObject($value)->format('Y-m-d H:i'); + try { + $value = Date::excelToDateTimeObject($value)->format('Y-m-d H:i'); + } catch (\TypeError) { + $value = (new \DateTimeImmutable($value))->format('Y-m-d H:i'); + } } elseif ($column->getType() === 'datetime' && $column->getSubtype() === 'date') { - $value = Date::excelToDateTimeObject($value)->format('Y-m-d'); + try { + $value = Date::excelToDateTimeObject($value)->format('Y-m-d'); + } catch (\TypeError) { + $value = (new \DateTimeImmutable($value))->format('Y-m-d'); + } } elseif ($column->getType() === 'number' && $column->getNumberSuffix() === '%') { $value = $value * 100; } elseif ($column->getType() === 'selection' && $column->getSubtype() === 'check') { @@ -482,7 +490,13 @@ private function parseColumnDataType(Cell $cell): array { 'subtype' => 'line', ]; - if (Date::isDateTime($cell) || $originDataType === DataType::TYPE_ISO_DATE) { + try { + $dateValue = new \DateTimeImmutable($value); + } catch (\Exception $e) { + } + if ((isset($dateValue) && $originDataType === DataType::TYPE_STRING) + || Date::isDateTime($cell) + || $originDataType === DataType::TYPE_ISO_DATE) { $dataType = [ 'type' => 'datetime', 'subtype' => $cell->getCalculateDateTimeType() === Cell::CALCULATE_DATE_TIME_ASIS ? 'date' : '', From 61a76fd0283a7910854e94e75391a48aa89caaa7 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Thu, 14 Nov 2024 22:56:45 +0100 Subject: [PATCH 06/11] tests(Integration): add ods test and consolidate scenarios in an outline Signed-off-by: Arthur Schiwon --- tests/integration/features/APIv1.feature | 63 +++--------------- .../resources/import-from-libreoffice.csv | 3 + .../resources/import-from-libreoffice.ods | Bin 0 -> 14457 bytes 3 files changed, 12 insertions(+), 54 deletions(-) create mode 100644 tests/integration/resources/import-from-libreoffice.csv create mode 100644 tests/integration/resources/import-from-libreoffice.ods diff --git a/tests/integration/features/APIv1.feature b/tests/integration/features/APIv1.feature index 77c45be4b..23b6ee828 100644 --- a/tests/integration/features/APIv1.feature +++ b/tests/integration/features/APIv1.feature @@ -187,39 +187,11 @@ Feature: APIv1 Then user deletes last created row Then user "participant1" deletes table with keyword "Rows check" - - @api1 @import - Scenario: Import csv table - Given file "/import.csv" exists for user "participant1" with following data - | Col1 | Col2 | Col3 | num | emoji | special | date | truth | - | Val1 | Val2 | Val3 | 1 | 💙 | Ä | 2024-02-24 | false | - | great | news | here | 99 | ⚠️ | Ö | 2016-06-01 | true | - Given table "Import test" with emoji "👨🏻‍💻" exists for user "participant1" as "base1" - When user imports file "/import.csv" into last created table - Then import results have the following data - | found_columns_count | 8 | - | created_columns_count | 8 | - | inserted_rows_count | 2 | - | errors_count | 0 | - Then table has at least following typed columns - | Col1 | text | - | Col2 | text | - | Col3 | text | - | num | number | - | emoji | text | - | special | text | - | date | datetime | - | truth | selection | - Then table contains at least following rows - | Col1 | Col2 | Col3 | num | emoji | special | date | truth | - | Val1 | Val2 | Val3 | 1 | 💙 | Ä | 2024-02-24 | false | - | great | news | here | 99 | ⚠️ | Ö | 2016-06-01 | true | - @api1 @import - Scenario: Import xlsx table generated by 365 - Given user "participant1" uploads file "import-from-ms365.xlsx" + Scenario Outline: Import a document file + Given user "participant1" uploads file "" And table "Import test" with emoji "👨🏻‍💻" exists for user "participant1" as "base1" - When user imports file "/import-from-ms365.xlsx" into last created table + When user imports file "/" into last created table Then import results have the following data | found_columns_count | 8 | | created_columns_count | 8 | @@ -239,29 +211,12 @@ Feature: APIv1 | Val1 | Val2 | Val3 | 1 | 💙 | Ä | 2024-02-24 | false | | great | news | here | 99 | ⚠ | Ö | 2016-06-01 | true | - @api1 @import - Scenario: Import xlsx table generated by LibreOffice - Given user "participant1" uploads file "import-from-libreoffice.xlsx" - And table "Import test" with emoji "👨🏻‍💻" exists for user "participant1" as "base1" - When user imports file "/import-from-libreoffice.xlsx" into last created table - Then import results have the following data - | found_columns_count | 8 | - | created_columns_count | 8 | - | inserted_rows_count | 2 | - | errors_count | 0 | - Then table has at least following typed columns - | Col1 | text | - | Col2 | text | - | Col3 | text | - | num | number | - | emoji | text | - | special | text | - | date | datetime | - | truth | selection | - Then table contains at least following rows - | Col1 | Col2 | Col3 | num | emoji | special | date | truth | - | Val1 | Val2 | Val3 | 1 | 💙 | Ä | 2024-02-24 | false | - | great | news | here | 99 | ⚠ | Ö | 2016-06-01 | true | + Examples: + | importfile | + | import-from-libreoffice.ods | + | import-from-libreoffice.xlsx | + | import-from-ms365.xlsx | + | import-from-libreoffice.csv | @api1 Scenario: Create, edit and delete views diff --git a/tests/integration/resources/import-from-libreoffice.csv b/tests/integration/resources/import-from-libreoffice.csv new file mode 100644 index 000000000..d27e813d1 --- /dev/null +++ b/tests/integration/resources/import-from-libreoffice.csv @@ -0,0 +1,3 @@ +Col1,Col2,Col3,num,emoji,special,date,truth +Val1,Val2,Val3,1,💙,Ä,2024-02-24,false +great,news,here,99,⚠,Ö,2016-06-01,true diff --git a/tests/integration/resources/import-from-libreoffice.ods b/tests/integration/resources/import-from-libreoffice.ods new file mode 100644 index 0000000000000000000000000000000000000000..5d539f6e6f511248bee5a88bb135735b173a5016 GIT binary patch literal 14457 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 Date: Thu, 14 Nov 2024 23:16:24 +0100 Subject: [PATCH 07/11] test(reuse): add license files to office test docs Signed-off-by: Arthur Schiwon --- .../integration/resources/import-from-libreoffice.csv.license | 3 +++ .../integration/resources/import-from-libreoffice.ods.license | 3 +++ .../integration/resources/import-from-libreoffice.xlsx.license | 3 +++ tests/integration/resources/import-from-ms365.xlsx.license | 3 +++ 4 files changed, 12 insertions(+) create mode 100644 tests/integration/resources/import-from-libreoffice.csv.license create mode 100644 tests/integration/resources/import-from-libreoffice.ods.license create mode 100644 tests/integration/resources/import-from-libreoffice.xlsx.license create mode 100644 tests/integration/resources/import-from-ms365.xlsx.license diff --git a/tests/integration/resources/import-from-libreoffice.csv.license b/tests/integration/resources/import-from-libreoffice.csv.license new file mode 100644 index 000000000..21db92f72 --- /dev/null +++ b/tests/integration/resources/import-from-libreoffice.csv.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later + diff --git a/tests/integration/resources/import-from-libreoffice.ods.license b/tests/integration/resources/import-from-libreoffice.ods.license new file mode 100644 index 000000000..21db92f72 --- /dev/null +++ b/tests/integration/resources/import-from-libreoffice.ods.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later + diff --git a/tests/integration/resources/import-from-libreoffice.xlsx.license b/tests/integration/resources/import-from-libreoffice.xlsx.license new file mode 100644 index 000000000..21db92f72 --- /dev/null +++ b/tests/integration/resources/import-from-libreoffice.xlsx.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later + diff --git a/tests/integration/resources/import-from-ms365.xlsx.license b/tests/integration/resources/import-from-ms365.xlsx.license new file mode 100644 index 000000000..21db92f72 --- /dev/null +++ b/tests/integration/resources/import-from-ms365.xlsx.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later + From 0e9fd3a69dfda25005fe0ae223b21a54ef00ad96 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 15 Nov 2024 16:42:03 +0100 Subject: [PATCH 08/11] fix(Import): use correct int index of literal one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … and get date value with fault tolerance Signed-off-by: Arthur Schiwon --- lib/Service/ImportService.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/Service/ImportService.php b/lib/Service/ImportService.php index 78aa70b4b..6e9ddade2 100644 --- a/lib/Service/ImportService.php +++ b/lib/Service/ImportService.php @@ -154,13 +154,18 @@ private function getPreviewData(Worksheet $worksheet): array { $cellIterator = $row->getCellIterator(); $cellIterator->setIterateOnlyExistingCells(false); - foreach ($cellIterator as $cellIndex => $cell) { + foreach ($cellIterator as $cell) { $value = $cell->getValue(); - $colIndex = (int) $cellIndex; + // $cellIterator`s index is based on 1, not 0. + $colIndex = $cellIterator->getCurrentColumnIndex() - 1; $column = $this->columns[$colIndex]; if (($column && $column->getType() === 'datetime') || (is_array($columns[$colIndex]) && $columns[$colIndex]['type'] === 'datetime')) { - $value = Date::excelToDateTimeObject($value)->format('Y-m-d H:i'); + try { + $value = Date::excelToDateTimeObject($value)->format('Y-m-d H:i'); + } catch (\TypeError) { + $value = (new \DateTimeImmutable($value))->format('Y-m-d H:i'); + } } elseif (($column && $column->getType() === 'number' && $column->getNumberSuffix() === '%') || (is_array($columns[$colIndex]) && $columns[$colIndex]['type'] === 'number' && $columns[$colIndex]['numberSuffix'] === '%')) { $value = $value * 100; From 044d62d5c26486aabd001a373d2d179faadb9f2f Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 15 Nov 2024 19:34:00 +0100 Subject: [PATCH 09/11] fix(Import): reliable detection of intented datetime type Signed-off-by: Arthur Schiwon --- .../ColumnTypes/DatetimeTimeBusiness.php | 4 +- lib/Service/ImportService.php | 55 +++++++++++++++--- tests/integration/features/APIv1.feature | 14 ++--- .../resources/import-from-libreoffice.csv | 6 +- .../resources/import-from-libreoffice.ods | Bin 14457 -> 17166 bytes .../resources/import-from-libreoffice.xlsx | Bin 6498 -> 6619 bytes .../resources/import-from-ms365.xlsx | Bin 13695 -> 16234 bytes 7 files changed, 59 insertions(+), 20 deletions(-) diff --git a/lib/Service/ColumnTypes/DatetimeTimeBusiness.php b/lib/Service/ColumnTypes/DatetimeTimeBusiness.php index 6d06cc39e..e0d41e4b5 100644 --- a/lib/Service/ColumnTypes/DatetimeTimeBusiness.php +++ b/lib/Service/ColumnTypes/DatetimeTimeBusiness.php @@ -17,7 +17,7 @@ class DatetimeTimeBusiness extends SuperBusiness implements IColumnTypeBusiness * @return string */ public function parseValue($value, ?Column $column = null): string { - return json_encode($this->isValidDate($value, 'H:i') ? $value : ''); + return json_encode($this->isValidDate((string)$value, 'H:i') ? $value : ''); } /** @@ -26,7 +26,7 @@ public function parseValue($value, ?Column $column = null): string { * @return bool */ public function canBeParsed($value, ?Column $column = null): bool { - return $this->isValidDate($value, 'H:i'); + return $this->isValidDate((string)$value, 'H:i'); } } diff --git a/lib/Service/ImportService.php b/lib/Service/ImportService.php index 6e9ddade2..1403ab233 100644 --- a/lib/Service/ImportService.php +++ b/lib/Service/ImportService.php @@ -161,10 +161,18 @@ private function getPreviewData(Worksheet $worksheet): array { $column = $this->columns[$colIndex]; if (($column && $column->getType() === 'datetime') || (is_array($columns[$colIndex]) && $columns[$colIndex]['type'] === 'datetime')) { + if (isset($columns[$colIndex]['subtype']) && $columns[$colIndex]['subtype'] === 'date') { + $format = 'Y-m-d'; + } elseif (isset($columns[$colIndex]['subtype']) && $columns[$colIndex]['subtype'] === 'time') { + $format = 'H:i'; + } else { + $format = 'Y-m-d H:i'; + } + try { - $value = Date::excelToDateTimeObject($value)->format('Y-m-d H:i'); + $value = Date::excelToDateTimeObject($value)->format($format); } catch (\TypeError) { - $value = (new \DateTimeImmutable($value))->format('Y-m-d H:i'); + $value = (new \DateTimeImmutable($value))->format($format); } } elseif (($column && $column->getType() === 'number' && $column->getNumberSuffix() === '%') || (is_array($columns[$colIndex]) && $columns[$colIndex]['type'] === 'number' && $columns[$colIndex]['numberSuffix'] === '%')) { @@ -372,11 +380,19 @@ private function createRow(Row $row): void { $value = $cell->getValue(); $hasData = $hasData || !empty($value); - if ($column->getType() === 'datetime' && $column->getSubtype() === '') { + + if ($column->getType() === 'datetime') { + if ($column->getType() === 'datetime' && $column->getSubtype() === 'date') { + $format = 'Y-m-d'; + } elseif ($column->getType() === 'datetime' && $column->getSubtype() === 'time') { + $format = 'H:i'; + } else { + $format = 'Y-m-d H:i'; + } try { - $value = Date::excelToDateTimeObject($value)->format('Y-m-d H:i'); + $value = Date::excelToDateTimeObject($value)->format($format); } catch (\TypeError) { - $value = (new \DateTimeImmutable($value))->format('Y-m-d H:i'); + $value = (new \DateTimeImmutable($value))->format($format); } } elseif ($column->getType() === 'datetime' && $column->getSubtype() === 'date') { try { @@ -384,6 +400,12 @@ private function createRow(Row $row): void { } catch (\TypeError) { $value = (new \DateTimeImmutable($value))->format('Y-m-d'); } + } elseif ($column->getType() === 'datetime' && $column->getSubtype() === 'time') { + try { + $value = Date::excelToDateTimeObject($value)->format('H:i'); + } catch (\TypeError) { + $value = (new \DateTimeImmutable($value))->format('H:i'); + } } elseif ($column->getType() === 'number' && $column->getNumberSuffix() === '%') { $value = $value * 100; } elseif ($column->getType() === 'selection' && $column->getSubtype() === 'check') { @@ -496,15 +518,32 @@ private function parseColumnDataType(Cell $cell): array { ]; try { + if ($value === false) { + throw new \Exception('We do not accept `false` here'); + } $dateValue = new \DateTimeImmutable($value); - } catch (\Exception $e) { + } catch (\Exception) { } - if ((isset($dateValue) && $originDataType === DataType::TYPE_STRING) + + if (isset($dateValue) || Date::isDateTime($cell) || $originDataType === DataType::TYPE_ISO_DATE) { + // the formatted value stems from the office document and shows the original user intent + $dateAnalysis = date_parse($formattedValue); + $containsDate = $dateAnalysis['year'] !== false || $dateAnalysis['month'] !== false || $dateAnalysis['day'] !== false; + $containsTime = $dateAnalysis['hour'] !== false || $dateAnalysis['minute'] !== false || $dateAnalysis['second'] !== false; + + if ($containsDate && !$containsTime) { + $subType = 'date'; + } elseif (!$containsDate && $containsTime) { + $subType = 'time'; + } else { + $subType = ''; + } + $dataType = [ 'type' => 'datetime', - 'subtype' => $cell->getCalculateDateTimeType() === Cell::CALCULATE_DATE_TIME_ASIS ? 'date' : '', + 'subtype' => $subType, ]; } elseif ($originDataType === DataType::TYPE_NUMERIC) { if (str_contains($formattedValue, '%')) { diff --git a/tests/integration/features/APIv1.feature b/tests/integration/features/APIv1.feature index 23b6ee828..a1e2985df 100644 --- a/tests/integration/features/APIv1.feature +++ b/tests/integration/features/APIv1.feature @@ -193,10 +193,10 @@ Feature: APIv1 And table "Import test" with emoji "👨🏻‍💻" exists for user "participant1" as "base1" When user imports file "/" into last created table Then import results have the following data - | found_columns_count | 8 | - | created_columns_count | 8 | - | inserted_rows_count | 2 | - | errors_count | 0 | + | found_columns_count | 10 | + | created_columns_count | 10 | + | inserted_rows_count | 2 | + | errors_count | 0 | Then table has at least following typed columns | Col1 | text | | Col2 | text | @@ -207,9 +207,9 @@ Feature: APIv1 | date | datetime | | truth | selection | Then table contains at least following rows - | Col1 | Col2 | Col3 | num | emoji | special | date | truth | - | Val1 | Val2 | Val3 | 1 | 💙 | Ä | 2024-02-24 | false | - | great | news | here | 99 | ⚠ | Ö | 2016-06-01 | true | + | Date and Time | Col1 | Col2 | Col3 | num | emoji | special | date | truth | time | + | 2022-02-20 08:42 | Val1 | Val2 | Val3 | 1 | 💙 | Ä | 2024-02-24 | false | 18:48 | + | 2016-06-01 13:37 | great | news | here | 99 | ⚠ | Ö | 2016-06-01 | true | 01:23 | Examples: | importfile | diff --git a/tests/integration/resources/import-from-libreoffice.csv b/tests/integration/resources/import-from-libreoffice.csv index d27e813d1..13d758cf7 100644 --- a/tests/integration/resources/import-from-libreoffice.csv +++ b/tests/integration/resources/import-from-libreoffice.csv @@ -1,3 +1,3 @@ -Col1,Col2,Col3,num,emoji,special,date,truth -Val1,Val2,Val3,1,💙,Ä,2024-02-24,false -great,news,here,99,⚠,Ö,2016-06-01,true +Date and Time,Col1,Col2,Col3,num,emoji,special,date,truth,time +2022-02-20 08:42,Val1,Val2,Val3,1,💙,Ä,2024-02-24,false,18:48 +2016-06-01 13:37,great,news,here,99,⚠,Ö,2016-06-01,true,01:23 diff --git a/tests/integration/resources/import-from-libreoffice.ods b/tests/integration/resources/import-from-libreoffice.ods index 5d539f6e6f511248bee5a88bb135735b173a5016..617a945086ad2a9589f216e9e891c028c7f93ada 100644 GIT binary patch literal 17166 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 literal 14457 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 Date: Tue, 26 Nov 2024 13:09:35 +0100 Subject: [PATCH 10/11] fix(Import): increase error again, when there was a gap in column headers Signed-off-by: Arthur Schiwon --- lib/Service/ImportService.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/Service/ImportService.php b/lib/Service/ImportService.php index 1403ab233..09813aa35 100644 --- a/lib/Service/ImportService.php +++ b/lib/Service/ImportService.php @@ -457,6 +457,8 @@ private function getColumns(Row $firstRow, Row $secondRow): void { $index = 0; $countMatchingColumnsFromConfig = 0; $countCreatedColumnsFromConfig = 0; + $lastCellWasEmpty = false; + $hasGapInTitles = false; foreach ($cellIterator as $cell) { if ($cell && $cell->getValue() !== null && $cell->getValue() !== '') { $title = $cell->getValue(); @@ -480,12 +482,16 @@ private function getColumns(Row $firstRow, Row $secondRow): void { // Convert data type to our data type $dataTypes[] = $this->parseColumnDataType($secondRowCellIterator->current()); + if ($lastCellWasEmpty) { + $hasGapInTitles = true; + } + $lastCellWasEmpty = false; } else { $this->logger->debug('No cell given or cellValue is empty while loading columns for importing'); if ($cell->getDataType() === 'null') { // LibreOffice generated XLSX doc may have more empty columns in the first row. - // Continue without increasing error count. - // Question: What about tables where a column does not have a heading? + // Continue without increasing error count, but leave a marker to detect gaps in titles. + $lastCellWasEmpty = true; continue; } $this->countErrors++; @@ -494,6 +500,11 @@ private function getColumns(Row $firstRow, Row $secondRow): void { $index++; } + if ($hasGapInTitles) { + $this->logger->info('Imported table is having a gap in column titles'); + $this->countErrors++; + } + $this->rawColumnTitles = $titles; $this->rawColumnDataTypes = $dataTypes; From 36179dc13eafa89626e287e9cede138f79b9e835 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Tue, 26 Nov 2024 22:26:42 +0100 Subject: [PATCH 11/11] fix(UI): more specific import info Signed-off-by: Arthur Schiwon --- src/modules/modals/Import.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/modals/Import.vue b/src/modules/modals/Import.vue index e6b663630..ec71c91f0 100644 --- a/src/modules/modals/Import.vue +++ b/src/modules/modals/Import.vue @@ -41,7 +41,7 @@

{{ t('tables', 'Supported formats: xlsx, xls, csv, html, xml') }}
- {{ t('tables', 'First row of the file must contain column headings.') }} + {{ t('tables', 'First row of the file must contain column headings without gaps.') }}