From 7b9e609a795ec997e505da0c818f683cec0b5d08 Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Thu, 5 Nov 2020 22:00:10 +0100 Subject: [PATCH 01/25] Enable test execution by calling "composer test" --- composer.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/composer.json b/composer.json index 1edb3500..4a15044d 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,12 @@ "source": "https://github.com/abrain/einsatzverwaltung", "docs": "https://einsatzverwaltung.abrain.de/dokumentation/" }, + "scripts": { + "test": [ + "Composer\\Config::disableProcessTimeout", + "phpunit" + ] + }, "config": { "platform": { "php": "7.1" From 4e8708ed179e11a2961bf2361f7ac8a2339e35fc Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Fri, 6 Nov 2020 00:18:33 +0100 Subject: [PATCH 02/25] Switch to a more general approach to pages in the admin area --- src/Admin/Initializer.php | 6 +-- src/AdminPage.php | 96 +++++++++++++++++++++++++++++++++++++++ src/Import/Page.php | 24 ++++++++++ 3 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 src/AdminPage.php create mode 100644 src/Import/Page.php diff --git a/src/Admin/Initializer.php b/src/Admin/Initializer.php index ee4ee0b3..f11a268f 100644 --- a/src/Admin/Initializer.php +++ b/src/Admin/Initializer.php @@ -6,7 +6,7 @@ use abrain\Einsatzverwaltung\CustomFieldsRepository; use abrain\Einsatzverwaltung\Data; use abrain\Einsatzverwaltung\Export\Tool as ExportTool; -use abrain\Einsatzverwaltung\Import\Tool as ImportTool; +use abrain\Einsatzverwaltung\Import\Page as ImportPage; use abrain\Einsatzverwaltung\Options; use abrain\Einsatzverwaltung\PermalinkController; use abrain\Einsatzverwaltung\Settings\MainPage; @@ -65,8 +65,8 @@ public function __construct(Data $data, Options $options, Utilities $utilities, add_action('admin_menu', array($mainPage, 'addToSettingsMenu')); add_action('admin_init', array($mainPage, 'registerSettings')); - $importTool = new ImportTool($utilities, $data); - add_action('admin_menu', array($importTool, 'addToolToMenu')); + $importPage = new ImportPage(); + add_action('admin_menu', array($importPage, 'registerAsToolPage')); $exportTool = new ExportTool(); add_action('admin_menu', array($exportTool, 'addToolToMenu')); diff --git a/src/AdminPage.php b/src/AdminPage.php new file mode 100644 index 00000000..a427d38d --- /dev/null +++ b/src/AdminPage.php @@ -0,0 +1,96 @@ +pageTitle = $pageTitle; + } + + abstract protected function echoPageContent(); + + /** + * Prints an error message. + * + * @param string $message The message text. + */ + public function printError(string $message) + { + printf('

%s

', esc_html($message)); + } + + /** + * Prints an informational message. + * + * @param string $message The message text. + */ + public function printInfo(string $message) + { + printf('

%s

', esc_html($message)); + } + + /** + * Prints a success message. + * + * @param string $message The message text. + */ + public function printSuccess(string $message) + { + printf('

%s

', esc_html($message)); + } + + /** + * Prints a warning message. + * + * @param string $message The message text. + */ + public function printWarning(string $message) + { + printf('

%s

', esc_html($message)); + } + + /** + * Registers this page under the Tools menu. + * + * @param string $menuSlug + */ + public function registerAsToolPage(string $menuSlug) + { + add_management_page( + $this->pageTitle, + esc_html($this->pageTitle), + 'manage_options', + $menuSlug, + array($this, 'render') + ); + } + + /** + * Generates the output of the page. + */ + public function render() + { + echo '
'; + printf("

%s

", esc_html($this->pageTitle)); + $this->echoPageContent(); + echo '
'; + } +} diff --git a/src/Import/Page.php b/src/Import/Page.php new file mode 100644 index 00000000..4b0fdc82 --- /dev/null +++ b/src/Import/Page.php @@ -0,0 +1,24 @@ +Content

'; + } +} From ad7d4abc4b4d7d9fd041037b03882b55bd2b4992 Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Fri, 6 Nov 2020 13:17:02 +0100 Subject: [PATCH 03/25] Use the predefined mocks for translation functions --- tests/unit/UnitTestCase.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/UnitTestCase.php b/tests/unit/UnitTestCase.php index 2ef322b8..0b4d1071 100644 --- a/tests/unit/UnitTestCase.php +++ b/tests/unit/UnitTestCase.php @@ -17,12 +17,13 @@ protected function setUp() { parent::setUp(); Monkey\setUp(); - Monkey\Functions\when('__')->returnArg(1); - Monkey\Functions\when('_e')->echoArg(1); + Monkey\Functions\stubTranslationFunctions(); + Monkey\Functions\stubEscapeFunctions(); + + // Plurals are not yet covered by Brain Monkey Monkey\Functions\when('_n')->alias(function ($single, $plural, $number) { return $number === 1 ? $single : $plural; }); - Monkey\Functions\when('esc_url')->returnArg(1); } protected function tearDown() From 755df8e3c853ff3b6fb896af58717f086dce39f0 Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Fri, 6 Nov 2020 16:54:26 +0100 Subject: [PATCH 04/25] Define the menu slug on the class itself --- src/AdminPage.php | 11 +++++++---- src/Import/Page.php | 8 ++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/AdminPage.php b/src/AdminPage.php index a427d38d..95635f0b 100644 --- a/src/AdminPage.php +++ b/src/AdminPage.php @@ -27,6 +27,11 @@ public function __construct(string $pageTitle) abstract protected function echoPageContent(); + /** + * @return string + */ + abstract protected function getMenuSlug(); + /** * Prints an error message. * @@ -69,16 +74,14 @@ public function printWarning(string $message) /** * Registers this page under the Tools menu. - * - * @param string $menuSlug */ - public function registerAsToolPage(string $menuSlug) + public function registerAsToolPage() { add_management_page( $this->pageTitle, esc_html($this->pageTitle), 'manage_options', - $menuSlug, + $this->getMenuSlug(), array($this, 'render') ); } diff --git a/src/Import/Page.php b/src/Import/Page.php index 4b0fdc82..d61c4083 100644 --- a/src/Import/Page.php +++ b/src/Import/Page.php @@ -17,6 +17,14 @@ public function __construct() parent::__construct('Einsatzberichte importieren'); } + /** + * @inheritDoc + */ + protected function getMenuSlug() + { + return 'einsatzvw-tool-import'; + } + protected function echoPageContent() { echo '

Content

'; From 2e986a8314aeab91425ce052c984ddc6855678ba Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Sat, 7 Nov 2020 00:05:40 +0100 Subject: [PATCH 05/25] Test for wp-einsatz import source --- src/Exceptions/ImportCheckException.php | 13 ++ src/Import/Sources/AbstractSource.php | 66 +++++-- src/Import/Sources/Csv.php | 70 ++------ src/Import/Sources/WpEinsatz.php | 94 ++++------ tests/unit/Import/Sources/WpEinsatzTest.php | 181 ++++++++++++++++++++ tests/unit/UnitTestCase.php | 6 + tests/unit/bootstrap.php | 1 + tests/unit/constants.php | 4 + 8 files changed, 303 insertions(+), 132 deletions(-) create mode 100644 src/Exceptions/ImportCheckException.php create mode 100644 tests/unit/Import/Sources/WpEinsatzTest.php create mode 100644 tests/unit/constants.php diff --git a/src/Exceptions/ImportCheckException.php b/src/Exceptions/ImportCheckException.php new file mode 100644 index 00000000..1af3908e --- /dev/null +++ b/src/Exceptions/ImportCheckException.php @@ -0,0 +1,13 @@ +description; + } /** - * @param $action + * @param string $action * @return string */ - public function getActionAttribute($action) + public function getActionAttribute(string $action) { return $this->getIdentifier() . ':' . $action; } @@ -79,7 +98,7 @@ public function getActionAttribute($action) * * @return array|bool Das Array der Action oder false, wenn es keines für $slug gibt */ - public function getAction($slug) + public function getAction(string $slug) { if (empty($slug)) { return false; @@ -114,6 +133,7 @@ abstract public function getDateFormat(); * Felder abgefragt. * * @return array + * @throws ImportException */ abstract public function getEntries($fields); @@ -141,15 +161,19 @@ public function getFirstAction() * * @return string Eindeutiger Bezeichner der Importquelle */ - abstract public function getIdentifier(); + public function getIdentifier() + { + return $this->identifier; + } /** * Gibt den Wert für das name-Attribut eines Formularelements zurück * * @param string $field Bezeichner des Felds + * * @return string Eindeutiger Name bestehend aus Bezeichnern der Importquelle und des Felds */ - public function getInputName($field) + public function getInputName(string $field) { $fieldId = array_search($field, $this->getFields()); return $this->getIdentifier() . '-field' . $fieldId; @@ -160,6 +184,7 @@ public function getInputName($field) * @param array $ownFields Felder der Einsatzverwaltung * * @return array + * @throws ImportException */ public function getMapping($sourceFields, $ownFields) { @@ -172,7 +197,7 @@ public function getMapping($sourceFields, $ownFields) if (array_key_exists($ownField, $ownFields)) { $mapping[$sourceField] = $ownField; } else { - $this->utilities->printWarning("Unbekanntes Feld: $ownField"); + throw new ImportException(sprintf(__('Unknown field: %s', 'einsatzverwaltung'), $ownField)); } } } @@ -188,7 +213,10 @@ public function getMapping($sourceFields, $ownFields) * * @return string Name der Importquelle */ - abstract public function getName(); + public function getName() + { + return $this->name; + } /** * Gibt die nächste Action der Importquelle zurück @@ -197,7 +225,7 @@ abstract public function getName(); * * @return array|bool Ein Array, das die nächste Action beschreibt, oder false, wenn es keine weitere gibt */ - public function getNextAction($currentAction) + public function getNextAction(array $currentAction) { if (empty($this->actionOrder)) { return false; diff --git a/src/Import/Sources/Csv.php b/src/Import/Sources/Csv.php index c1ee6434..1ba51c2f 100644 --- a/src/Import/Sources/Csv.php +++ b/src/Import/Sources/Csv.php @@ -1,17 +1,15 @@ utilities = $utilities; + $this->description = 'Importiert Einsatzberichte aus einer CSV-Datei.'; + $this->identifier = 'evw_csv'; + $this->name = 'CSV'; $this->actionOrder = array( array( @@ -51,7 +50,7 @@ public function __construct($utilities) } /** - * @return boolean True, wenn Voraussetzungen stimmen, ansonsten false + * @inheritDoc */ public function checkPreconditions() { @@ -64,33 +63,28 @@ public function checkPreconditions() $attachmentId = $this->args['csv_file_id']; if (empty($attachmentId)) { - $this->utilities->printError('Keine Datei ausgewählt'); - return false; + throw new ImportCheckException('Keine Datei ausgewählt'); } if (!is_numeric($attachmentId)) { - $this->utilities->printError('Attachment ID ist keine Zahl'); - return false; + throw new ImportCheckException('Attachment ID ist keine Zahl'); } $csvFilePath = get_attached_file($attachmentId); if (empty($csvFilePath)) { - $this->utilities->printError(sprintf('Konnte Attachment mit ID %d nicht finden', $attachmentId)); - return false; + throw new ImportCheckException(sprintf('Konnte Attachment mit ID %d nicht finden', $attachmentId)); } $this->csvFilePath = $csvFilePath; if (!file_exists($csvFilePath)) { - $this->utilities->printError('Datei existiert nicht'); - return false; + throw new ImportCheckException('Datei existiert nicht'); } - $readFile = $this->readFile(0); - if (false === $readFile) { - return false; + try { + $this->readFile(0); + } catch (Exception $e) { + throw new ImportCheckException($e->getMessage()); } - - return true; } /** @@ -133,16 +127,6 @@ public function getDateFormat() return $this->args['import_date_format']; } - /** - * Gibt die Beschreibung der Importquelle zurück - * - * @return string Beschreibung der Importquelle - */ - public function getDescription() - { - return 'Importiert Einsatzberichte aus einer CSV-Datei.'; - } - /** * Gibt die Einsatzberichte der Importquelle zurück * @@ -194,26 +178,6 @@ public function getFields() return $fields[0]; } - /** - * Gibt den eindeutigen Bezeichner der Importquelle zurück - * - * @return string Eindeutiger Bezeichner der Importquelle - */ - public function getIdentifier() - { - return 'evw_csv'; - } - - /** - * Gibt den Namen der Importquelle zurück - * - * @return string Name der Importquelle - */ - public function getName() - { - return 'CSV'; - } - /** * @return string */ @@ -232,6 +196,7 @@ public function getTimeFormat() * @param array $requestedFields * * @return array|bool + * @throws Exception */ private function readFile($numLines = null, $requestedFields = array()) { @@ -245,8 +210,7 @@ private function readFile($numLines = null, $requestedFields = array()) $handle = fopen($this->csvFilePath, 'r'); if (empty($handle)) { - $this->utilities->printError('Konnte Datei nicht öffnen'); - return false; + throw new Exception('Konnte Datei nicht öffnen'); } if ($numLines === 0) { diff --git a/src/Import/Sources/WpEinsatz.php b/src/Import/Sources/WpEinsatz.php index 738e16ee..f02089eb 100644 --- a/src/Import/Sources/WpEinsatz.php +++ b/src/Import/Sources/WpEinsatz.php @@ -1,8 +1,13 @@ utilities = $utilities; - - /** @var wpdb $wpdb */ global $wpdb; - $this->tablename = $wpdb->prefix . 'einsaetze'; + $this->tablename = "{$wpdb->prefix}einsaetze"; + + $this->description = 'Importiert Einsätze aus dem WordPress-Plugin wp-einsatz.'; + $this->identifier = 'evw_wpe'; + $this->name = 'wp-einsatz'; $this->autoMatchFields = array( 'Datum' => 'post_date' @@ -53,14 +53,23 @@ public function __construct($utilities) */ public function checkPreconditions() { - global $wpdb; /** @var wpdb $wpdb */ + global $wpdb; if ($wpdb->get_var("SHOW TABLES LIKE '$this->tablename'") != $this->tablename) { - $this->utilities->printError('Die Tabelle, in der wp-einsatz seine Daten speichert, konnte nicht gefunden werden.'); - return false; + throw new ImportCheckException(__('Database table of wp-einsatz does not exist', 'einsatzverwaltung')); } - $this->utilities->printSuccess('Die Tabelle, in der wp-einsatz seine Daten speichert, wurde gefunden.'); - return true; + $fields = $this->getFields(); + foreach ($fields as $field) { + if (strpbrk($field, 'äöüÄÖÜß/#')) { + $this->problematicFields[] = $field; + } + } + if (!empty($this->problematicFields)) { + throw new ImportCheckException(sprintf( + __('One or more fields have a special character in their name. This can become a problem during the import. Please rename the following fields in the settings of wp-einsatz: %s', 'einsatzverwaltung'), + esc_html(join(', ', $this->problematicFields)) + )); + } } /** @@ -74,45 +83,20 @@ public function getDateFormat() /** * @inheritDoc */ - public function getDescription() + public function getEntries($fields = null) { - return 'Importiert Einsätze aus dem WordPress-Plugin wp-einsatz.'; - } - - /** - * @inheritDoc - */ - public function getEntries($fields) - { - global $wpdb; /** @var wpdb $wpdb */ + global $wpdb; $queryFields = (null === $fields ? '*' : implode(array_merge(array('ID'), $fields), ',')); - $query = sprintf('SELECT %s FROM %s ORDER BY Datum', $queryFields, $this->tablename); + $query = sprintf('SELECT %s FROM \'%s\' ORDER BY Datum', $queryFields, $this->tablename); $entries = $wpdb->get_results($query, ARRAY_A); if ($entries === null) { - $this->utilities->printError('Dieser Fehler sollte nicht auftreten, da hat der Entwickler Mist gebaut...'); - return false; + throw new ImportException('Dieser Fehler sollte nicht auftreten, da hat der Entwickler Mist gebaut...'); } return $entries; } - /** - * @inheritDoc - */ - public function getIdentifier() - { - return 'evw_wpe'; - } - - /** - * @inheritDoc - */ - public function getName() - { - return 'wp-einsatz'; - } - /** * @return string */ @@ -133,10 +117,10 @@ public function getFields() return $this->cachedFields; } - global $wpdb; /** @var wpdb $wpdb */ + global $wpdb; $fields = array(); - foreach ($wpdb->get_col("DESC " . $this->tablename, 0) as $columnName) { + foreach ($wpdb->get_col("DESCRIBE '$this->tablename'", 0) as $columnName) { // Unwichtiges ignorieren if ($columnName == 'ID' || $columnName == 'Nr_Jahr' || $columnName == 'Nr_Monat') { continue; @@ -145,16 +129,6 @@ public function getFields() $fields[] = $columnName; } - foreach ($fields as $field) { - if (strpbrk($field, 'äöüÄÖÜß/#')) { - $this->utilities->printWarning(sprintf( - 'Feldname %s enthält Zeichen (z.B. Umlaute oder Sonderzeichen), die beim Import zu Problemen führen.
Bitte das Feld in den Einstellungen von wp-einsatz umbenennen, wenn Sie es importieren wollen.', - $field - )); - $this->problematicFields[] = $field; - } - } - $this->cachedFields = $fields; return $fields; diff --git a/tests/unit/Import/Sources/WpEinsatzTest.php b/tests/unit/Import/Sources/WpEinsatzTest.php new file mode 100644 index 00000000..a3a69930 --- /dev/null +++ b/tests/unit/Import/Sources/WpEinsatzTest.php @@ -0,0 +1,181 @@ +getIdentifier(); + $this->assertIsString($identifier); + $this->assertNotEmpty($identifier); + } + + public function testHasAName() + { + $source = new WpEinsatz(); + $name = $source->getName(); + $this->assertIsString($name); + $this->assertNotEmpty($name); + } + + public function testHasADescription() + { + $source = new WpEinsatz(); + $description = $source->getDescription(); + $this->assertIsString($description); + $this->assertNotEmpty($description); + } + + public function testCheckShouldFailWhenTableDoesNotExist() + { + /** @var Mockery\Mock $wpdb */ + global $wpdb; + $wpdb->expects()->get_var(Mockery::type('string'))->andReturn(''); + + $this->expectException(ImportCheckException::class); + $source = new WpEinsatz(); + $source->checkPreconditions(); + } + + public function testCheckShouldFailWhenFieldsContainSpecialCharacters() + { + /** @var Mockery\Mock $wpdb */ + global $wpdb; + + // Pretend that the table exists and return some bad column names + $wpdb->expects()->get_var(Mockery::type('string'))->andReturn('wpunit_einsaetze'); + $wpdb->expects()->get_col("DESCRIBE 'wpunit_einsaetze'", 0)->andReturn(['Datum', 'Örtlichkeit', 'Einsatz#']); + + $this->expectException(ImportCheckException::class); + $source = new WpEinsatz(); + $source->checkPreconditions(); + } + + public function testCheckShouldPassIfConditionsAreMet() + { + /** @var Mockery\Mock $wpdb */ + global $wpdb; + + // Pretend that the table exists and return some good column names + $wpdb->expects()->get_var("SHOW TABLES LIKE 'wpunit_einsaetze'")->andReturn('wpunit_einsaetze'); + $wpdb->expects()->get_col("DESCRIBE 'wpunit_einsaetze'", 0)->andReturn(['Datum', 'Ort', 'Art', 'Einsatztext']); + + $source = new WpEinsatz(); + try { + $source->checkPreconditions(); + } catch (ImportCheckException $e) { + $this->fail("Check for preconditions failed when it shouldn't"); + } + } + + public function testCanGetFieldNames() + { + /** @var Mockery\Mock $wpdb */ + global $wpdb; + + // Return some column names + $wpdb->expects()->get_col("DESCRIBE 'wpunit_einsaetze'", 0)->andReturn(['ID', 'Nr_Jahr', 'Nr_Monat', 'Datum', 'Ort']); + + $source = new WpEinsatz(); + $this->assertEqualSets(['Datum', 'Ort'], $source->getFields()); + } + + public function testFieldsGetCached() + { + /** @var Mockery\Mock $wpdb */ + global $wpdb; + + // Return some column names + $wpdb->expects()->get_col("DESCRIBE 'wpunit_einsaetze'", 0)->andReturn(['ID', 'Nr_Jahr', 'Nr_Monat', 'Datum', 'Ort']); + + $source = new WpEinsatz(); + $this->assertEqualSets(['Datum', 'Ort'], $source->getFields()); + + // Just ask a second time for the fields, the database should not be hit again + $this->assertEqualSets(['Datum', 'Ort'], $source->getFields()); + } + + public function testGetsEntriesForAllFields() + { + /** @var Mockery\Mock $wpdb */ + global $wpdb; + + // Return some entries + $entries = [ + ['ID' => 1, 'colA' => 'value1', 'colB' => 'value2', 'colC' => 'value3'], + ['ID' => 2, 'colA' => 'value4', 'colB' => 'value5', 'colC' => 'value6'], + ['ID' => 3, 'colA' => 'value7', 'colB' => 'value8', 'colC' => 'value9'] + ]; + $wpdb->expects()->get_results("SELECT * FROM 'wpunit_einsaetze' ORDER BY Datum", ARRAY_A)->andReturn($entries); + + $source = new WpEinsatz(); + try { + $this->assertEqualSets($entries, $source->getEntries()); + } catch (ImportException $e) { + $this->fail(); + } + } + + public function testGetsEntriesForCertainFields() + { + /** @var Mockery\Mock $wpdb */ + global $wpdb; + + // Return some entries + $entries = [ + ['ID' => 1, 'colA' => 'value1', 'colC' => 'value2'], + ['ID' => 2, 'colA' => 'value3', 'colC' => 'value4'], + ['ID' => 3, 'colA' => 'value5', 'colC' => 'value6'] + ]; + $wpdb->expects() + ->get_results("SELECT ID,colA,colC FROM 'wpunit_einsaetze' ORDER BY Datum", ARRAY_A) + ->andReturn($entries); + + $source = new WpEinsatz(); + try { + $this->assertEqualSets($entries, $source->getEntries(['colA', 'colC'])); + } catch (ImportException $e) { + $this->fail(); + } + } + + public function testThrowsWhenEntriesCannotBeQueried() + { + /** @var Mockery\Mock $wpdb */ + global $wpdb; + + // Return some null for failure + $wpdb->expects()->get_results(Mockery::type('string'), ARRAY_A)->andReturn(null); + + $this->expectException(ImportException::class); + $source = new WpEinsatz(); + $source->getEntries(); + } + + public function testReturnsCorrectDateFormat() + { + $source = new WpEinsatz(); + $dateFormat = $source->getDateFormat(); + $this->assertEquals('Y-m-d', $dateFormat); + } + + public function testReturnsCorrectTimeFormat() + { + $source = new WpEinsatz(); + $timeFormat = $source->getTimeFormat(); + $this->assertEquals('H:i:s', $timeFormat); + } +} diff --git a/tests/unit/UnitTestCase.php b/tests/unit/UnitTestCase.php index 0b4d1071..63bee535 100644 --- a/tests/unit/UnitTestCase.php +++ b/tests/unit/UnitTestCase.php @@ -2,6 +2,7 @@ namespace abrain\Einsatzverwaltung; use Brain\Monkey; +use Mockery; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use PHPUnit\Framework\TestCase; @@ -24,6 +25,11 @@ protected function setUp() Monkey\Functions\when('_n')->alias(function ($single, $plural, $number) { return $number === 1 ? $single : $plural; }); + + // Fake the global database object + global $wpdb; + $wpdb = Mockery::mock('\wpdb'); + $wpdb->prefix = 'wpunit_'; } protected function tearDown() diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index 28c629a9..1e31fd9f 100644 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -9,6 +9,7 @@ } require __DIR__ . '/../../vendor/autoload.php'; +require __DIR__ . '/constants.php'; require __DIR__ . '/UnitTestCase.php'; spl_autoload_register(function ($class) { diff --git a/tests/unit/constants.php b/tests/unit/constants.php new file mode 100644 index 00000000..dce92c9c --- /dev/null +++ b/tests/unit/constants.php @@ -0,0 +1,4 @@ + Date: Sat, 7 Nov 2020 22:14:10 +0100 Subject: [PATCH 06/25] Implement basic page loading --- src/Import/Page.php | 74 ++++++++++++++++ src/Import/Sources/AbstractSource.php | 10 +++ src/Import/Tool.php | 120 -------------------------- 3 files changed, 84 insertions(+), 120 deletions(-) diff --git a/src/Import/Page.php b/src/Import/Page.php index d61c4083..731f7674 100644 --- a/src/Import/Page.php +++ b/src/Import/Page.php @@ -2,6 +2,19 @@ namespace abrain\Einsatzverwaltung\Import; use abrain\Einsatzverwaltung\AdminPage; +use abrain\Einsatzverwaltung\Import\Sources\AbstractSource; +use abrain\Einsatzverwaltung\Import\Sources\Csv; +use abrain\Einsatzverwaltung\Import\Sources\WpEinsatz; +use function __; +use function array_key_exists; +use function check_admin_referer; +use function esc_html; +use function explode; +use function filter_input; +use function submit_button; +use function wp_nonce_field; +use const FILTER_SANITIZE_STRING; +use const INPUT_POST; /** * The main page for the Import tool @@ -9,6 +22,17 @@ */ class Page extends AdminPage { + + /** + * @var AbstractSource + */ + private $currentSource; + + /** + * @var AbstractSource[] + */ + private $sources; + /** * Page constructor. */ @@ -27,6 +51,56 @@ protected function getMenuSlug() protected function echoPageContent() { + $this->loadSources(); + + $action = null; + $postAction = filter_input(INPUT_POST, 'action', FILTER_SANITIZE_STRING); + if (!empty($postAction)) { + list($identifier, $action) = explode(':', $postAction); + if (array_key_exists($identifier, $this->sources)) { + $this->currentSource = $this->sources[$identifier]; + } + } + + if (null == $this->currentSource || !($this->currentSource instanceof AbstractSource) || empty($action)) { + printf('

%s

', __('You can import incident reports from the following sources:', 'einsatzverwaltung')); + + echo '
    '; + foreach ($this->sources as $source) { + $firstAction = $source->getFirstAction(); + + echo '
  • '; + printf('

    %s

    ', esc_html($source->getName())); + printf('

    %s

    ', esc_html($source->getDescription())); + if (false !== $firstAction) { + echo '
    '; + printf('', $source->getActionAttribute($firstAction['slug'])); + wp_nonce_field($source->getNonce($firstAction['slug'])); + submit_button($firstAction['button_text'], 'secondary', 'submit', false); + echo '
    '; + } + echo '
  • '; + } + echo '
'; + return; + } + + // Check if the request has been sent through the form + check_admin_referer($this->currentSource->getNonce($action)); + + // Set variables for further flow control + $currentAction = $this->currentSource->getAction($action); + $nextAction = $this->currentSource->getNextAction($currentAction); + echo '

Content

'; } + + private function loadSources() + { + $wpEinsatz = new WpEinsatz(); + $this->sources[$wpEinsatz->getIdentifier()] = $wpEinsatz; + + $csv = new Csv(); + $this->sources[$csv->getIdentifier()] = $csv; + } } diff --git a/src/Import/Sources/AbstractSource.php b/src/Import/Sources/AbstractSource.php index ef4051d2..30d2a6fd 100644 --- a/src/Import/Sources/AbstractSource.php +++ b/src/Import/Sources/AbstractSource.php @@ -240,6 +240,16 @@ public function getNextAction(array $currentAction) return $this->actionOrder[$key + 1]; } + /** + * @param string $action + * + * @return string + */ + public function getNonce(string $action) + { + return sprintf("%s_%s", $this->getIdentifier(), $action); + } + /** * @return array */ diff --git a/src/Import/Tool.php b/src/Import/Tool.php index 98801d70..9996232c 100644 --- a/src/Import/Tool.php +++ b/src/Import/Tool.php @@ -1,12 +1,9 @@ utilities = $utilities; - $this->data = $data; - - $this->loadSources(); - } - - /** - * Fügt das Werkzeug zum Menü hinzu - */ - public function addToolToMenu() - { - add_management_page( - 'Einsatzberichte importieren', - 'Einsatzberichte importieren', - 'manage_options', - self::EVW_TOOL_IMPORT_SLUG, - array($this, 'renderToolPage') - ); - } - - /** - * @param AbstractSource $source - * @param string $action - */ - private function checkNonce($source, $action) - { - check_admin_referer($this->getNonceAction($source, $action)); - } - - /** - * @param AbstractSource $source - * @param string $action - * @return string - */ - private function getNonceAction($source, $action) - { - return $source->getIdentifier() . '_' . $action; - } - - private function loadSources() - { - $wpEinsatz = new WpEinsatz($this->utilities); - $this->sources[$wpEinsatz->getIdentifier()] = $wpEinsatz; - - $csv = new Csv($this->utilities); - $this->sources[$csv->getIdentifier()] = $csv; - } - /** * Generiert den Inhalt der Werkzeugseite */ @@ -116,48 +40,6 @@ public function renderToolPage() $this->helper->taxonomies = IncidentReport::getTerms(); $this->helper->postFields = IncidentReport::getPostFields(); - echo '
'; - echo '

' . 'Einsatzberichte importieren' . '

'; - - $aktion = null; - if (array_key_exists('aktion', $_POST)) { - list($identifier, $aktion) = explode(':', $_POST['aktion']); - if (array_key_exists($identifier, $this->sources)) { - $this->currentSource = $this->sources[$identifier]; - } - } - - if (null == $this->currentSource || !($this->currentSource instanceof AbstractSource) || empty($aktion)) { - echo '

Dieses Werkzeug importiert Einsatzberichte aus verschiedenen Quellen.

'; - - echo '
    '; - /** @var AbstractSource $source */ - foreach ($this->sources as $source) { - $firstAction = $source->getFirstAction(); - - echo '
  • '; - echo '

    ' . $source->getName() . '

    '; - echo '

    ' . $source->getDescription() . '

    '; - if (false !== $firstAction) { - echo '
    '; - echo ''; - wp_nonce_field($this->getNonceAction($source, $firstAction['slug'])); - submit_button($firstAction['button_text'], 'secondary', 'submit', false); - echo '
    '; - } - echo '
  • '; - } - echo '
'; - return; - } - - // Nonce überprüfen - $this->checkNonce($this->currentSource, $aktion); - - // Variablen für die Ablaufsteuerung - $this->currentAction = $this->currentSource->getAction($aktion); - $this->nextAction = $this->currentSource->getNextAction($this->currentAction); - // Einstellungen an die Importquelle übergeben if (array_key_exists('args', $this->currentAction) && is_array($this->currentAction['args'])) { foreach ($this->currentAction['args'] as $arg) { @@ -243,8 +125,6 @@ public function renderToolPage() submit_button($this->nextAction['button_text']); echo ''; } - - echo '
'; } private function analysisPage() From b4cf944ddeb8969433cb55c6a22a0941eb293ce9 Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Sat, 7 Nov 2020 22:14:56 +0100 Subject: [PATCH 07/25] Add a few simple tests for the CSV source --- tests/unit/Import/Sources/CsvTest.php | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/unit/Import/Sources/CsvTest.php diff --git a/tests/unit/Import/Sources/CsvTest.php b/tests/unit/Import/Sources/CsvTest.php new file mode 100644 index 00000000..ac00afdc --- /dev/null +++ b/tests/unit/Import/Sources/CsvTest.php @@ -0,0 +1,37 @@ +getIdentifier(); + $this->assertIsString($identifier); + $this->assertNotEmpty($identifier); + } + + public function testHasAName() + { + $source = new Csv(); + $name = $source->getName(); + $this->assertIsString($name); + $this->assertNotEmpty($name); + } + + public function testHasADescription() + { + $source = new Csv(); + $description = $source->getDescription(); + $this->assertIsString($description); + $this->assertNotEmpty($description); + } +} From 593155142989eb19b35a70625b8774eb12729d46 Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Sat, 7 Nov 2020 22:18:18 +0100 Subject: [PATCH 08/25] Simplify how the menu slug for a page is set / retrieved --- src/AdminPage.php | 16 +++++++++------- src/Import/Page.php | 10 +--------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/AdminPage.php b/src/AdminPage.php index 95635f0b..96aea9c6 100644 --- a/src/AdminPage.php +++ b/src/AdminPage.php @@ -12,6 +12,11 @@ */ abstract class AdminPage { + /** + * @var string + */ + private $menuSlug; + /** * @var string */ @@ -19,19 +24,16 @@ abstract class AdminPage /** * @param string $pageTitle + * @param string $menuSlug */ - public function __construct(string $pageTitle) + public function __construct(string $pageTitle, string $menuSlug) { $this->pageTitle = $pageTitle; + $this->menuSlug = $menuSlug; } abstract protected function echoPageContent(); - /** - * @return string - */ - abstract protected function getMenuSlug(); - /** * Prints an error message. * @@ -81,7 +83,7 @@ public function registerAsToolPage() $this->pageTitle, esc_html($this->pageTitle), 'manage_options', - $this->getMenuSlug(), + $this->menuSlug, array($this, 'render') ); } diff --git a/src/Import/Page.php b/src/Import/Page.php index 731f7674..59ccc8b8 100644 --- a/src/Import/Page.php +++ b/src/Import/Page.php @@ -38,15 +38,7 @@ class Page extends AdminPage */ public function __construct() { - parent::__construct('Einsatzberichte importieren'); - } - - /** - * @inheritDoc - */ - protected function getMenuSlug() - { - return 'einsatzvw-tool-import'; + parent::__construct('Einsatzberichte importieren', 'einsatzvw-tool-import'); } protected function echoPageContent() From dce8970b956adde2cad4e3462ca36161b0cb408c Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Sat, 7 Nov 2020 22:53:33 +0100 Subject: [PATCH 09/25] Use proper escaping --- src/Import/Page.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Import/Page.php b/src/Import/Page.php index 59ccc8b8..996a7237 100644 --- a/src/Import/Page.php +++ b/src/Import/Page.php @@ -8,7 +8,9 @@ use function __; use function array_key_exists; use function check_admin_referer; +use function esc_attr; use function esc_html; +use function esc_html__; use function explode; use function filter_input; use function submit_button; @@ -55,7 +57,7 @@ protected function echoPageContent() } if (null == $this->currentSource || !($this->currentSource instanceof AbstractSource) || empty($action)) { - printf('

%s

', __('You can import incident reports from the following sources:', 'einsatzverwaltung')); + printf('

%s

', esc_html__('You can import incident reports from the following sources:', 'einsatzverwaltung')); echo '
    '; foreach ($this->sources as $source) { @@ -66,7 +68,7 @@ protected function echoPageContent() printf('

    %s

    ', esc_html($source->getDescription())); if (false !== $firstAction) { echo '
    '; - printf('', $source->getActionAttribute($firstAction['slug'])); + printf('', esc_attr($source->getActionAttribute($firstAction['slug']))); wp_nonce_field($source->getNonce($firstAction['slug'])); submit_button($firstAction['button_text'], 'secondary', 'submit', false); echo '
    '; From eee34e7727e270a33dc5a898c09799c20e0265cd Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Mon, 9 Nov 2020 18:00:56 +0100 Subject: [PATCH 10/25] Add new class for reading CSV files --- src/Exceptions/FileReadException.php | 11 +++ src/Import/CsvReader.php | 124 +++++++++++++++++++++++++++ tests/unit/Import/CsvReaderTest.php | 71 +++++++++++++++ tests/unit/Import/reports.csv | 12 +++ 4 files changed, 218 insertions(+) create mode 100644 src/Exceptions/FileReadException.php create mode 100644 src/Import/CsvReader.php create mode 100644 tests/unit/Import/CsvReaderTest.php create mode 100644 tests/unit/Import/reports.csv diff --git a/src/Exceptions/FileReadException.php b/src/Exceptions/FileReadException.php new file mode 100644 index 00000000..37f7bd1c --- /dev/null +++ b/src/Exceptions/FileReadException.php @@ -0,0 +1,11 @@ +delimiter = $delimiter; + $this->enclosure = $enclosure; + $this->filePath = $filePath; + } + + /** + * @param int $numLines How many lines to read. + * @param int[] $columns Array of 0-based column indices which should be returned. Empty array gets all columns. + * @param int $offset How many lines to skip before reading, default 0. + * + * @return string[][] + * @throws FileReadException + */ + public function getLines(int $numLines, array $columns = [], int $offset = 0): array + { + $handle = fopen($this->filePath, 'r'); + if ($handle === false) { + $message = sprintf(__('Could not open file %s', 'einsatzverwaltung'), $this->filePath); + throw new FileReadException($message); + } + + // If an offset is defines, some lines should be skipped + if ($offset > 0) { + // The CSV parsing has to be used here as well, as line breaks could appear inside field delimiters + $this->readLines($handle, $offset, []); + } + + $lines = $this->readLines($handle, $numLines, $columns); + + fclose($handle); + return $lines; + } + + /** + * @param resource $handle + * @param int $numLines + * @param array $columns + * + * @return array + * @throws FileReadException + */ + private function readLines($handle, int $numLines, array $columns): array + { + $linesRead = 0; + $lines = array(); + while ($numLines === 0 || $linesRead < $numLines) { + $line = fgetcsv($handle, 0, $this->delimiter, $this->enclosure); + $linesRead++; + + // End of file reached? + if ($line === false && feof($handle)) { + break; + } + + // Problem while reading the file + if (empty($line)) { + throw new FileReadException(); + } + + // Empty line in the file, skip this + if (is_array($line) && $line[0] == null) { + continue; + } + + // Return entire line when all columns have been requested + if (empty($columns)) { + $lines[] = $line; + continue; + } + + // Return only the requested columns + $filteredLine = array(); + foreach ($columns as $columnIndex) { + // If the line has less columns than expected, the value will be an empty string + $filteredLine[] = array_key_exists($columnIndex, $line) ? $line[$columnIndex] : ''; + } + $lines[] = $filteredLine; + } + + return $lines; + } +} diff --git a/tests/unit/Import/CsvReaderTest.php b/tests/unit/Import/CsvReaderTest.php new file mode 100644 index 00000000..4ecc4f9e --- /dev/null +++ b/tests/unit/Import/CsvReaderTest.php @@ -0,0 +1,71 @@ +expectException(FileReadException::class); + $csvReader = new CsvReader(__DIR__ . '/no-such-file.csv', ';', '"'); + + // Suppress the warning, otherwise PHPUnit would convert it to an exception + @$csvReader->getLines(1); + } + + public function testCanReadCertainNumberOfLines() + { + $csvReader = new CsvReader(__DIR__ . '/reports.csv', ';', '"'); + $lines = $csvReader->getLines(3); + $this->assertEquals([ + ['First', 'Second', 'Third', 'Last column'], + ['OJM19KwAeh', 'I3vSoJFB9M', 'Y161hkjINb', 'FMy5jUPI9Y'], + ['ID2ftXEztI', 'FKKNOoOKiK m5RJBo4HjD', '135wtpYq0I', 'YmDXv1t4HB'] + ], $lines); + } + + public function testCanReadEntireFile() + { + $csvReader = new CsvReader(__DIR__ . '/reports.csv', ';', '"'); + $lines = $csvReader->getLines(0); + $this->assertCount(11, $lines); + } + + public function testCanSkipLines() + { + $csvReader = new CsvReader(__DIR__ . '/reports.csv', ';', '"'); + $lines = $csvReader->getLines(2, [], 3); + $this->assertEquals([ + ['4XJOXyaahT', 'II1G1rIC3R', 'N9xLHxULuu iTb24Cr0W2', 'ekwgQCyBBs'], + ['o2T2kmvnEw', 'aKM7zt7H9M', 'fjHlHxUTU8', 'SvcKLU7Smc'] + ], $lines); + } + + public function testReturnsOnlyRequestedColumns() + { + $csvReader = new CsvReader(__DIR__ . '/reports.csv', ';', '"'); + $lines = $csvReader->getLines(3, [0,3]); + $this->assertEquals([ + ['First', 'Last column'], + ['OJM19KwAeh', 'FMy5jUPI9Y'], + ['ID2ftXEztI', 'YmDXv1t4HB'] + ], $lines); + } + + public function testFillsNotExistingColumns() + { + $csvReader = new CsvReader(__DIR__ . '/reports.csv', ';', '"'); + $lines = $csvReader->getLines(2, [1,3], 8); + $this->assertEquals([ + ['Ru18STzsnj', ''], + ['9f0NPAB0HU', ''] + ], $lines); + } +} diff --git a/tests/unit/Import/reports.csv b/tests/unit/Import/reports.csv new file mode 100644 index 00000000..7a329f78 --- /dev/null +++ b/tests/unit/Import/reports.csv @@ -0,0 +1,12 @@ +First;Second;Third;Last column +OJM19KwAeh;I3vSoJFB9M;Y161hkjINb;FMy5jUPI9Y +ID2ftXEztI;FKKNOoOKiK m5RJBo4HjD;135wtpYq0I;YmDXv1t4HB +4XJOXyaahT;II1G1rIC3R;N9xLHxULuu iTb24Cr0W2;ekwgQCyBBs +o2T2kmvnEw;aKM7zt7H9M;fjHlHxUTU8;SvcKLU7Smc +TweXDv0mph;SFwrH1TJsJ;39OcbnuXdR;vXxMrsiVyE + +cdt48vLr4n;EWh4XUKUK8;mTBF74hEuX;dextkGI2U9 +fxdGgCtaI5;Ru18STzsnj;; +INyDBBeT67;9f0NPAB0HU +6gVaWIeKzM;BlsJZDUEFY;TnOmbmYvSv;cki9zg3EtJ +wmFR3eHdJ3;h2Bm7iQzqT;AdYvSKMp2z;NKa9y9DxdL From 32055c18309b38b0e6c920089f3918acff3ae203 Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Wed, 11 Nov 2020 18:09:56 +0100 Subject: [PATCH 11/25] Use proper objects for representing the import steps --- src/Import/Page.php | 38 +++++----- src/Import/Sources/AbstractSource.php | 78 ++++++++++++--------- src/Import/Sources/Csv.php | 24 ++----- src/Import/Sources/WpEinsatz.php | 17 +---- src/Import/Step.php | 77 ++++++++++++++++++++ tests/unit/Import/Sources/CsvTest.php | 1 + tests/unit/Import/Sources/WpEinsatzTest.php | 1 + 7 files changed, 153 insertions(+), 83 deletions(-) create mode 100644 src/Import/Step.php diff --git a/src/Import/Page.php b/src/Import/Page.php index 996a7237..a97c6f1b 100644 --- a/src/Import/Page.php +++ b/src/Import/Page.php @@ -5,7 +5,6 @@ use abrain\Einsatzverwaltung\Import\Sources\AbstractSource; use abrain\Einsatzverwaltung\Import\Sources\Csv; use abrain\Einsatzverwaltung\Import\Sources\WpEinsatz; -use function __; use function array_key_exists; use function check_admin_referer; use function esc_attr; @@ -14,6 +13,7 @@ use function explode; use function filter_input; use function submit_button; +use function wp_die; use function wp_nonce_field; use const FILTER_SANITIZE_STRING; use const INPUT_POST; @@ -49,28 +49,22 @@ protected function echoPageContent() $action = null; $postAction = filter_input(INPUT_POST, 'action', FILTER_SANITIZE_STRING); - if (!empty($postAction)) { - list($identifier, $action) = explode(':', $postAction); - if (array_key_exists($identifier, $this->sources)) { - $this->currentSource = $this->sources[$identifier]; - } - } - if (null == $this->currentSource || !($this->currentSource instanceof AbstractSource) || empty($action)) { + if (empty($postAction)) { printf('

    %s

    ', esc_html__('You can import incident reports from the following sources:', 'einsatzverwaltung')); echo '
      '; foreach ($this->sources as $source) { - $firstAction = $source->getFirstAction(); + $firstStep = $source->getFirstStep(); echo '
    • '; printf('

      %s

      ', esc_html($source->getName())); printf('

      %s

      ', esc_html($source->getDescription())); - if (false !== $firstAction) { + if (false !== $firstStep) { echo '
      '; - printf('', esc_attr($source->getActionAttribute($firstAction['slug']))); - wp_nonce_field($source->getNonce($firstAction['slug'])); - submit_button($firstAction['button_text'], 'secondary', 'submit', false); + printf('', esc_attr($source->getActionAttribute($firstStep))); + wp_nonce_field($source->getNonce($firstStep)); + submit_button($firstStep->getButtonText(), 'secondary', 'submit', false); echo '
      '; } echo '
    • '; @@ -79,12 +73,22 @@ protected function echoPageContent() return; } - // Check if the request has been sent through the form - check_admin_referer($this->currentSource->getNonce($action)); + list($identifier, $action) = explode(':', $postAction); + if (!array_key_exists($identifier, $this->sources)) { + wp_die('Invalid source'); + } + $this->currentSource = $this->sources[$identifier]; // Set variables for further flow control - $currentAction = $this->currentSource->getAction($action); - $nextAction = $this->currentSource->getNextAction($currentAction); + $currentStep = $this->currentSource->getStep($action); + if ($currentStep === false) { + wp_die('Invalid step'); + } + + // Check if the request has been sent through the form + check_admin_referer($this->currentSource->getNonce($currentStep)); + + $nextStep = $this->currentSource->getNextStep($currentStep); echo '

      Content

      '; } diff --git a/src/Import/Sources/AbstractSource.php b/src/Import/Sources/AbstractSource.php index 30d2a6fd..0e7609d5 100644 --- a/src/Import/Sources/AbstractSource.php +++ b/src/Import/Sources/AbstractSource.php @@ -3,6 +3,8 @@ use abrain\Einsatzverwaltung\Exceptions\ImportCheckException; use abrain\Einsatzverwaltung\Exceptions\ImportException; +use abrain\Einsatzverwaltung\Import\Step; +use function esc_attr; use function sprintf; /** @@ -10,11 +12,9 @@ */ abstract class AbstractSource { - protected $actionOrder = array(); protected $args = array(); protected $autoMatchFields = array(); protected $cachedFields; - /** * @var string */ @@ -34,6 +34,11 @@ abstract class AbstractSource protected $problematicFields = array(); + /** + * @var Step[] + */ + protected $steps = array(); + /** * AbstractSource constructor. * @@ -48,14 +53,16 @@ abstract public function __construct(); abstract public function checkPreconditions(); /** + * TODO: The source shouldn't echo anything, but return its requirements in a standardized way + * * Generiert für Argumente, die in der nächsten Action wieder gebraucht werden, Felder, die in das Formular * eingebaut werden können, damit diese mitgenommen werden * - * @param array $nextAction Die nächste Action + * @param Step $nextStep */ - public function echoExtraFormFields(array $nextAction) + public function echoExtraFormFields(Step $nextStep) { - if (empty($nextAction)) { + if (empty($nextStep)) { return; } @@ -65,9 +72,9 @@ public function echoExtraFormFields(array $nextAction) echo ' /> Einsatzberichte sofort veröffentlichen'; echo '

      Das Setzen dieser Option verlängert die Importzeit deutlich, Benutzung auf eigene Gefahr. Standardmäßig werden die Berichte als Entwurf importiert.

      '; - foreach ($nextAction['args'] as $arg) { + foreach ($nextStep->getArguments() as $arg) { if (array_key_exists($arg, $this->args)) { - echo ''; + printf('', esc_attr($arg), esc_attr($this->args[$arg])); } } } @@ -83,30 +90,31 @@ public function getDescription() } /** - * @param string $action + * @param Step $step + * * @return string */ - public function getActionAttribute(string $action) + public function getActionAttribute(Step $step) { - return $this->getIdentifier() . ':' . $action; + return sprintf("%s:%s", $this->getIdentifier(), $step->getSlug()); } /** - * Gibt das Action-Array für $slug zurück + * Gets a Step object based on its slug. * - * @param string $slug Slug der Action + * @param string $slug Slug of the step * - * @return array|bool Das Array der Action oder false, wenn es keines für $slug gibt + * @return Step|false The Step object or false if there is no step for this slug */ - public function getAction(string $slug) + public function getStep(string $slug) { if (empty($slug)) { return false; } - foreach ($this->actionOrder as $action) { - if ($action['slug'] == $slug) { - return $action; + foreach ($this->steps as $step) { + if ($step->getSlug() == $slug) { + return $step; } } @@ -143,17 +151,17 @@ abstract public function getEntries($fields); abstract public function getFields(); /** - * Gibt die erste Action der Importquelle zurück + * Returns the first step of the import source. * - * @return array|bool Ein Array, das die erste Action beschreibt, oder false, wenn es keine Action gibt + * @return Step|false The Step object representing the first step or false if there are no steps defined. */ - public function getFirstAction() + public function getFirstStep() { - if (empty($this->actionOrder)) { + if (empty($this->steps)) { return false; } - return $this->actionOrder[0]; + return $this->steps[0]; } /** @@ -221,33 +229,39 @@ public function getName() /** * Gibt die nächste Action der Importquelle zurück * - * @param array $currentAction Array, das die aktuelle Action beschreibt + * @param Step $currentStep Array, das die aktuelle Action beschreibt * - * @return array|bool Ein Array, das die nächste Action beschreibt, oder false, wenn es keine weitere gibt + * @return Step|false Ein Array, das die nächste Action beschreibt, oder false, wenn es keine weitere gibt */ - public function getNextAction(array $currentAction) + public function getNextStep(Step $currentStep) { - if (empty($this->actionOrder)) { + if (empty($this->steps)) { return false; } - $key = array_search($currentAction, $this->actionOrder); + $key = array_search($currentStep, $this->steps); + + // Make sure the given step was found in the list of steps + if ($key === false) { + return false; + } - if ($key + 1 >= count($this->actionOrder)) { + // Return false if this was the last step + if ($key + 1 >= count($this->steps)) { return false; } - return $this->actionOrder[$key + 1]; + return $this->steps[$key + 1]; } /** - * @param string $action + * @param Step $step * * @return string */ - public function getNonce(string $action) + public function getNonce(Step $step) { - return sprintf("%s_%s", $this->getIdentifier(), $action); + return sprintf("%s_%s", $this->getIdentifier(), $step->getSlug()); } /** diff --git a/src/Import/Sources/Csv.php b/src/Import/Sources/Csv.php index 1ba51c2f..1b350b7d 100644 --- a/src/Import/Sources/Csv.php +++ b/src/Import/Sources/Csv.php @@ -2,6 +2,7 @@ namespace abrain\Einsatzverwaltung\Import\Sources; use abrain\Einsatzverwaltung\Exceptions\ImportCheckException; +use abrain\Einsatzverwaltung\Import\Step; use abrain\Einsatzverwaltung\Utilities; use Exception; @@ -27,26 +28,9 @@ public function __construct() $this->identifier = 'evw_csv'; $this->name = 'CSV'; - $this->actionOrder = array( - array( - 'slug' => 'selectcsvfile', - 'name' => 'Dateiauswahl', - 'button_text' => 'Datei auswählen', - 'args' => array() - ), - array( - 'slug' => 'analysis', - 'name' => 'Analyse', - 'button_text' => 'Datei analysieren', - 'args' => array('csv_file_id', 'has_headlines', 'delimiter') - ), - array( - 'slug' => 'import', - 'name' => 'Import', - 'button_text' => 'Import starten', - 'args' => array('csv_file_id', 'has_headlines', 'delimiter') - ) - ); + $this->steps[] = new Step('selectcsvfile', 'Dateiauswahl', 'Datei auswählen'); + $this->steps[] = new Step('analysis', 'Analyse', 'Datei analysieren', ['csv_file_id', 'has_headlines', 'delimiter']); + $this->steps[] = new Step('import', 'Import', 'Import starten', ['csv_file_id', 'has_headlines', 'delimiter']); } /** diff --git a/src/Import/Sources/WpEinsatz.php b/src/Import/Sources/WpEinsatz.php index f02089eb..e55f9a26 100644 --- a/src/Import/Sources/WpEinsatz.php +++ b/src/Import/Sources/WpEinsatz.php @@ -3,6 +3,7 @@ use abrain\Einsatzverwaltung\Exceptions\ImportCheckException; use abrain\Einsatzverwaltung\Exceptions\ImportException; +use abrain\Einsatzverwaltung\Import\Step; use function __; use function esc_html; use function join; @@ -32,20 +33,8 @@ public function __construct() 'Datum' => 'post_date' ); - $this->actionOrder = array( - array( - 'slug' => 'analysis', - 'name' => 'Analyse', - 'button_text' => 'Datenbank analysieren', - 'args' => array() - ), - array( - 'slug' => 'import', - 'name' => 'Import', - 'button_text' => 'Import starten', - 'args' => array() - ) - ); + $this->steps[] = new Step('analysis', 'Analyse', 'Datenbank analysieren'); + $this->steps[] = new Step('import', 'Import', 'Import starten'); } /** diff --git a/src/Import/Step.php b/src/Import/Step.php new file mode 100644 index 00000000..f224c76e --- /dev/null +++ b/src/Import/Step.php @@ -0,0 +1,77 @@ +slug = $slug; + $this->title = $title; + $this->buttonText = $buttonText; + $this->arguments = $arguments; + } + + /** + * @return string[] + */ + public function getArguments(): array + { + return $this->arguments; + } + + /** + * @return string + */ + public function getButtonText(): string + { + return $this->buttonText; + } + + /** + * @return string + */ + public function getSlug(): string + { + return $this->slug; + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->title; + } +} diff --git a/tests/unit/Import/Sources/CsvTest.php b/tests/unit/Import/Sources/CsvTest.php index ac00afdc..ad2d963e 100644 --- a/tests/unit/Import/Sources/CsvTest.php +++ b/tests/unit/Import/Sources/CsvTest.php @@ -8,6 +8,7 @@ * @covers \abrain\Einsatzverwaltung\Import\Sources\AbstractSource * @covers \abrain\Einsatzverwaltung\Import\Sources\Csv * @package abrain\Einsatzverwaltung\Import\Sources + * @uses \abrain\Einsatzverwaltung\Import\Step */ class CsvTest extends TestCase { diff --git a/tests/unit/Import/Sources/WpEinsatzTest.php b/tests/unit/Import/Sources/WpEinsatzTest.php index a3a69930..469138bb 100644 --- a/tests/unit/Import/Sources/WpEinsatzTest.php +++ b/tests/unit/Import/Sources/WpEinsatzTest.php @@ -12,6 +12,7 @@ * @covers \abrain\Einsatzverwaltung\Import\Sources\AbstractSource * @covers \abrain\Einsatzverwaltung\Import\Sources\WpEinsatz * @package abrain\Einsatzverwaltung\Import\Sources + * @uses \abrain\Einsatzverwaltung\Import\Step */ class WpEinsatzTest extends UnitTestCase { From 5b33e429534523217a96757f05cd5df61392ad22 Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Wed, 25 Nov 2020 16:01:35 +0100 Subject: [PATCH 12/25] Carry over settings from step to step --- src/Import/Helper.php | 4 ++++ src/Import/Page.php | 27 +++++++++++++++++++++++++++ src/Import/Tool.php | 29 ----------------------------- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/Import/Helper.php b/src/Import/Helper.php index 6afa9619..63debc9c 100644 --- a/src/Import/Helper.php +++ b/src/Import/Helper.php @@ -49,6 +49,10 @@ public function __construct(Utilities $utilities, Data $data) { $this->utilities = $utilities; $this->data = $data; + + $this->metaFields = IncidentReport::getMetaFields(); + $this->taxonomies = IncidentReport::getTerms(); + $this->postFields = IncidentReport::getPostFields(); } /** diff --git a/src/Import/Page.php b/src/Import/Page.php index a97c6f1b..ac60de29 100644 --- a/src/Import/Page.php +++ b/src/Import/Page.php @@ -5,6 +5,7 @@ use abrain\Einsatzverwaltung\Import\Sources\AbstractSource; use abrain\Einsatzverwaltung\Import\Sources\Csv; use abrain\Einsatzverwaltung\Import\Sources\WpEinsatz; +use abrain\Einsatzverwaltung\Utilities; use function array_key_exists; use function check_admin_referer; use function esc_attr; @@ -12,6 +13,7 @@ use function esc_html__; use function explode; use function filter_input; +use function sanitize_text_field; use function submit_button; use function wp_die; use function wp_nonce_field; @@ -90,6 +92,31 @@ protected function echoPageContent() $nextStep = $this->currentSource->getNextStep($currentStep); + // Read the settings that have been passed from the previous step + foreach ($currentStep->getArguments() as $argument) { + $value = filter_input(INPUT_POST, $argument, FILTER_SANITIZE_STRING); + $this->currentSource->putArg($argument, $value); + } + + // Pass settings for date and time to the CSV source + // TODO move custom logic into the class of the source + if ('evw_csv' == $this->currentSource->getIdentifier()) { + if (array_key_exists('import_date_format', $_POST)) { + $this->currentSource->putArg('import_date_format', sanitize_text_field($_POST['import_date_format'])); + } + + if (array_key_exists('import_time_format', $_POST)) { + $this->currentSource->putArg('import_time_format', sanitize_text_field($_POST['import_time_format'])); + } + } + + // Carry over the setting whether to publish imported reports immediately + $publishReports = filter_input(INPUT_POST, 'import_publish_reports', FILTER_SANITIZE_STRING); + $this->currentSource->putArg( + 'import_publish_reports', + Utilities::sanitizeCheckbox($publishReports) + ); + echo '

      Content

      '; } diff --git a/src/Import/Tool.php b/src/Import/Tool.php index 9996232c..bde78b3d 100644 --- a/src/Import/Tool.php +++ b/src/Import/Tool.php @@ -36,35 +36,6 @@ class Tool public function renderToolPage() { $this->helper = new Helper($this->utilities, $this->data); - $this->helper->metaFields = IncidentReport::getMetaFields(); - $this->helper->taxonomies = IncidentReport::getTerms(); - $this->helper->postFields = IncidentReport::getPostFields(); - - // Einstellungen an die Importquelle übergeben - if (array_key_exists('args', $this->currentAction) && is_array($this->currentAction['args'])) { - foreach ($this->currentAction['args'] as $arg) { - $value = (array_key_exists($arg, $_POST) ? sanitize_text_field($_POST[$arg]) : null); - $this->currentSource->putArg($arg, $value); - } - } - - // Datums- und Zeitformat für CSV-Import übernehmen - if ('evw_csv' == $this->currentSource->getIdentifier()) { - if (array_key_exists('import_date_format', $_POST)) { - $this->currentSource->putArg('import_date_format', sanitize_text_field($_POST['import_date_format'])); - } - - if (array_key_exists('import_time_format', $_POST)) { - $this->currentSource->putArg('import_time_format', sanitize_text_field($_POST['import_time_format'])); - } - } - - // 'Sofort veröffentlichen'-Option übernehmen - $publishReports = filter_input(INPUT_POST, 'import_publish_reports', FILTER_SANITIZE_STRING); - $this->currentSource->putArg( - 'import_publish_reports', - Utilities::sanitizeCheckbox($publishReports) - ); echo "

      {$this->currentAction['name']}

      "; From 536c44ede8212f59b6dbc381be6778867f972a4c Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Mon, 30 Nov 2020 15:58:16 +0100 Subject: [PATCH 13/25] Added switch for the actions and adjusted the file chooser --- src/Import/Page.php | 73 ++++++++++++++++++++++++++- src/Import/Sources/AbstractSource.php | 9 +++- src/Import/Sources/Csv.php | 59 ++++++++++++++-------- src/Import/Sources/FileSource.php | 23 +++++++++ src/Import/Sources/WpEinsatz.php | 4 +- src/Import/Tool.php | 69 ------------------------- 6 files changed, 141 insertions(+), 96 deletions(-) create mode 100644 src/Import/Sources/FileSource.php diff --git a/src/Import/Page.php b/src/Import/Page.php index ac60de29..a1a28c62 100644 --- a/src/Import/Page.php +++ b/src/Import/Page.php @@ -4,8 +4,10 @@ use abrain\Einsatzverwaltung\AdminPage; use abrain\Einsatzverwaltung\Import\Sources\AbstractSource; use abrain\Einsatzverwaltung\Import\Sources\Csv; +use abrain\Einsatzverwaltung\Import\Sources\FileSource; use abrain\Einsatzverwaltung\Import\Sources\WpEinsatz; use abrain\Einsatzverwaltung\Utilities; +use function __; use function array_key_exists; use function check_admin_referer; use function esc_attr; @@ -13,7 +15,10 @@ use function esc_html__; use function explode; use function filter_input; +use function get_posts; +use function printf; use function sanitize_text_field; +use function sprintf; use function submit_button; use function wp_die; use function wp_nonce_field; @@ -117,7 +122,73 @@ protected function echoPageContent() Utilities::sanitizeCheckbox($publishReports) ); - echo '

      Content

      '; + printf("

      %s

      ", esc_html($currentStep->getTitle())); + + switch ($action) { + case AbstractSource::STEP_ANALYSIS: + echo "Analysiere..."; + break; + case AbstractSource::STEP_CHOOSEFILE: + if (!$this->currentSource instanceof FileSource) { + $this->printError('The selected source does not import from a file'); + return; + } + $this->echoFileChooser($this->currentSource, $nextStep); + break; + case AbstractSource::STEP_IMPORT: + echo "Import..."; + break; + default: + $this->printError(sprintf('Action %s is unknown', esc_html($action))); + } + } + + /** + * @param FileSource $source + * @param Step $nextStep + */ + private function echoFileChooser(FileSource $source, Step $nextStep) + { + $mimeType = $source->getMimeType(); + if (empty($mimeType)) { + $this->printError('The MIME type must not be empty'); + return; + } + + echo '

      Bitte werfe einen Blick in die Dokumentation, um herauszufinden, welche Anforderungen an die Datei gestellt werden.

      '; + + echo '

      In der Mediathek gefundene Dateien

      '; + echo 'Bevor eine Datei für den Import verwendet werden kann, muss sie in die Mediathek hochgeladen worden sein. Nach erfolgreichem Import kann die Datei gelöscht werden.'; + $this->printWarning('Der Inhalt der Mediathek ist öffentlich abrufbar. Achte darauf, dass die Importdatei keine sensiblen Daten enthält.'); + + $attachments = get_posts(array( + 'post_type' => 'attachment', + 'post_mime_type' => $mimeType + )); + + if (empty($attachments)) { + $this->printInfo(sprintf(__('No files of type %s found.', 'einsatzverwaltung'), $mimeType)); + return; + } + + echo '
      '; + wp_nonce_field($this->currentSource->getNonce($nextStep)); + + echo '
      '; + foreach ($attachments as $attachment) { + printf( + '
      ', + esc_attr($attachment->ID), + esc_html($attachment->post_title) + ); + } + echo '
      '; + + $source->echoExtraFormFields(AbstractSource::STEP_CHOOSEFILE, $nextStep); + + printf('', $this->currentSource->getActionAttribute($nextStep)); + submit_button($nextStep->getButtonText()); + echo '
      '; } private function loadSources() diff --git a/src/Import/Sources/AbstractSource.php b/src/Import/Sources/AbstractSource.php index 0e7609d5..5da2a8c2 100644 --- a/src/Import/Sources/AbstractSource.php +++ b/src/Import/Sources/AbstractSource.php @@ -5,6 +5,7 @@ use abrain\Einsatzverwaltung\Exceptions\ImportException; use abrain\Einsatzverwaltung\Import\Step; use function esc_attr; +use function in_array; use function sprintf; /** @@ -12,6 +13,9 @@ */ abstract class AbstractSource { + const STEP_ANALYSIS = 'analysis'; + const STEP_CHOOSEFILE = 'choosefile'; + const STEP_IMPORT = 'import'; protected $args = array(); protected $autoMatchFields = array(); protected $cachedFields; @@ -58,11 +62,12 @@ abstract public function checkPreconditions(); * Generiert für Argumente, die in der nächsten Action wieder gebraucht werden, Felder, die in das Formular * eingebaut werden können, damit diese mitgenommen werden * + * @param string $currentAction * @param Step $nextStep */ - public function echoExtraFormFields(Step $nextStep) + public function echoExtraFormFields(string $currentAction, Step $nextStep) { - if (empty($nextStep)) { + if (empty($nextStep) || !in_array($currentAction, [self::STEP_ANALYSIS, self::STEP_IMPORT])) { return; } diff --git a/src/Import/Sources/Csv.php b/src/Import/Sources/Csv.php index 1b350b7d..52997507 100644 --- a/src/Import/Sources/Csv.php +++ b/src/Import/Sources/Csv.php @@ -5,11 +5,13 @@ use abrain\Einsatzverwaltung\Import\Step; use abrain\Einsatzverwaltung\Utilities; use Exception; +use function __; +use function in_array; /** * Importiert Einsatzberichte aus einer CSV-Datei */ -class Csv extends AbstractSource +class Csv extends FileSource { private $dateFormats = array('d.m.Y', 'd.m.y', 'Y-m-d', 'm/d/Y', 'm/d/y'); private $timeFormats = array('H:i', 'G:i', 'H:i:s', 'G:i:s'); @@ -26,11 +28,12 @@ public function __construct() { $this->description = 'Importiert Einsatzberichte aus einer CSV-Datei.'; $this->identifier = 'evw_csv'; + $this->mimeType = 'text/csv'; $this->name = 'CSV'; - $this->steps[] = new Step('selectcsvfile', 'Dateiauswahl', 'Datei auswählen'); - $this->steps[] = new Step('analysis', 'Analyse', 'Datei analysieren', ['csv_file_id', 'has_headlines', 'delimiter']); - $this->steps[] = new Step('import', 'Import', 'Import starten', ['csv_file_id', 'has_headlines', 'delimiter']); + $this->steps[] = new Step(self::STEP_CHOOSEFILE, __('Choose a CSV file', 'einsatzverwaltung'), 'Datei auswählen'); + $this->steps[] = new Step(self::STEP_ANALYSIS, 'Analyse', 'Datei analysieren', ['file_id', 'has_headlines', 'delimiter']); + $this->steps[] = new Step(self::STEP_IMPORT, 'Import', 'Import starten', ['file_id', 'has_headlines', 'delimiter']); } /** @@ -74,28 +77,40 @@ public function checkPreconditions() /** * @inheritDoc */ - public function echoExtraFormFields($nextAction) + public function echoExtraFormFields(string $currentAction, Step $nextStep) { - echo '

      Datums- und Zeitformat

      '; - $dateExample = strtotime('December 31st 5:29 am'); - - echo '
      '; - foreach ($this->dateFormats as $dateFormat) { - echo '
      '; - } - echo '
      '; + if ($currentAction === self::STEP_CHOOSEFILE) { + echo '

      Aufbau der CSV-Datei

      '; + echo ''; + echo ''; + echo '

      Setze diesen Haken, wenn die erste Zeile der CSV-Datei keine Daten von Einsätzen enthält, sondern nur die Überschriften der jeweiligen Spalten.

      '; + echo '
      Trennzeichen zwischen den Spalten: '; + echo ''; + echo ' '; + echo '

      Wenn du unsicher bist, kannst du die CSV-Datei mit einem Texteditor öffnen und nachsehen.

      '; + echo '

      Als Feldbegrenzerzeichen (umschließt ggf. den Inhalt einer Spalte) wird das Anführungszeichen " erwartet.

      '; + } elseif (in_array($currentAction, [self::STEP_ANALYSIS, self::STEP_IMPORT])) { + echo '

      Datums- und Zeitformat

      '; + $dateExample = strtotime('December 31st 5:29 am'); + + echo '
      '; + foreach ($this->dateFormats as $dateFormat) { + echo '
      '; + } + echo '
      '; - echo '
      '; - foreach ($this->timeFormats as $timeFormat) { - echo '
      '; + echo '
      '; + foreach ($this->timeFormats as $timeFormat) { + echo '
      '; + } + echo '
      '; } - echo '
      '; - parent::echoExtraFormFields($nextAction); + parent::echoExtraFormFields($currentAction, $nextStep); } /** diff --git a/src/Import/Sources/FileSource.php b/src/Import/Sources/FileSource.php new file mode 100644 index 00000000..8a3eb89e --- /dev/null +++ b/src/Import/Sources/FileSource.php @@ -0,0 +1,23 @@ +mimeType; + } +} diff --git a/src/Import/Sources/WpEinsatz.php b/src/Import/Sources/WpEinsatz.php index e55f9a26..c96b7a58 100644 --- a/src/Import/Sources/WpEinsatz.php +++ b/src/Import/Sources/WpEinsatz.php @@ -33,8 +33,8 @@ public function __construct() 'Datum' => 'post_date' ); - $this->steps[] = new Step('analysis', 'Analyse', 'Datenbank analysieren'); - $this->steps[] = new Step('import', 'Import', 'Import starten'); + $this->steps[] = new Step(self::STEP_ANALYSIS, 'Analyse', 'Datenbank analysieren'); + $this->steps[] = new Step(self::STEP_IMPORT, 'Import', 'Import starten'); } /** diff --git a/src/Import/Tool.php b/src/Import/Tool.php index bde78b3d..86905c32 100644 --- a/src/Import/Tool.php +++ b/src/Import/Tool.php @@ -6,7 +6,6 @@ use abrain\Einsatzverwaltung\Import\Sources\AbstractSource; use abrain\Einsatzverwaltung\Model\IncidentReport; use abrain\Einsatzverwaltung\Utilities; -use WP_Post; /** * Werkzeug für den Import von Einsatzberichten aus verschiedenen Quellen @@ -37,64 +36,11 @@ public function renderToolPage() { $this->helper = new Helper($this->utilities, $this->data); - echo "

      {$this->currentAction['name']}

      "; - // TODO gemeinsame Prüfungen auslagern if ('analysis' == $aktion) { $this->analysisPage(); } elseif ('import' == $aktion) { $this->importPage(); - } elseif ('selectcsvfile' == $aktion) { - if (false === $this->nextAction) { - $this->utilities->printError('Keine Nachfolgeaktion gefunden!'); - return; - } - - echo '

      Die CSV-Datei muss für den Import ein bestimmtes Format aufweisen. Jede Zeile in der Datei steht für einen Einsatzbericht und jede Spalte für ein Feld des Einsatzberichts (z.B. Alarmzeit, Einsatzort, ...). Die Reihenfolge der Spalten ist unerheblich, im nächsten Schritt können die Felder aus der Datei denen in der Einsatzverwaltung zugeordnet werden. Die erste Zeile in der Datei kann als Beschriftung der Spalten verwendet werden.

      '; - $this->printDataNotice(); - - echo '

      In der Mediathek gefundene CSV-Dateien

      '; - echo 'Bevor eine Datei für den Import verwendet werden kann, muss sie in die Mediathek hochgeladen worden sein. Nach erfolgreichem Import kann die Datei gelöscht werden.'; - $this->utilities->printWarning('Der Inhalt der Mediathek ist öffentlich abrufbar. Achte darauf, dass die Importdatei keine sensiblen Daten enthält.'); - - $csvAttachments = get_posts(array( - 'post_type' => 'attachment', - 'post_mime_type' => 'text/csv' - )); - - if (empty($csvAttachments)) { - echo '

      Keine CSV-Dateien gefunden.

      '; - return; - } - - echo '
      '; - wp_nonce_field($this->getNonceAction($this->currentSource, $this->nextAction['slug'])); - - echo '
      '; - foreach ($csvAttachments as $csvAttachment) { - /** @var WP_Post $csvAttachment */ - printf( - '
      ', - esc_attr($csvAttachment->ID), - esc_html($csvAttachment->post_title) - ); - } - echo '
      '; - ?> -

      Aufbau der CSV-Datei

      - - -

      Setze diesen Haken, wenn die erste Zeile der CSV-Datei keine Daten von Einsätzen enthält, sondern nur die Überschriften der jeweiligen Spalten.

      -
      - Trennzeichen zwischen den Spalten:  - -   -

      Meist werden die Spalten mit einem Semikolon voneinander getrennt. Wenn du unsicher bist, solltest du die CSV-Datei mit einem Texteditor öffnen und nachsehen.

      -

      Als Feldbegrenzerzeichen (umschließt ggf. den Inhalt einer Spalte) wird das Anführungszeichen " erwartet.

      - currentSource->getActionAttribute($this->nextAction['slug']) . '" />'; - submit_button($this->nextAction['button_text']); - echo '
      '; } } @@ -193,19 +139,4 @@ private function importPage() $url = admin_url('edit.php?post_type=einsatz'); printf('Zu den Einsatzberichten', $url); } - - private function printDataNotice() - { - // Hinweise ausgeben - echo '

      Hinweise zu den erwarteten Daten

      '; - echo '

      Die Felder Berichtstext, Berichtstitel, Einsatzleiter, Einsatzort und Mannschaftsstärke sind Freitextfelder.

      '; - echo '

      Für die Felder Alarmierungsart, Einsatzart, Externe Einsatzmittel und Fahrzeuge wird eine kommagetrennte Liste erwartet.
      Bisher unbekannte Einträge werden automatisch angelegt, die Einsatzart sollte nur ein einzelner Wert sein.

      '; - if ('evw_wpe' == $this->currentSource->getIdentifier()) { - echo '

      Das Feld Einsatzende erwartet eine Datums- und Zeitangabe im Format JJJJ-MM-TT hh:mm:ss (z.B. 2014-04-21 21:48:06). Die Sekundenangabe ist optional.

      '; - } - if ('evw_csv' == $this->currentSource->getIdentifier()) { - echo '

      Die Felder Alarmzeit und Einsatzende erwarten eine Datums- und Zeitangabe, das Format kann bei der Zuordnung der Felder angegeben werden.

      '; - } - echo '

      Die Felder Besonderer Einsatz und Fehlalarm erwarten Ja/Nein-Werte. Als Ja interpretiert werden 1 und Ja (Groß- und Kleinschreibung unerheblich), alle anderen Werte einschließlich eines leeren Feldes zählen als Nein.

      '; - } } From e3109061236d7733911426cdadda9846a107d6be Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Mon, 30 Nov 2020 17:56:28 +0100 Subject: [PATCH 14/25] Add the analysis step again --- src/Import/Helper.php | 123 -------------------- src/Import/Page.php | 193 +++++++++++++++++++++++++++---- src/Import/Sources/Csv.php | 2 +- src/Import/Sources/WpEinsatz.php | 4 +- src/Import/Tool.php | 64 ---------- 5 files changed, 176 insertions(+), 210 deletions(-) diff --git a/src/Import/Helper.php b/src/Import/Helper.php index 63debc9c..9f16e5c3 100644 --- a/src/Import/Helper.php +++ b/src/Import/Helper.php @@ -55,60 +55,6 @@ public function __construct(Utilities $utilities, Data $data) $this->postFields = IncidentReport::getPostFields(); } - /** - * Gibt ein Auswahlfeld zur Zuordnung der Felder in Einsatzverwaltung aus - * - * @param array $args { - * @type string $name Name des Dropdownfelds im Formular - * @type string $selected Wert der ausgewählten Option - * @type array $unmatchableFields Felder, die nicht als Importziel auswählbar sein sollen - * } - */ - private function dropdownEigeneFelder($args) - { - $defaults = array( - 'name' => null, - 'selected' => '-', - 'unmatchableFields' => array() - ); - $parsedArgs = wp_parse_args($args, $defaults); - - if (null === $parsedArgs['name'] || empty($parsedArgs['name'])) { - _doing_it_wrong(__FUNCTION__, 'Name darf nicht null oder leer sein', ''); - } - - $fields = IncidentReport::getFields(); - - // Felder, die automatisch beschrieben werden, nicht zur Auswahl stellen - foreach ($parsedArgs['unmatchableFields'] as $ownField) { - unset($fields[$ownField]); - } - - // Sortieren und ausgeben - uasort($fields, function ($field1, $field2) { - return strcmp($field1['label'], $field2['label']); - }); - $string = ''; - - echo $string; - } - /** * @param array $mapping * @param array $sourceEntry @@ -354,75 +300,6 @@ public function prepareImport($source, $mapping, &$preparedInsertArgs, &$yearsAf } } - /** - * Gibt das Formular für die Zuordnung zwischen zu importieren Feldern und denen von Einsatzverwaltung aus - * - * @param AbstractSource $source - * @param array $args { - * @type array $mapping Zuordnung von zu importieren Feldern auf Einsatzverwaltungsfelder - * @type array $next_action Array der nächsten Action - * @type string $nonce_action Wert der Nonce - * @type string $action_value Wert der action-Variable - * @type string submit_button_text Beschriftung für den Button unter dem Formular - * } - */ - public function renderMatchForm($source, $args) - { - $defaults = array( - 'mapping' => array(), - 'next_action' => null, - 'nonce_action' => '', - 'action_value' => '', - 'submit_button_text' => 'Import starten' - ); - - $parsedArgs = wp_parse_args($args, $defaults); - $fields = $source->getFields(); - - $unmatchableFields = $source->getUnmatchableFields(); - if (ReportNumberController::isAutoIncidentNumbers()) { - $this->utilities->printInfo('Einsatznummern können nur importiert werden, wenn die automatische Verwaltung deaktiviert ist.'); - - $unmatchableFields[] = 'einsatz_incidentNumber'; - } - - echo '
      '; - wp_nonce_field($parsedArgs['nonce_action']); - echo ''; - echo ''; - foreach ($fields as $field) { - echo ''; - } - echo '
      '; - printf('Feld in %s', $source->getName()); - echo '' . 'Feld in Einsatzverwaltung' . '
      ' . $field . ''; - if (array_key_exists($field, $source->getAutoMatchFields())) { - echo 'wird automatisch zugeordnet'; - } elseif (in_array($field, $source->getProblematicFields())) { - $this->utilities->printWarning(sprintf('Probleme mit Feld %s, siehe Analyse', $field)); - } else { - $selected = '-'; - if (!empty($parsedArgs['mapping']) && - array_key_exists($field, $parsedArgs['mapping']) && - !empty($parsedArgs['mapping'][$field]) - ) { - $selected = $parsedArgs['mapping'][$field]; - } - - $this->dropdownEigeneFelder(array( - 'name' => $source->getInputName($field), - 'selected' => $selected, - 'unmatchableFields' => $unmatchableFields - )); - } - echo '
      '; - if (!empty($parsedArgs['next_action'])) { - $source->echoExtraFormFields($parsedArgs['next_action']); - } - submit_button($parsedArgs['submit_button_text']); - echo '
      '; - } - /** * @param array $preparedInsertArgs * @param AbstractSource $source diff --git a/src/Import/Page.php b/src/Import/Page.php index a1a28c62..ed4cceab 100644 --- a/src/Import/Page.php +++ b/src/Import/Page.php @@ -2,13 +2,19 @@ namespace abrain\Einsatzverwaltung\Import; use abrain\Einsatzverwaltung\AdminPage; +use abrain\Einsatzverwaltung\Exceptions\ImportCheckException; +use abrain\Einsatzverwaltung\Exceptions\ImportException; use abrain\Einsatzverwaltung\Import\Sources\AbstractSource; use abrain\Einsatzverwaltung\Import\Sources\Csv; use abrain\Einsatzverwaltung\Import\Sources\FileSource; use abrain\Einsatzverwaltung\Import\Sources\WpEinsatz; +use abrain\Einsatzverwaltung\Model\IncidentReport; +use abrain\Einsatzverwaltung\ReportNumberController; use abrain\Einsatzverwaltung\Utilities; use function __; +use function _n; use function array_key_exists; +use function array_keys; use function check_admin_referer; use function esc_attr; use function esc_html; @@ -16,10 +22,15 @@ use function explode; use function filter_input; use function get_posts; +use function implode; +use function in_array; use function printf; use function sanitize_text_field; +use function selected; use function sprintf; +use function strcmp; use function submit_button; +use function uasort; use function wp_die; use function wp_nonce_field; use const FILTER_SANITIZE_STRING; @@ -31,12 +42,6 @@ */ class Page extends AdminPage { - - /** - * @var AbstractSource - */ - private $currentSource; - /** * @var AbstractSource[] */ @@ -84,40 +89,40 @@ protected function echoPageContent() if (!array_key_exists($identifier, $this->sources)) { wp_die('Invalid source'); } - $this->currentSource = $this->sources[$identifier]; + $currentSource = $this->sources[$identifier]; // Set variables for further flow control - $currentStep = $this->currentSource->getStep($action); + $currentStep = $currentSource->getStep($action); if ($currentStep === false) { wp_die('Invalid step'); } // Check if the request has been sent through the form - check_admin_referer($this->currentSource->getNonce($currentStep)); + check_admin_referer($currentSource->getNonce($currentStep)); - $nextStep = $this->currentSource->getNextStep($currentStep); + $nextStep = $currentSource->getNextStep($currentStep); // Read the settings that have been passed from the previous step foreach ($currentStep->getArguments() as $argument) { $value = filter_input(INPUT_POST, $argument, FILTER_SANITIZE_STRING); - $this->currentSource->putArg($argument, $value); + $currentSource->putArg($argument, $value); } // Pass settings for date and time to the CSV source // TODO move custom logic into the class of the source - if ('evw_csv' == $this->currentSource->getIdentifier()) { + if ('evw_csv' == $currentSource->getIdentifier()) { if (array_key_exists('import_date_format', $_POST)) { - $this->currentSource->putArg('import_date_format', sanitize_text_field($_POST['import_date_format'])); + $currentSource->putArg('import_date_format', sanitize_text_field($_POST['import_date_format'])); } if (array_key_exists('import_time_format', $_POST)) { - $this->currentSource->putArg('import_time_format', sanitize_text_field($_POST['import_time_format'])); + $currentSource->putArg('import_time_format', sanitize_text_field($_POST['import_time_format'])); } } // Carry over the setting whether to publish imported reports immediately $publishReports = filter_input(INPUT_POST, 'import_publish_reports', FILTER_SANITIZE_STRING); - $this->currentSource->putArg( + $currentSource->putArg( 'import_publish_reports', Utilities::sanitizeCheckbox($publishReports) ); @@ -126,14 +131,14 @@ protected function echoPageContent() switch ($action) { case AbstractSource::STEP_ANALYSIS: - echo "Analysiere..."; + $this->echoAnalysis($currentSource, $currentStep, $nextStep); break; case AbstractSource::STEP_CHOOSEFILE: - if (!$this->currentSource instanceof FileSource) { + if (!$currentSource instanceof FileSource) { $this->printError('The selected source does not import from a file'); return; } - $this->echoFileChooser($this->currentSource, $nextStep); + $this->echoFileChooser($currentSource, $nextStep); break; case AbstractSource::STEP_IMPORT: echo "Import..."; @@ -143,6 +148,65 @@ protected function echoPageContent() } } + /** + * @param AbstractSource $source + * @param Step $currentStep + * @param Step $nextStep + */ + private function echoAnalysis(AbstractSource $source, Step $currentStep, Step $nextStep) + { + try { + $source->checkPreconditions(); + } catch (ImportCheckException $e) { + $this->printError(sprintf('Voraussetzung nicht erfüllt: %s', $e->getMessage())); + return; + } + + $fields = $source->getFields(); + if (empty($fields)) { + $this->printError('Es wurden keine Felder gefunden'); + return; + } + $numberOfFields = count($fields); + $this->printSuccess(sprintf( + _n('Found %1$d field: %2$s', 'Found %1$d fields: %2$s', $numberOfFields, 'einsatzverwaltung'), + $numberOfFields, + esc_html(implode($fields, ', ')) + )); + + // Check for mandatory fields + $mandatoryFieldsOk = true; + foreach (array_keys($source->getAutoMatchFields()) as $autoMatchField) { + if (!in_array($autoMatchField, $fields)) { + $this->printError( + sprintf('Das automatisch zu importierende Feld %s konnte nicht gefunden werden!', $autoMatchField) + ); + $mandatoryFieldsOk = false; + } + } + if (!$mandatoryFieldsOk) { + return; + } + + // Count the entries + try { + $entries = $source->getEntries(null); + } catch (ImportException $e) { + $this->printError(sprintf('Fehler beim Abfragen der Einsätze: %s', $e->getMessage())); + return; + } + + if (empty($entries)) { + $this->printWarning('Es wurden keine Einsätze gefunden.'); + return; + } + $this->printSuccess(sprintf("Es wurden %s Einsätze gefunden", count($entries))); + + // Felder matchen + echo "

      Felder zuordnen

      "; + $this->renderMatchForm($source, $currentStep, $nextStep); + } + /** * @param FileSource $source * @param Step $nextStep @@ -172,7 +236,7 @@ private function echoFileChooser(FileSource $source, Step $nextStep) } echo '
      '; - wp_nonce_field($this->currentSource->getNonce($nextStep)); + wp_nonce_field($source->getNonce($nextStep)); echo '
      '; foreach ($attachments as $attachment) { @@ -186,7 +250,7 @@ private function echoFileChooser(FileSource $source, Step $nextStep) $source->echoExtraFormFields(AbstractSource::STEP_CHOOSEFILE, $nextStep); - printf('', $this->currentSource->getActionAttribute($nextStep)); + printf('', $source->getActionAttribute($nextStep)); submit_button($nextStep->getButtonText()); echo ''; } @@ -199,4 +263,93 @@ private function loadSources() $csv = new Csv(); $this->sources[$csv->getIdentifier()] = $csv; } + + /** + * Gibt das Formular für die Zuordnung zwischen zu importieren Feldern und denen von Einsatzverwaltung aus + * + * @param AbstractSource $source + * @param Step $currentStep + * @param Step $nextStep + * @param array $mapping + */ + private function renderMatchForm(AbstractSource $source, Step $currentStep, Step $nextStep, array $mapping = []) + { + $fields = $source->getFields(); + + // If the incident numbers are managed automatically, don't offer to import them + $unmatchableFields = $source->getUnmatchableFields(); + if (ReportNumberController::isAutoIncidentNumbers()) { + $unmatchableFields[] = 'einsatz_incidentNumber'; + } + + echo '
      '; + wp_nonce_field($source->getNonce($nextStep)); + printf('', esc_attr($source->getActionAttribute($nextStep))); + echo ''; + foreach ($fields as $field) { + printf("'; + } + echo '
      '; + printf('Feld in %s', $source->getName()); + echo 'Feld in Einsatzverwaltung
      %s", esc_html($field)); + if (array_key_exists($field, $source->getAutoMatchFields())) { + echo 'wird automatisch zugeordnet'; + } elseif (in_array($field, $source->getProblematicFields())) { + $this->printWarning(sprintf('Probleme mit Feld %s, siehe Analyse', $field)); + } else { + $selected = '-'; + if (!empty($mapping) && array_key_exists($field, $mapping) && !empty($mapping[$field])) { + $selected = $mapping[$field]; + } + + $this->renderOwnFieldsDropdown($source->getInputName($field), $selected, $unmatchableFields); + } + echo '
      '; + if (!empty($nextStep)) { + $source->echoExtraFormFields($currentStep->getSlug(), $nextStep); + } + submit_button($nextStep->getButtonText()); + echo '
      '; + } + + /** + * Generates a select tag for selecting the available properties of reports + * + * @param string $name Name of the select tag + * @param string $selected Value of the selected option, defaults to '-' for 'do not import' + * @param array $fieldsToSkip Array of own field names that should be skipped during output + */ + private function renderOwnFieldsDropdown(string $name, string $selected = '-', array $fieldsToSkip = []) + { + $fields = IncidentReport::getFields(); + + // Remove fields that should not be presented as an option + foreach ($fieldsToSkip as $ownField) { + unset($fields[$ownField]); + } + + // Sortieren und ausgeben + uasort($fields, function ($field1, $field2) { + return strcmp($field1['label'], $field2['label']); + }); + $string = sprintf(''; + + echo $string; + } } diff --git a/src/Import/Sources/Csv.php b/src/Import/Sources/Csv.php index 52997507..91591fb2 100644 --- a/src/Import/Sources/Csv.php +++ b/src/Import/Sources/Csv.php @@ -48,7 +48,7 @@ public function checkPreconditions() $this->delimiter = $delimiter; } - $attachmentId = $this->args['csv_file_id']; + $attachmentId = $this->args['file_id']; if (empty($attachmentId)) { throw new ImportCheckException('Keine Datei ausgewählt'); } diff --git a/src/Import/Sources/WpEinsatz.php b/src/Import/Sources/WpEinsatz.php index c96b7a58..73f2900a 100644 --- a/src/Import/Sources/WpEinsatz.php +++ b/src/Import/Sources/WpEinsatz.php @@ -76,7 +76,7 @@ public function getEntries($fields = null) { global $wpdb; $queryFields = (null === $fields ? '*' : implode(array_merge(array('ID'), $fields), ',')); - $query = sprintf('SELECT %s FROM \'%s\' ORDER BY Datum', $queryFields, $this->tablename); + $query = sprintf('SELECT %s FROM `%s` ORDER BY `Datum`', $queryFields, $this->tablename); $entries = $wpdb->get_results($query, ARRAY_A); if ($entries === null) { @@ -109,7 +109,7 @@ public function getFields() global $wpdb; $fields = array(); - foreach ($wpdb->get_col("DESCRIBE '$this->tablename'", 0) as $columnName) { + foreach ($wpdb->get_col("DESCRIBE `$this->tablename`", 0) as $columnName) { // Unwichtiges ignorieren if ($columnName == 'ID' || $columnName == 'Nr_Jahr' || $columnName == 'Nr_Monat') { continue; diff --git a/src/Import/Tool.php b/src/Import/Tool.php index 86905c32..89d2ca71 100644 --- a/src/Import/Tool.php +++ b/src/Import/Tool.php @@ -29,70 +29,6 @@ class Tool */ private $utilities; - /** - * Generiert den Inhalt der Werkzeugseite - */ - public function renderToolPage() - { - $this->helper = new Helper($this->utilities, $this->data); - - // TODO gemeinsame Prüfungen auslagern - if ('analysis' == $aktion) { - $this->analysisPage(); - } elseif ('import' == $aktion) { - $this->importPage(); - } - } - - private function analysisPage() - { - if (!$this->currentSource->checkPreconditions()) { - return; - } - - $felder = $this->currentSource->getFields(); - if (empty($felder)) { - $this->utilities->printError('Es wurden keine Felder gefunden'); - return; - } - $this->utilities->printSuccess('Es wurden ' . count($felder) . ' Feld(er) gefunden: ' . implode($felder, ', ')); - - // Auf Pflichtfelder prüfen - $mandatoryFieldsOk = true; - foreach (array_keys($this->currentSource->getAutoMatchFields()) as $autoMatchField) { - if (!in_array($autoMatchField, $felder)) { - $this->utilities->printError( - sprintf('Das automatisch zu importierende Feld %s konnte nicht gefunden werden!', $autoMatchField) - ); - $mandatoryFieldsOk = false; - } - } - if (!$mandatoryFieldsOk) { - return; - } - - // Einsätze zählen - $entries = $this->currentSource->getEntries(null); - if (empty($entries)) { - $this->utilities->printWarning('Es wurden keine Einsätze gefunden.'); - return; - } - $this->utilities->printSuccess(sprintf("Es wurden %s Einsätze gefunden", count($entries))); - - if ('evw_wpe' == $this->currentSource->getIdentifier()) { - $this->printDataNotice(); - } - - // Felder matchen - echo "

      Felder zuordnen

      "; - - $this->helper->renderMatchForm($this->currentSource, array( - 'nonce_action' => $this->getNonceAction($this->currentSource, $this->nextAction['slug']), - 'action_value' => $this->currentSource->getActionAttribute($this->nextAction['slug']), - 'next_action' => $this->nextAction - )); - } - private function importPage() { if (!$this->currentSource->checkPreconditions()) { From 0a36deb4fc5ba35a0a6b6bad9be3a05f2df9c3e0 Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Tue, 1 Dec 2020 19:12:13 +0100 Subject: [PATCH 15/25] Clean up a bit --- src/Import/Sources/Csv.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Import/Sources/Csv.php b/src/Import/Sources/Csv.php index 91591fb2..b0699d3e 100644 --- a/src/Import/Sources/Csv.php +++ b/src/Import/Sources/Csv.php @@ -13,12 +13,12 @@ */ class Csv extends FileSource { - private $dateFormats = array('d.m.Y', 'd.m.y', 'Y-m-d', 'm/d/Y', 'm/d/y'); - private $timeFormats = array('H:i', 'G:i', 'H:i:s', 'G:i:s'); private $csvFilePath; + private $dateFormats = array('d.m.Y', 'd.m.y', 'Y-m-d', 'm/d/Y', 'm/d/y'); private $delimiter = ';'; private $enclosure = '"'; private $fileHasHeadlines = false; + private $timeFormats = array('H:i', 'G:i', 'H:i:s', 'G:i:s'); /** * Csv constructor. @@ -119,8 +119,7 @@ public function echoExtraFormFields(string $currentAction, Step $nextStep) public function getDateFormat() { if (!array_key_exists('import_date_format', $this->args)) { - $fallbackDateFormat = $this->dateFormats[0]; - return $fallbackDateFormat; + return $this->dateFormats[0]; } return $this->args['import_date_format']; @@ -183,8 +182,7 @@ public function getFields() public function getTimeFormat() { if (!array_key_exists('import_time_format', $this->args)) { - $fallbackTimeFormat = $this->timeFormats[0]; - return $fallbackTimeFormat; + return $this->timeFormats[0]; } return $this->args['import_time_format']; From 66e39f03a543cd5aab9602074dca6f3e2f8256f6 Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Wed, 2 Dec 2020 13:12:48 +0100 Subject: [PATCH 16/25] Make the CSV source use the new CsvReader class --- src/Import/Page.php | 25 ++++-- src/Import/Sources/AbstractSource.php | 8 +- src/Import/Sources/Csv.php | 117 +++++++++----------------- src/Import/Sources/WpEinsatz.php | 4 +- 4 files changed, 67 insertions(+), 87 deletions(-) diff --git a/src/Import/Page.php b/src/Import/Page.php index ed4cceab..2bd09d7c 100644 --- a/src/Import/Page.php +++ b/src/Import/Page.php @@ -162,7 +162,13 @@ private function echoAnalysis(AbstractSource $source, Step $currentStep, Step $n return; } - $fields = $source->getFields(); + try { + $fields = $source->getFields(); + } catch (ImportException $e) { + $this->printError('Fehler beim Abrufen der Felder'); + return; + } + if (empty($fields)) { $this->printError('Es wurden keine Felder gefunden'); return; @@ -190,7 +196,7 @@ private function echoAnalysis(AbstractSource $source, Step $currentStep, Step $n // Count the entries try { - $entries = $source->getEntries(null); + $entries = $source->getEntries(); } catch (ImportException $e) { $this->printError(sprintf('Fehler beim Abfragen der Einsätze: %s', $e->getMessage())); return; @@ -274,7 +280,12 @@ private function loadSources() */ private function renderMatchForm(AbstractSource $source, Step $currentStep, Step $nextStep, array $mapping = []) { - $fields = $source->getFields(); + try { + $fields = $source->getFields(); + } catch (ImportException $e) { + $this->printError('Fehler beim Abrufen der Felder'); + return; + } // If the incident numbers are managed automatically, don't offer to import them $unmatchableFields = $source->getUnmatchableFields(); @@ -300,7 +311,11 @@ private function renderMatchForm(AbstractSource $source, Step $currentStep, Step $selected = $mapping[$field]; } - $this->renderOwnFieldsDropdown($source->getInputName($field), $selected, $unmatchableFields); + try { + $this->renderOwnFieldsDropdown($source->getInputName($field), $selected, $unmatchableFields); + } catch (ImportException $e) { + echo 'ERROR'; + } } echo ''; } @@ -328,7 +343,7 @@ private function renderOwnFieldsDropdown(string $name, string $selected = '-', a unset($fields[$ownField]); } - // Sortieren und ausgeben + // Sort fields by name uasort($fields, function ($field1, $field2) { return strcmp($field1['label'], $field2['label']); }); diff --git a/src/Import/Sources/AbstractSource.php b/src/Import/Sources/AbstractSource.php index 5da2a8c2..91e8070c 100644 --- a/src/Import/Sources/AbstractSource.php +++ b/src/Import/Sources/AbstractSource.php @@ -142,16 +142,17 @@ abstract public function getDateFormat(); /** * Gibt die Einsatzberichte der Importquelle zurück * - * @param array $fields Felder der Importquelle, die abgefragt werden sollen. Ist dieser Parameter null, werden alle - * Felder abgefragt. + * @param string[] $requestedFields Names of the fields that should be queried from the source. Defaults to empty + * array, which requests all fields. * * @return array * @throws ImportException */ - abstract public function getEntries($fields); + abstract public function getEntries(array $requestedFields = []); /** * @return array + * @throws ImportException */ abstract public function getFields(); @@ -185,6 +186,7 @@ public function getIdentifier() * @param string $field Bezeichner des Felds * * @return string Eindeutiger Name bestehend aus Bezeichnern der Importquelle und des Felds + * @throws ImportException */ public function getInputName(string $field) { diff --git a/src/Import/Sources/Csv.php b/src/Import/Sources/Csv.php index b0699d3e..16209a27 100644 --- a/src/Import/Sources/Csv.php +++ b/src/Import/Sources/Csv.php @@ -1,12 +1,18 @@ readFile(0); - } catch (Exception $e) { + $csvReader = new CsvReader($csvFilePath, $this->delimiter, $this->enclosure); + $csvReader->getLines(1); + } catch (FileReadException $e) { throw new ImportCheckException($e->getMessage()); } } @@ -126,16 +133,25 @@ public function getDateFormat() } /** - * Gibt die Einsatzberichte der Importquelle zurück - * - * @param array $fields Felder der Importquelle, die abgefragt werden sollen. Ist dieser Parameter null, werden alle - * Felder abgefragt. - * - * @return array|bool + * @inheritDoc */ - public function getEntries($fields) + public function getEntries(array $requestedFields = []) { - $lines = $this->readFile(null, $fields); + $columns = []; + if (!empty($requestedFields)) { + $fields = $this->getFields(); + foreach ($requestedFields as $requestedField) { + $columns[] = array_search($requestedField, $fields); + } + error_log('Requested fields: '.print_r($requestedFields, true).', columns: '.print_r($columns, true)); + } + + $csvReader = new CsvReader($this->csvFilePath, $this->delimiter, $this->enclosure); + try { + $lines = $csvReader->getLines(0, $columns); + } catch (FileReadException $e) { + throw new ImportException($e->getMessage()); + } if (empty($lines)) { return false; @@ -149,7 +165,7 @@ public function getEntries($fields) } /** - * @return array + * @inheritDoc */ public function getFields() { @@ -157,23 +173,28 @@ public function getFields() return $this->cachedFields; } - $fields = $this->readFile(1); + $csvReader = new CsvReader($this->csvFilePath, $this->delimiter, $this->enclosure); + try { + $lines = $csvReader->getLines(1); + $fields = $lines[0]; + } catch (FileReadException $e) { + throw new ImportException($e->getMessage()); + } if (empty($fields)) { return array(); } - // Gebe nummerierte Spalten zurück, wenn es keine Überschriften gibt + // If the first line does not contain the column names, return names like Coulumn 1, Column 2, ... if (!$this->fileHasHeadlines) { return array_map(function ($number) { - return sprintf('Spalte %d', $number); - }, range(1, count($fields[0]))); + return sprintf(__('Column %d', 'einsatzverwaltung'), $number); + }, range(1, count($fields))); } - $this->cachedFields = $fields[0]; + $this->cachedFields = $fields; - // Gebe die Überschriften der Spalten zurück - return $fields[0]; + return $fields; } /** @@ -187,62 +208,4 @@ public function getTimeFormat() return $this->args['import_time_format']; } - - /** - * @param int|null $numLines Maximale Anzahl zu lesender Zeilen, oder null um alle Zeilen einzulesen - * @param array $requestedFields - * - * @return array|bool - * @throws Exception - */ - private function readFile($numLines = null, $requestedFields = array()) - { - $fieldMap = array(); - if (!empty($requestedFields)) { - $fields = $this->getFields(); - foreach ($requestedFields as $requestedField) { - $fieldMap[$requestedField] = array_search($requestedField, $fields); - } - } - - $handle = fopen($this->csvFilePath, 'r'); - if (empty($handle)) { - throw new Exception('Konnte Datei nicht öffnen'); - } - - if ($numLines === 0) { - fclose($handle); - return array(); - } - - $lines = array(); - while (null === $numLines || count($lines) < $numLines) { - $line = fgetcsv($handle, 0, $this->delimiter, $this->enclosure); - - // Problem beim Lesen oder Ende der Datei - if (empty($line)) { - break; - } - - // Leere Zeile - if (is_array($line) && $line[0] == null) { - continue; - } - - if (empty($requestedFields)) { - $lines[] = $line; - continue; - } - - $filteredLine = array(); - foreach ($fieldMap as $fieldName => $index) { - // Fehlende Felder in zu kurzen Zeilen werden als leer gewertet - $filteredLine[$fieldName] = array_key_exists($index, $line) ? $line[$index] : ''; - } - $lines[] = $filteredLine; - } - - fclose($handle); - return $lines; - } } diff --git a/src/Import/Sources/WpEinsatz.php b/src/Import/Sources/WpEinsatz.php index 73f2900a..362e159a 100644 --- a/src/Import/Sources/WpEinsatz.php +++ b/src/Import/Sources/WpEinsatz.php @@ -72,10 +72,10 @@ public function getDateFormat() /** * @inheritDoc */ - public function getEntries($fields = null) + public function getEntries(array $requestedFields = []) { global $wpdb; - $queryFields = (null === $fields ? '*' : implode(array_merge(array('ID'), $fields), ',')); + $queryFields = (empty($requestedFields) ? '*' : implode(array_merge(array('ID'), $requestedFields), ',')); $query = sprintf('SELECT %s FROM `%s` ORDER BY `Datum`', $queryFields, $this->tablename); $entries = $wpdb->get_results($query, ARRAY_A); From ba889ed634256997286b68e96b0afb6388c3fa1e Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Wed, 2 Dec 2020 13:15:54 +0100 Subject: [PATCH 17/25] Fix the tests --- tests/unit/Import/Sources/CsvTest.php | 4 ++-- tests/unit/Import/Sources/WpEinsatzTest.php | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/Import/Sources/CsvTest.php b/tests/unit/Import/Sources/CsvTest.php index ad2d963e..acdde3bd 100644 --- a/tests/unit/Import/Sources/CsvTest.php +++ b/tests/unit/Import/Sources/CsvTest.php @@ -1,7 +1,7 @@ expects()->get_var(Mockery::type('string'))->andReturn('wpunit_einsaetze'); - $wpdb->expects()->get_col("DESCRIBE 'wpunit_einsaetze'", 0)->andReturn(['Datum', 'Örtlichkeit', 'Einsatz#']); + $wpdb->expects()->get_col("DESCRIBE `wpunit_einsaetze`", 0)->andReturn(['Datum', 'Örtlichkeit', 'Einsatz#']); $this->expectException(ImportCheckException::class); $source = new WpEinsatz(); @@ -72,7 +72,7 @@ public function testCheckShouldPassIfConditionsAreMet() // Pretend that the table exists and return some good column names $wpdb->expects()->get_var("SHOW TABLES LIKE 'wpunit_einsaetze'")->andReturn('wpunit_einsaetze'); - $wpdb->expects()->get_col("DESCRIBE 'wpunit_einsaetze'", 0)->andReturn(['Datum', 'Ort', 'Art', 'Einsatztext']); + $wpdb->expects()->get_col("DESCRIBE `wpunit_einsaetze`", 0)->andReturn(['Datum', 'Ort', 'Art', 'Einsatztext']); $source = new WpEinsatz(); try { @@ -88,7 +88,7 @@ public function testCanGetFieldNames() global $wpdb; // Return some column names - $wpdb->expects()->get_col("DESCRIBE 'wpunit_einsaetze'", 0)->andReturn(['ID', 'Nr_Jahr', 'Nr_Monat', 'Datum', 'Ort']); + $wpdb->expects()->get_col("DESCRIBE `wpunit_einsaetze`", 0)->andReturn(['ID', 'Nr_Jahr', 'Nr_Monat', 'Datum', 'Ort']); $source = new WpEinsatz(); $this->assertEqualSets(['Datum', 'Ort'], $source->getFields()); @@ -100,7 +100,7 @@ public function testFieldsGetCached() global $wpdb; // Return some column names - $wpdb->expects()->get_col("DESCRIBE 'wpunit_einsaetze'", 0)->andReturn(['ID', 'Nr_Jahr', 'Nr_Monat', 'Datum', 'Ort']); + $wpdb->expects()->get_col("DESCRIBE `wpunit_einsaetze`", 0)->andReturn(['ID', 'Nr_Jahr', 'Nr_Monat', 'Datum', 'Ort']); $source = new WpEinsatz(); $this->assertEqualSets(['Datum', 'Ort'], $source->getFields()); @@ -120,7 +120,7 @@ public function testGetsEntriesForAllFields() ['ID' => 2, 'colA' => 'value4', 'colB' => 'value5', 'colC' => 'value6'], ['ID' => 3, 'colA' => 'value7', 'colB' => 'value8', 'colC' => 'value9'] ]; - $wpdb->expects()->get_results("SELECT * FROM 'wpunit_einsaetze' ORDER BY Datum", ARRAY_A)->andReturn($entries); + $wpdb->expects()->get_results("SELECT * FROM `wpunit_einsaetze` ORDER BY `Datum`", ARRAY_A)->andReturn($entries); $source = new WpEinsatz(); try { @@ -142,7 +142,7 @@ public function testGetsEntriesForCertainFields() ['ID' => 3, 'colA' => 'value5', 'colC' => 'value6'] ]; $wpdb->expects() - ->get_results("SELECT ID,colA,colC FROM 'wpunit_einsaetze' ORDER BY Datum", ARRAY_A) + ->get_results("SELECT ID,colA,colC FROM `wpunit_einsaetze` ORDER BY `Datum`", ARRAY_A) ->andReturn($entries); $source = new WpEinsatz(); From f31f51e8a231db59ff3a2f085b936ea5b86f3a9e Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Wed, 2 Dec 2020 15:20:01 +0100 Subject: [PATCH 18/25] Generate and check the mapping --- src/Import/Helper.php | 49 +------------- src/Import/MappingHelper.php | 97 +++++++++++++++++++++++++++ src/Import/Page.php | 43 ++++++++++-- src/Import/Sources/AbstractSource.php | 33 +-------- src/Import/Sources/Csv.php | 2 +- src/Import/Tool.php | 78 --------------------- 6 files changed, 140 insertions(+), 162 deletions(-) create mode 100644 src/Import/MappingHelper.php delete mode 100644 src/Import/Tool.php diff --git a/src/Import/Helper.php b/src/Import/Helper.php index 9f16e5c3..086d7b76 100644 --- a/src/Import/Helper.php +++ b/src/Import/Helper.php @@ -6,7 +6,6 @@ use abrain\Einsatzverwaltung\Exceptions\ImportPreparationException; use abrain\Einsatzverwaltung\Import\Sources\AbstractSource; use abrain\Einsatzverwaltung\Model\IncidentReport; -use abrain\Einsatzverwaltung\ReportNumberController; use abrain\Einsatzverwaltung\Utilities; use DateTime; @@ -264,6 +263,7 @@ public function sanitizeBooleanValues($value) * @param array $mapping * @param array $preparedInsertArgs * @param array $yearsAffected + * @throws ImportException * @throws ImportPreparationException */ public function prepareImport($source, $mapping, &$preparedInsertArgs, &$yearsAffected) @@ -334,51 +334,4 @@ public function runImport($preparedInsertArgs, $source, $yearsAffected, $importS } } } - - /** - * Prüft, ob das Mapping stimmig ist und gibt Warnungen oder Fehlermeldungen aus - * - * @param array $mapping Das zu prüfende Mapping - * @param AbstractSource $source - * - * @return bool True bei bestandener Prüfung, false bei Unstimmigkeiten - */ - public function validateMapping($mapping, $source) - { - $valid = true; - - // Pflichtfelder prüfen - if (!in_array('post_date', $mapping)) { - $this->utilities->printError('Pflichtfeld Alarmzeit wurde nicht zugeordnet'); - $valid = false; - } - - $unmatchableFields = $source->getUnmatchableFields(); - $autoMatchFields = $source->getAutoMatchFields(); - if (ReportNumberController::isAutoIncidentNumbers()) { - $unmatchableFields[] = 'einsatz_incidentNumber'; - } - foreach ($unmatchableFields as $unmatchableField) { - if (in_array($unmatchableField, $mapping) && !in_array($unmatchableField, $autoMatchFields)) { - $this->utilities->printError(sprintf( - 'Feld %s kann nicht für ein zu importierendes Feld als Ziel angegeben werden', - esc_html($unmatchableField) - )); - $valid = false; - } - } - - // Mehrfache Zuweisungen prüfen - foreach (array_count_values($mapping) as $ownField => $count) { - if ($count > 1) { - $this->utilities->printError(sprintf( - 'Feld %s kann nicht für mehr als ein zu importierendes Feld als Ziel angegeben werden', - IncidentReport::getFieldLabel($ownField) - )); - $valid = false; - } - } - - return $valid; - } } diff --git a/src/Import/MappingHelper.php b/src/Import/MappingHelper.php new file mode 100644 index 00000000..33ad84f6 --- /dev/null +++ b/src/Import/MappingHelper.php @@ -0,0 +1,97 @@ +getFields() as $sourceField) { + $ownField = filter_input(INPUT_POST, $source->getInputName($sourceField), FILTER_SANITIZE_STRING); + + // Skip source fields that are not mapped to a field + if (empty($ownField) || !is_string($ownField) || $ownField === '-') { + continue; + } + + if (!array_key_exists($ownField, $ownFields)) { + throw new ImportCheckException(sprintf(__('Unknown field: %s', 'einsatzverwaltung'), $ownField)); + } + + $mapping[$sourceField] = $ownField; + } + + // The source may give a mandatory mapping for certain fields + foreach ($source->getAutoMatchFields() as $sourceFieldAuto => $ownFieldAuto) { + $mapping[$sourceFieldAuto] = $ownFieldAuto; + } + + return $mapping; + } + + /** + * Prüft, ob das Mapping stimmig ist und gibt Warnungen oder Fehlermeldungen aus + * + * @param array $mapping Das zu prüfende Mapping + * @param AbstractSource $source + * + * @throws ImportCheckException + */ + public function validateMapping(array $mapping, AbstractSource $source) + { + // Pflichtfelder prüfen + if (!in_array('post_date', $mapping)) { + throw new ImportCheckException('Pflichtfeld Alarmzeit wurde nicht zugeordnet'); + } + + $unmatchableFields = $source->getUnmatchableFields(); + $autoMatchFields = $source->getAutoMatchFields(); + if (ReportNumberController::isAutoIncidentNumbers()) { + $unmatchableFields[] = 'einsatz_incidentNumber'; + } + foreach ($unmatchableFields as $unmatchableField) { + if (in_array($unmatchableField, $mapping) && !in_array($unmatchableField, $autoMatchFields)) { + throw new ImportCheckException(sprintf( + 'Feld %s kann nicht für ein zu importierendes Feld als Ziel angegeben werden', + esc_html($unmatchableField) + )); + } + } + + // Mehrfache Zuweisungen prüfen + foreach (array_count_values($mapping) as $ownField => $count) { + if ($count > 1) { + throw new ImportCheckException(sprintf( + 'Feld %s kann nicht für mehr als ein zu importierendes Feld als Ziel angegeben werden', + IncidentReport::getFieldLabel($ownField) + )); + } + } + } +} diff --git a/src/Import/Page.php b/src/Import/Page.php index 2bd09d7c..64c99886 100644 --- a/src/Import/Page.php +++ b/src/Import/Page.php @@ -141,7 +141,7 @@ protected function echoPageContent() $this->echoFileChooser($currentSource, $nextStep); break; case AbstractSource::STEP_IMPORT: - echo "Import..."; + $this->echoImport($currentSource, $currentStep); break; default: $this->printError(sprintf('Action %s is unknown', esc_html($action))); @@ -164,7 +164,7 @@ private function echoAnalysis(AbstractSource $source, Step $currentStep, Step $n try { $fields = $source->getFields(); - } catch (ImportException $e) { + } catch (ImportCheckException $e) { $this->printError('Fehler beim Abrufen der Felder'); return; } @@ -261,6 +261,41 @@ private function echoFileChooser(FileSource $source, Step $nextStep) echo ''; } + /** + * @param AbstractSource $source + * @param Step $currentStep + */ + private function echoImport(AbstractSource $source, Step $currentStep) + { + try { + $source->checkPreconditions(); + } catch (ImportCheckException $e) { + $this->printError(sprintf('Voraussetzung nicht erfüllt: %s', $e->getMessage())); + return; + } + + // Get the mapping of the source fields to our internal fields + $mappingHelper = new MappingHelper(); + try { + $mapping = $mappingHelper->getMapping($source, IncidentReport::getFields()); + $mappingHelper->validateMapping($mapping, $source); + } catch (ImportCheckException $e) { + $this->printError(sprintf("Fehler bei der Zuordnung: %s", $e->getMessage())); + + // Repeat the mapping + $this->renderMatchForm($source, $currentStep, $currentStep, empty($mapping) ? [] : $mapping); + return; + } + + // Start the import + echo '

      Die Daten werden eingelesen, das kann einen Moment dauern.

      '; + // TODO do the import + + $this->printSuccess('Der Import ist abgeschlossen'); + $url = admin_url('edit.php?post_type=einsatz'); + printf('Zu den Einsatzberichten', $url); + } + private function loadSources() { $wpEinsatz = new WpEinsatz(); @@ -282,7 +317,7 @@ private function renderMatchForm(AbstractSource $source, Step $currentStep, Step { try { $fields = $source->getFields(); - } catch (ImportException $e) { + } catch (ImportCheckException $e) { $this->printError('Fehler beim Abrufen der Felder'); return; } @@ -313,7 +348,7 @@ private function renderMatchForm(AbstractSource $source, Step $currentStep, Step try { $this->renderOwnFieldsDropdown($source->getInputName($field), $selected, $unmatchableFields); - } catch (ImportException $e) { + } catch (ImportCheckException $e) { echo 'ERROR'; } } diff --git a/src/Import/Sources/AbstractSource.php b/src/Import/Sources/AbstractSource.php index 91e8070c..71011eaa 100644 --- a/src/Import/Sources/AbstractSource.php +++ b/src/Import/Sources/AbstractSource.php @@ -152,7 +152,7 @@ abstract public function getEntries(array $requestedFields = []); /** * @return array - * @throws ImportException + * @throws ImportCheckException */ abstract public function getFields(); @@ -186,7 +186,7 @@ public function getIdentifier() * @param string $field Bezeichner des Felds * * @return string Eindeutiger Name bestehend aus Bezeichnern der Importquelle und des Felds - * @throws ImportException + * @throws ImportCheckException */ public function getInputName(string $field) { @@ -194,35 +194,6 @@ public function getInputName(string $field) return $this->getIdentifier() . '-field' . $fieldId; } - /** - * @param array $sourceFields Felder der Importquelle - * @param array $ownFields Felder der Einsatzverwaltung - * - * @return array - * @throws ImportException - */ - public function getMapping($sourceFields, $ownFields) - { - $mapping = array(); - foreach ($sourceFields as $sourceField) { - $index = $this->getInputName($sourceField); - if (array_key_exists($index, $_POST)) { - $ownField = $_POST[$index]; - if (!empty($ownField) && is_string($ownField) && $ownField != '-') { - if (array_key_exists($ownField, $ownFields)) { - $mapping[$sourceField] = $ownField; - } else { - throw new ImportException(sprintf(__('Unknown field: %s', 'einsatzverwaltung'), $ownField)); - } - } - } - } - foreach ($this->autoMatchFields as $sourceFieldAuto => $ownFieldAuto) { - $mapping[$sourceFieldAuto] = $ownFieldAuto; - } - return $mapping; - } - /** * Gibt den Namen der Importquelle zurück * diff --git a/src/Import/Sources/Csv.php b/src/Import/Sources/Csv.php index 16209a27..b5b11c2c 100644 --- a/src/Import/Sources/Csv.php +++ b/src/Import/Sources/Csv.php @@ -178,7 +178,7 @@ public function getFields() $lines = $csvReader->getLines(1); $fields = $lines[0]; } catch (FileReadException $e) { - throw new ImportException($e->getMessage()); + throw new ImportCheckException($e->getMessage()); } if (empty($fields)) { diff --git a/src/Import/Tool.php b/src/Import/Tool.php deleted file mode 100644 index 89d2ca71..00000000 --- a/src/Import/Tool.php +++ /dev/null @@ -1,78 +0,0 @@ -currentSource->checkPreconditions()) { - return; - } - - $sourceFields = $this->currentSource->getFields(); - if (empty($sourceFields)) { - $this->utilities->printError('Es wurden keine Felder gefunden'); - return; - } - - // Mapping einlesen - $mapping = $this->currentSource->getMapping($sourceFields, IncidentReport::getFields()); - - // Prüfen, ob mehrere Felder das gleiche Zielfeld haben - if (!$this->helper->validateMapping($mapping, $this->currentSource)) { - // Und gleich nochmal... - $this->nextAction = $this->currentAction; - - $this->helper->renderMatchForm($this->currentSource, array( - 'mapping' => $mapping, - 'nonce_action' => $this->getNonceAction($this->currentSource, $this->nextAction['slug']), - 'action_value' => $this->currentSource->getActionAttribute($this->nextAction['slug']), - 'next_action' => $this->nextAction - )); - return; - } - - // Import starten - echo '

      Die Daten werden eingelesen, das kann einen Moment dauern.

      '; - $importStatus = new ImportStatus($this->utilities, 0); - try { - $this->helper->import($this->currentSource, $mapping, $importStatus); - } catch (ImportException $e) { - $importStatus->abort('Import abgebrochen, Ursache: ' . $e->getMessage()); - return; - } catch (ImportPreparationException $e) { - $importStatus->abort('Importvorbereitung abgebrochen, Ursache: ' . $e->getMessage()); - return; - } - - $this->utilities->printSuccess('Der Import ist abgeschlossen'); - $url = admin_url('edit.php?post_type=einsatz'); - printf('Zu den Einsatzberichten', $url); - } -} From 04ed3f3f4b9500cae11dd84e506f9d4021265c4e Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Fri, 29 Jan 2021 18:33:45 +0100 Subject: [PATCH 19/25] Fix deprecated use of the implode function --- src/Import/Page.php | 2 +- src/Import/Sources/WpEinsatz.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Import/Page.php b/src/Import/Page.php index 64c99886..de8af213 100644 --- a/src/Import/Page.php +++ b/src/Import/Page.php @@ -177,7 +177,7 @@ private function echoAnalysis(AbstractSource $source, Step $currentStep, Step $n $this->printSuccess(sprintf( _n('Found %1$d field: %2$s', 'Found %1$d fields: %2$s', $numberOfFields, 'einsatzverwaltung'), $numberOfFields, - esc_html(implode($fields, ', ')) + esc_html(implode(', ', $fields)) )); // Check for mandatory fields diff --git a/src/Import/Sources/WpEinsatz.php b/src/Import/Sources/WpEinsatz.php index 362e159a..ff3051e6 100644 --- a/src/Import/Sources/WpEinsatz.php +++ b/src/Import/Sources/WpEinsatz.php @@ -75,7 +75,7 @@ public function getDateFormat() public function getEntries(array $requestedFields = []) { global $wpdb; - $queryFields = (empty($requestedFields) ? '*' : implode(array_merge(array('ID'), $requestedFields), ',')); + $queryFields = (empty($requestedFields) ? '*' : implode(',', array_merge(array('ID'), $requestedFields))); $query = sprintf('SELECT %s FROM `%s` ORDER BY `Datum`', $queryFields, $this->tablename); $entries = $wpdb->get_results($query, ARRAY_A); From 12287fb562320ed1d0f87cdbbcab7523e19f4c08 Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Sat, 6 Feb 2021 13:10:54 +0100 Subject: [PATCH 20/25] Make a tiny change, to trigger a new build of the PR and check the results on Code Climate --- src/Import/MappingHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Import/MappingHelper.php b/src/Import/MappingHelper.php index 33ad84f6..c4193c36 100644 --- a/src/Import/MappingHelper.php +++ b/src/Import/MappingHelper.php @@ -28,7 +28,7 @@ class MappingHelper * @return array * @throws ImportCheckException */ - public function getMapping(AbstractSource $source, array $ownFields) + public function getMapping(AbstractSource $source, array $ownFields): array { $mapping = []; From fd3844957e5e218805e69d2ab7250d394931ec1b Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Sat, 6 Feb 2021 15:34:00 +0100 Subject: [PATCH 21/25] No need for bash --- bin/check-branch-name.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/check-branch-name.sh b/bin/check-branch-name.sh index 2a8a7504..2d05ec79 100755 --- a/bin/check-branch-name.sh +++ b/bin/check-branch-name.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh if [ "$DRONE_BUILD_EVENT" == "pull_request" ]; then case "$DRONE_SOURCE_BRANCH" in From 255c8e0f679528cdc88af0487c64129770911895 Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Sat, 6 Feb 2021 16:15:16 +0100 Subject: [PATCH 22/25] Send the test report in the same script, or the env variable is no longer set --- .drone.yml | 3 +-- bin/{determine-cc-branch.sh => report-code-coverage.sh} | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) rename bin/{determine-cc-branch.sh => report-code-coverage.sh} (71%) diff --git a/.drone.yml b/.drone.yml index 8db056ba..8ca00ef3 100644 --- a/.drone.yml +++ b/.drone.yml @@ -31,8 +31,7 @@ steps: - chmod +x ./cc-test-reporter - ./cc-test-reporter before-build - XDEBUG_MODE=coverage ./vendor/bin/phpunit -c phpunit.xml - - ./bin/determine-cc-branch.sh - - ./cc-test-reporter after-build --debug --coverage-input-type clover --exit-code $? + - ./bin/report-code-coverage.sh - name: slack image: plugins/slack settings: diff --git a/bin/determine-cc-branch.sh b/bin/report-code-coverage.sh similarity index 71% rename from bin/determine-cc-branch.sh rename to bin/report-code-coverage.sh index 54c2cc23..3461ef6a 100755 --- a/bin/determine-cc-branch.sh +++ b/bin/report-code-coverage.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +# The root directory of the project is one up +cd "$(dirname "$0")/.." + # Set the environment variable GIT_BRANCH for the Code Climate test-reporter, so it does not report the coverage for the # target branch of pull requests but for the source branch if [ "$DRONE_BUILD_EVENT" == "pull_request" ]; then @@ -7,3 +10,5 @@ if [ "$DRONE_BUILD_EVENT" == "pull_request" ]; then else export GIT_BRANCH="$DRONE_COMMIT_BRANCH" fi + +./cc-test-reporter after-build --debug --coverage-input-type clover \ No newline at end of file From 0f0fc2a165ccfc34712cf73c7d79ae9ebad4beb2 Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Sat, 24 Aug 2024 21:57:49 +0200 Subject: [PATCH 23/25] Fix composer.json --- composer.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/composer.json b/composer.json index 445ec4e8..4fe08d43 100644 --- a/composer.json +++ b/composer.json @@ -19,12 +19,6 @@ "source": "https://github.com/abrain/einsatzverwaltung", "docs": "https://einsatzverwaltung.org/dokumentation/" }, - "scripts": { - "test": [ - "Composer\\Config::disableProcessTimeout", - "phpunit" - ] - }, "config": { "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true From 4a3a7363ffdd4387e8a9da884dcdb672f25fc84a Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Sat, 24 Aug 2024 22:06:55 +0200 Subject: [PATCH 24/25] Add missing translators comments --- src/Import/CsvReader.php | 1 + src/Import/MappingHelper.php | 1 + src/Import/Page.php | 2 ++ src/Import/Sources/Csv.php | 1 + src/Import/Sources/WpEinsatz.php | 1 + 5 files changed, 6 insertions(+) diff --git a/src/Import/CsvReader.php b/src/Import/CsvReader.php index e5194f98..a913c4bc 100644 --- a/src/Import/CsvReader.php +++ b/src/Import/CsvReader.php @@ -57,6 +57,7 @@ public function getLines(int $numLines, array $columns = [], int $offset = 0): a { $handle = fopen($this->filePath, 'r'); if ($handle === false) { + // translators: 1: file path $message = sprintf(__('Could not open file %s', 'einsatzverwaltung'), $this->filePath); throw new FileReadException($message); } diff --git a/src/Import/MappingHelper.php b/src/Import/MappingHelper.php index c4193c36..3cd2d73a 100644 --- a/src/Import/MappingHelper.php +++ b/src/Import/MappingHelper.php @@ -41,6 +41,7 @@ public function getMapping(AbstractSource $source, array $ownFields): array } if (!array_key_exists($ownField, $ownFields)) { + // translators: 1: field name throw new ImportCheckException(sprintf(__('Unknown field: %s', 'einsatzverwaltung'), $ownField)); } diff --git a/src/Import/Page.php b/src/Import/Page.php index de8af213..a0f1bf1e 100644 --- a/src/Import/Page.php +++ b/src/Import/Page.php @@ -175,6 +175,7 @@ private function echoAnalysis(AbstractSource $source, Step $currentStep, Step $n } $numberOfFields = count($fields); $this->printSuccess(sprintf( + // translators: 1: number of fields, 2: comma-separated list of field names _n('Found %1$d field: %2$s', 'Found %1$d fields: %2$s', $numberOfFields, 'einsatzverwaltung'), $numberOfFields, esc_html(implode(', ', $fields)) @@ -237,6 +238,7 @@ private function echoFileChooser(FileSource $source, Step $nextStep) )); if (empty($attachments)) { + // translators: 1: MIME type $this->printInfo(sprintf(__('No files of type %s found.', 'einsatzverwaltung'), $mimeType)); return; } diff --git a/src/Import/Sources/Csv.php b/src/Import/Sources/Csv.php index b5b11c2c..ac799f33 100644 --- a/src/Import/Sources/Csv.php +++ b/src/Import/Sources/Csv.php @@ -188,6 +188,7 @@ public function getFields() // If the first line does not contain the column names, return names like Coulumn 1, Column 2, ... if (!$this->fileHasHeadlines) { return array_map(function ($number) { + // translators: 1: column number return sprintf(__('Column %d', 'einsatzverwaltung'), $number); }, range(1, count($fields))); } diff --git a/src/Import/Sources/WpEinsatz.php b/src/Import/Sources/WpEinsatz.php index ff3051e6..20343709 100644 --- a/src/Import/Sources/WpEinsatz.php +++ b/src/Import/Sources/WpEinsatz.php @@ -55,6 +55,7 @@ public function checkPreconditions() } if (!empty($this->problematicFields)) { throw new ImportCheckException(sprintf( + // translators: 1: comma-separated list of field names __('One or more fields have a special character in their name. This can become a problem during the import. Please rename the following fields in the settings of wp-einsatz: %s', 'einsatzverwaltung'), esc_html(join(', ', $this->problematicFields)) )); From e61630e03279c0523a5bc608f0e5bc311a407b4f Mon Sep 17 00:00:00 2001 From: Andreas Brain Date: Sun, 25 Aug 2024 15:51:26 +0200 Subject: [PATCH 25/25] Improve CSV reader error handling and tests --- patchwork.json | 6 +++++ src/Import/CsvReader.php | 20 +++++++++------ tests/unit/Import/CsvReaderTest.php | 39 +++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 patchwork.json diff --git a/patchwork.json b/patchwork.json new file mode 100644 index 00000000..79951553 --- /dev/null +++ b/patchwork.json @@ -0,0 +1,6 @@ +{ + "redefinable-internals": [ + "feof", + "fgetcsv" + ] +} \ No newline at end of file diff --git a/src/Import/CsvReader.php b/src/Import/CsvReader.php index a913c4bc..1af28a15 100644 --- a/src/Import/CsvReader.php +++ b/src/Import/CsvReader.php @@ -88,20 +88,16 @@ private function readLines($handle, int $numLines, array $columns): array $lines = array(); while ($numLines === 0 || $linesRead < $numLines) { $line = fgetcsv($handle, 0, $this->delimiter, $this->enclosure); - $linesRead++; - // End of file reached? - if ($line === false && feof($handle)) { + // Error while reading, most likely EOF + if ($line === false) { break; } - // Problem while reading the file - if (empty($line)) { - throw new FileReadException(); - } + $linesRead++; // Empty line in the file, skip this - if (is_array($line) && $line[0] == null) { + if ($line == [null]) { continue; } @@ -120,6 +116,14 @@ private function readLines($handle, int $numLines, array $columns): array $lines[] = $filteredLine; } + if (($numLines === 0 || $linesRead < $numLines) && feof($handle) === false) { + throw new FileReadException(sprintf( + // translators: 1: number of lines + _n('Reading was aborted after %d line', 'Reading was aborted after %d lines', $linesRead, 'einsatzverwaltung'), + $linesRead + )); + } + return $lines; } } diff --git a/tests/unit/Import/CsvReaderTest.php b/tests/unit/Import/CsvReaderTest.php index 4ecc4f9e..a9d50a4b 100644 --- a/tests/unit/Import/CsvReaderTest.php +++ b/tests/unit/Import/CsvReaderTest.php @@ -3,6 +3,7 @@ use abrain\Einsatzverwaltung\Exceptions\FileReadException; use abrain\Einsatzverwaltung\UnitTestCase; +use function Brain\Monkey\Functions\when; /** * Class CsvReaderTest @@ -68,4 +69,42 @@ public function testFillsNotExistingColumns() ['9f0NPAB0HU', ''] ], $lines); } + + public function testThrowsWhenReadingTooFewLines() + { + $this->expectException(FileReadException::class); + $this->expectExceptionMessage('2 lines'); + $csvReader = new CsvReader(__DIR__ . '/reports.csv', ';', '"'); + + $counter = 0; + when('fgetcsv')->alias(function () use (&$counter) { + if ($counter++ > 1) { + return false; + } + return ['lorem', 'ipsum']; + }); + when('feof')->justReturn(false); + + // Suppress the warning, otherwise PHPUnit would convert it to an exception + @$csvReader->getLines(3); + } + + public function testThrowsWhenStoppingBeforeEndOfFile() + { + $this->expectException(FileReadException::class); + $this->expectExceptionMessage('1 line'); + $csvReader = new CsvReader(__DIR__ . '/reports.csv', ';', '"'); + + $counter = 0; + when('fgetcsv')->alias(function () use (&$counter) { + if ($counter++ > 0) { + return false; + } + return ['lorem', 'ipsum']; + }); + when('feof')->justReturn(false); + + // Suppress the warning, otherwise PHPUnit would convert it to an exception + @$csvReader->getLines(0); + } }