]> git.agnieray.net Git - galette.git/blob - tests/Galette/IO/tests/units/CsvIn.php
Import dynamic fields from CSV; closes #940
[galette.git] / tests / Galette / IO / tests / units / CsvIn.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * CsvIn tests
7 *
8 * PHP version 5
9 *
10 * Copyright © 2020 The Galette Team
11 *
12 * This file is part of Galette (http://galette.tuxfamily.org).
13 *
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
18 *
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
26 *
27 * @category Core
28 * @package GaletteTests
29 *
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2020 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @version SVN: $Id$
34 * @link http://galette.tuxfamily.org
35 * @since 2020-05-11
36 */
37
38 namespace Galette\IO\test\units;
39
40 use \atoum;
41
42 use \Galette\Entity\Adherent;
43 use \Galette\DynamicFields\DynamicField;
44
45 /**
46 * CsvIn tests class
47 *
48 * @category Core
49 * @name CsvIn
50 * @package GaletteTests
51 * @author Johan Cwiklinski <johan@x-tnd.be>
52 * @copyright 2020 The Galette Team
53 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
54 * @link http://galette.tuxfamily.org
55 * @since 2020-05-11
56 */
57 class CsvIn extends atoum
58 {
59 private $zdb;
60 private $i18n;
61 private $preferences;
62 private $session;
63 private $login;
64 private $view;
65 private $history;
66 private $members_fields;
67 private $members_fields_cats;
68 private $flash;
69 private $flash_data;
70 private $container;
71 private $request;
72 private $response;
73 private $mocked_router;
74
75 /**
76 * Set up tests
77 *
78 * @param string $testMethod Calling method
79 *
80 * @return void
81 */
82 public function beforeTestMethod($testMethod)
83 {
84 $this->mocked_router = new \mock\Slim\Router();
85 $this->calling($this->mocked_router)->pathFor = function ($name, $params) {
86 return $name;
87 };
88 $this->zdb = new \Galette\Core\Db();
89 $this->i18n = new \Galette\Core\I18n(
90 \Galette\Core\I18n::DEFAULT_LANG
91 );
92 $this->preferences = new \Galette\Core\Preferences(
93 $this->zdb
94 );
95 $this->session = new \RKA\Session();
96 session_start();
97 $this->login = new \Galette\Core\Login($this->zdb, $this->i18n, $this->session);
98 $this->history = new \Galette\Core\History($this->zdb, $this->login);
99 $flash_data = [];
100 $this->flash_data = &$flash_data;
101 $this->flash = new \Slim\Flash\Messages($flash_data);
102
103 global $zdb, $i18n, $login, $hist;
104 $zdb = $this->zdb;
105 $i18n = $this->i18n;
106 $login = $this->login;
107 $hist = $this->history;
108
109 $app = new \Slim\App(['router' => $this->mocked_router, 'flash' => $this->flash]);
110 $container = $app->getContainer();
111 /*$this->view = new \mock\Slim\Views\Smarty(
112 rtrim(GALETTE_ROOT . GALETTE_TPL_SUBDIR, DIRECTORY_SEPARATOR),
113 [
114 'cacheDir' => rtrim(GALETTE_CACHE_DIR, DIRECTORY_SEPARATOR),
115 'compileDir' => rtrim(GALETTE_COMPILE_DIR, DIRECTORY_SEPARATOR),
116 'pluginsDir' => [
117 GALETTE_ROOT . 'includes/smarty_plugins'
118 ]
119 ]
120 );
121 $this->calling($this->view)->render = function ($response) {
122 $response->getBody()->write('Atoum view rendered');
123 return $response;
124 };
125
126 $this->view->addSlimPlugins($container->get('router'), '/');
127 //$container['view'] = $this->view;*/
128 $container['view'] = null;
129 $container['zdb'] = $zdb;
130 $container['login'] = $this->login;
131 $container['session'] = $this->session;
132 $container['preferences'] = $this->preferences;
133 $container['logo'] = null;
134 $container['print_logo'] = null;
135 $container['plugins'] = null;
136 $container['history'] = $this->history;
137 $container['i18n'] = null;
138 $container['fields_config'] = null;
139 include_once GALETTE_ROOT . 'includes/fields_defs/members_fields.php';
140 $this->members_fields = $members_fields;
141 $container['members_fields'] = $this->members_fields;
142 include_once GALETTE_ROOT . 'includes/fields_defs/members_fields_cats.php';
143 $this->members_fields_cats = $members_fields_cats;
144 $container['members_fields_cats'] = $this->members_fields_cats;
145 $this->container = $container;
146 $this->request = $container->get('request');
147 $this->response = $container->get('response');
148 }
149
150 /**
151 * Tear down tests
152 *
153 * @param string $testMethod Calling method
154 *
155 * @return void
156 */
157 public function afterTestMethod($testMethod)
158 {
159 $delete = $this->zdb->delete(\Galette\Entity\Adherent::TABLE);
160 $this->zdb->execute($delete);
161 $delete = $this->zdb->delete(\Galette\Entity\DynamicFieldsHandle::TABLE);
162 $this->zdb->execute($delete);
163 $delete = $this->zdb->delete(DynamicField::TABLE);
164 $this->zdb->execute($delete);
165 }
166
167 /**
168 * Import text CSV file
169 *
170 * @param array $fields Fields name to use at import
171 * @param string $file_name File name
172 * @param array $flash_messages Excpeted flash messages from doImport route
173 * @param airay $members_list List of faked members data
174 * @param integer $count_before Count before insertions. Defaults to 0 if null.
175 * @param integer $count_after Count after insertions. Default to $count_before + count $members_list
176 * @param array $values Textual values for dynamic choices fields
177 *
178 * @return void
179 */
180 private function doImportFileTest(
181 array $fields,
182 $file_name,
183 array $flash_messages,
184 array $members_list,
185 $count_before = null,
186 $count_after = null,
187 array $values = []
188 ) {
189 if ($count_before === null) {
190 $count_before = 0;
191 }
192 if ($count_after === null) {
193 $count_after = $count_before + count($members_list);
194 }
195
196 $members = new \Galette\Repository\Members();
197 $list = $members->getList();
198 $this->integer($list->count())->isIdenticalTo($count_before, print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1), true));
199
200 $model = $this->getModel($fields);
201
202 //get csv model file to add data in
203 $controller = new \Galette\Controllers\CsvController($this->container);
204 $response = $controller->getImportModel($this->request, $this->response);
205 $csvin = new \galette\IO\CsvIn($this->zdb);
206
207 $this->integer($response->getStatusCode())->isIdenticalTo(200);
208 $this->array($response->getHeaders())
209 ->array['Content-Type']->isIdenticalTo(['text/csv'])
210 ->array['Content-Disposition']->isIdenticalTo(['attachment;filename="galette_import_model.csv"']);
211
212 $csvfile_model = $response->getBody()->__toString();
213 $this->string($csvfile_model)
214 ->isIdenticalTo("\"".implode("\";\"", $fields)."\"\r\n");
215
216 $fakedata = new \Galette\Util\FakeData($this->zdb, $this->i18n);
217 $contents = $csvfile_model;
218 foreach ($members_list as $member) {
219 $amember = [];
220 foreach ($fields as $field) {
221 $amember[$field] = $member[$field];
222 }
223 $contents .= "\"".implode("\";\"", $amember)."\"\r\n";
224 }
225
226 $path = GALETTE_CACHE_DIR . $file_name;
227 $this->integer(file_put_contents($path, $contents));
228 $_FILES['new_file'] = [
229 'error' => UPLOAD_ERR_OK,
230 'name' => $file_name,
231 'tmp_name' => $path,
232 'size' => filesize($path)
233 ];
234 $this->boolean($csvin->store($_FILES['new_file'], true))->isTrue();
235 $this->boolean(file_exists($csvin->getDestDir() . $csvin->getFileName()))->isTrue();
236
237 $post = [
238 'import_file' => $file_name
239 ];
240
241 $request = clone $this->request;
242 $request = $request->withParsedBody($post);
243
244 $response = $controller->doImports($request, $this->response);
245 $this->integer($response->getStatusCode())->isIdenticalTo(301);
246 $this->array($this->flash_data['slimFlash'])->isIdenticalTo($flash_messages);
247 $this->flash->clearMessages();
248
249 $members = new \Galette\Repository\Members();
250 $list = $members->getList();
251 $this->integer($list->count())->isIdenticalTo($count_after);
252
253 if ($count_before != $count_after) {
254 foreach ($list as $member) {
255 $created = $members_list[$member->fingerprint];
256 foreach ($fields as $field) {
257 if (property_exists($member, $field)) {
258 $this->variable($member->$field)->isEqualTo($created[$field]);
259 } else {
260 //manage dynamic fields
261 $matches = [];
262 if (preg_match('/^dynfield_(\d+)/', $field, $matches)) {
263 $adh = new Adherent($this->zdb, (int)$member->id_adh, ['dynamics' => true]);
264 $expected = [
265 [
266 'item_id' => $adh->id,
267 'field_form' => 'adh',
268 'val_index' => 1,
269 'field_val' => $created[$field]
270 ]
271 ];
272
273 $dfield = $adh->getDynamicFields()->getValues($matches[1]);
274 if (isset($dfield[0]['text_val'])) {
275 //choice, add textual value
276 $expected[0]['text_val'] = $values[$created[$field]];
277 }
278
279 $this->array($adh->getDynamicFields()->getValues($matches[1]))->isEqualTo($expected);
280 } else {
281 throw new \RuntimeException("Unknown field $field");
282 }
283 }
284 }
285 }
286 }
287 }
288
289 /**
290 * Test CSV import loading
291 *
292 * @return void
293 */
294 public function testImport()
295 {
296 $fakedata = new \Galette\Util\FakeData($this->zdb, $this->i18n);
297
298 $fields = ['nom_adh', 'ville_adh', 'bool_exempt_adh', 'fingerprint'];
299 $file_name = 'test-import-atoum.csv';
300 $flash_messages = [
301 'success_detected' => ["File '$file_name' has been successfully imported :)"]
302 ];
303 $members_list = [];
304 for ($i = 0; $i < 10; ++$i) {
305 $data = $fakedata->fakeMember();
306 $data['nom_adh'] = str_replace('"', '""', $data['nom_adh']);
307 $data['ville_adh'] = str_replace('"', '""', $data['ville_adh']);
308 $data['fingerprint'] = 'FAKER_' . $i;
309 $members_list[$data['fingerprint']] = $data;
310 }
311 $count_before = 0;
312 $count_after = 10;
313
314 $this->doImportFileTest($fields, $file_name, $flash_messages, $members_list, $count_before, $count_after);
315
316 //missing name
317 $file_name = 'test-import-atoum-noname.csv';
318 $flash_messages = [
319 'error_detected' => [
320 'File does not comply with requirements.',
321 'Field nom_adh is required, but missing in row 3'
322 ]
323 ];
324
325 $members_list = [];
326 for ($i = 0; $i < 10; ++$i) {
327 $data = $fakedata->fakeMember();
328 //two lines without name.
329 $data['nom_adh'] = (($i == 2 || $i ==5) ? '' : str_replace('"', '""', $data['nom_adh']));
330 $data['ville_adh'] = str_replace('"', '""', $data['ville_adh']);
331 $data['fingerprint'] = 'FAKER_' . $i;
332 $members_list[$data['fingerprint']] = $data;
333 }
334 $count_before = 10;
335 $count_after = 10;
336
337 $this->doImportFileTest($fields, $file_name, $flash_messages, $members_list, $count_before, $count_after);
338 }
339
340 /**
341 * Get CSV import model
342 *
343 * @param array $fields Fields list
344 *
345 * @return \Galette\Entity\ImportModel
346 */
347 protected function getModel($fields) :\Galette\Entity\ImportModel
348 {
349 $model = new \Galette\Entity\ImportModel();
350 $this->boolean($model->remove($this->zdb))->isTrue();
351
352 $this->object($model->setFields($fields))->isInstanceOf('Galette\Entity\ImportModel');
353 $this->boolean($model->store($this->zdb))->isTrue();
354 $this->boolean($model->load())->isTrue();
355 return $model;
356 }
357
358 /**
359 * Test import with dynamic fields
360 *
361 * @return void
362 */
363 public function testImportDynamics()
364 {
365 $field_data = [
366 'form' => 'adh',
367 'field_name' => 'Dynamic text field',
368 'field_perm' => DynamicField::PERM_USER_WRITE,
369 'field_type' => DynamicField::TEXT,
370 'field_required' => 1,
371 'field_repeat' => 1
372 ];
373
374 $df = DynamicField::getFieldType($this->zdb, $field_data['field_type']);
375
376 $stored = $df->store($field_data);
377 $error_detected = $df->getErrors();
378 $warning_detected = $df->getWarnings();
379 $this->boolean($stored)->isTrue(
380 implode(
381 ' ',
382 $df->getErrors() + $df->getWarnings()
383 )
384 );
385 $this->array($error_detected)->isEmpty(implode(' ', $df->getErrors()));
386 $this->array($warning_detected)->isEmpty(implode(' ', $df->getWarnings()));
387
388 $select = $this->zdb->select(DynamicField::TABLE);
389 $select->columns(array('num' => new \Laminas\Db\Sql\Expression('COUNT(1)')));
390 $result = $this->zdb->execute($select)->current();
391 $this->integer((int)$result->num)->isIdenticalTo(1);
392
393 $fakedata = new \Galette\Util\FakeData($this->zdb, $this->i18n);
394
395 $fields = ['nom_adh', 'ville_adh', 'dynfield_' . $df->getId(), 'fingerprint'];
396 $file_name = 'test-import-atoum-dyn.csv';
397 $flash_messages = [
398 'success_detected' => ["File '$file_name' has been successfully imported :)"]
399 ];
400 $members_list = [];
401 for ($i = 0; $i < 10; ++$i) {
402 $data = $fakedata->fakeMember();
403 $data['nom_adh'] = str_replace('"', '""', $data['nom_adh']);
404 $data['ville_adh'] = str_replace('"', '""', $data['ville_adh']);
405 $data['fingerprint'] = 'FAKER_' . $i;
406 $data['dynfield_' . $df->getId()] = 'Dynamic field value for ' . $data['fingerprint'];
407 $members_list[$data['fingerprint']] = $data;
408 }
409 $count_before = 0;
410 $count_after = 10;
411
412 $this->doImportFileTest($fields, $file_name, $flash_messages, $members_list, $count_before, $count_after);
413
414 //missing name
415 //$fields does not change from previous
416 $file_name = 'test-import-atoum-dyn-noname.csv';
417 $flash_messages = [
418 'error_detected' => [
419 'File does not comply with requirements.',
420 'Field nom_adh is required, but missing in row 3'
421 ]
422 ];
423 $members_list = [];
424 for ($i = 0; $i < 10; ++$i) {
425 $data = $fakedata->fakeMember();
426 //two lines without name.
427 $data['nom_adh'] = (($i == 2 || $i ==5) ? '' : str_replace('"', '""', $data['nom_adh']));
428 $data['ville_adh'] = str_replace('"', '""', $data['ville_adh']);
429 $data['fingerprint'] = 'FAKER_' . $i;
430 $data['dynfield_' . $df->getId()] = 'Dynamic field value for ' . $data['fingerprint'];
431 $members_list[$data['fingerprint']] = $data;
432 }
433 $count_before = 10;
434 $count_after = 10;
435
436 $this->doImportFileTest($fields, $file_name, $flash_messages, $members_list, $count_before, $count_after);
437
438 //missing required dynamic field
439 //$fields does not change from previous
440 $file_name = 'test-import-atoum-dyn-nodyn.csv';
441 $flash_messages = [
442 'error_detected' => [
443 'File does not comply with requirements.',
444 'Missing required field Dynamic text field'
445 ]
446 ];
447 $members_list = [];
448 for ($i = 0; $i < 10; ++$i) {
449 $data = $fakedata->fakeMember();
450 $data['nom_adh'] = str_replace('"', '""', $data['nom_adh']);
451 $data['ville_adh'] = str_replace('"', '""', $data['ville_adh']);
452 $data['fingerprint'] = 'FAKER_' . $i;
453 //two lines without required dynamic field.
454 $data['dynfield_' . $df->getId()] = (($i == 2 || $i ==5) ? '' :
455 'Dynamic field value for ' . $data['fingerprint']);
456 $members_list[$data['fingerprint']] = $data;
457 }
458 $count_before = 10;
459 $count_after = 10;
460
461 $this->doImportFileTest($fields, $file_name, $flash_messages, $members_list, $count_before, $count_after);
462
463 //cleanup members and dynamic fields values
464 $delete = $this->zdb->delete(\Galette\Entity\Adherent::TABLE);
465 $this->zdb->execute($delete);
466 $delete = $this->zdb->delete(\Galette\Entity\DynamicFieldsHandle::TABLE);
467 $this->zdb->execute($delete);
468
469 //new dynamic field, of type choice.
470 $values = [
471 'First value',
472 'Second value',
473 'Third value'
474 ];
475 $cfield_data = [
476 'form' => 'adh',
477 'field_name' => 'Dynamic choice field',
478 'field_perm' => DynamicField::PERM_USER_WRITE,
479 'field_type' => DynamicField::CHOICE,
480 'field_required' => 0,
481 'field_repeat' => 1,
482 'fixed_values' => implode("\n", $values)
483 ];
484
485 $cdf = DynamicField::getFieldType($this->zdb, $cfield_data['field_type']);
486
487 $stored = $cdf->store($cfield_data);
488 $error_detected = $cdf->getErrors();
489 $warning_detected = $cdf->getWarnings();
490 $this->boolean($stored)->isTrue(
491 implode(
492 ' ',
493 $cdf->getErrors() + $cdf->getWarnings()
494 )
495 );
496 $this->array($error_detected)->isEmpty(implode(' ', $cdf->getErrors()));
497 $this->array($warning_detected)->isEmpty(implode(' ', $cdf->getWarnings()));
498
499 $select = $this->zdb->select(DynamicField::TABLE);
500 $select->columns(array('num' => new \Laminas\Db\Sql\Expression('COUNT(1)')));
501 $result = $this->zdb->execute($select)->current();
502 $this->integer((int)$result->num)->isIdenticalTo(2);
503
504 $this->array($cdf->getValues())->isIdenticalTo($values);
505
506 $fields = ['nom_adh', 'ville_adh', 'dynfield_' . $cdf->getId(), 'fingerprint'];
507 $file_name = 'test-import-atoum-dyn-cdyn.csv';
508 $flash_messages = [
509 'success_detected' => ["File '$file_name' has been successfully imported :)"]
510 ];
511 $members_list = [];
512 for ($i = 0; $i < 10; ++$i) {
513 $data = $fakedata->fakeMember();
514 $data['nom_adh'] = str_replace('"', '""', $data['nom_adh']);
515 $data['ville_adh'] = str_replace('"', '""', $data['ville_adh']);
516 $data['fingerprint'] = 'FAKER_' . $i;
517 $faker = $fakedata->getFaker();
518 $data['dynfield_' . $cdf->getId()] = $faker->numberBetween(0, 2);
519 $members_list[$data['fingerprint']] = $data;
520 }
521 $count_before = 0;
522 $count_after = 10;
523
524 $this->doImportFileTest(
525 $fields,
526 $file_name,
527 $flash_messages,
528 $members_list,
529 $count_before,
530 $count_after,
531 $values
532 );
533
534 //cleanup members and dynamic fields values
535 $delete = $this->zdb->delete(\Galette\Entity\Adherent::TABLE);
536 $this->zdb->execute($delete);
537 $delete = $this->zdb->delete(\Galette\Entity\DynamicFieldsHandle::TABLE);
538 $this->zdb->execute($delete);
539
540 //new dynamic field, of type date.
541 $cfield_data = [
542 'form' => 'adh',
543 'field_name' => 'Dynamic date field',
544 'field_perm' => DynamicField::PERM_USER_WRITE,
545 'field_type' => DynamicField::DATE,
546 'field_required' => 0,
547 'field_repeat' => 1
548 ];
549
550 $cdf = DynamicField::getFieldType($this->zdb, $cfield_data['field_type']);
551
552 $stored = $cdf->store($cfield_data);
553 $error_detected = $cdf->getErrors();
554 $warning_detected = $cdf->getWarnings();
555 $this->boolean($stored)->isTrue(
556 implode(
557 ' ',
558 $cdf->getErrors() + $cdf->getWarnings()
559 )
560 );
561 $this->array($error_detected)->isEmpty(implode(' ', $cdf->getErrors()));
562 $this->array($warning_detected)->isEmpty(implode(' ', $cdf->getWarnings()));
563
564 $select = $this->zdb->select(DynamicField::TABLE);
565 $select->columns(array('num' => new \Laminas\Db\Sql\Expression('COUNT(1)')));
566 $result = $this->zdb->execute($select)->current();
567 $this->integer((int)$result->num)->isIdenticalTo(3);
568
569
570 $fields = ['nom_adh', 'ville_adh', 'dynfield_' . $cdf->getId(), 'fingerprint'];
571 $file_name = 'test-import-atoum-cdyn-date.csv';
572 $flash_messages = [
573 'success_detected' => ["File '$file_name' has been successfully imported :)"]
574 ];
575 $members_list = [];
576 for ($i = 0; $i < 10; ++$i) {
577 $data = $fakedata->fakeMember();
578 $data['nom_adh'] = str_replace('"', '""', $data['nom_adh']);
579 $data['ville_adh'] = str_replace('"', '""', $data['ville_adh']);
580 $data['fingerprint'] = 'FAKER_' . $i;
581 $data['dynfield_' . $cdf->getId()] = $data['date_crea_adh'];
582 $members_list[$data['fingerprint']] = $data;
583 }
584 $count_before = 0;
585 $count_after = 10;
586
587 $this->doImportFileTest($fields, $file_name, $flash_messages, $members_list, $count_before, $count_after);
588
589 //Test with a bad date
590 //$fields does not change from previous
591 $file_name = 'test-import-atoum-cdyn-baddate.csv';
592 $flash_messages = [
593 'error_detected' => [
594 'File does not comply with requirements.',
595 '- Wrong date format (Y-m-d) for Dynamic date field!'
596 ]
597 ];
598 $members_list = [];
599 for ($i = 0; $i < 10; ++$i) {
600 $data = $fakedata->fakeMember();
601 $data['nom_adh'] = str_replace('"', '""', $data['nom_adh']);
602 $data['ville_adh'] = str_replace('"', '""', $data['ville_adh']);
603 $data['fingerprint'] = 'FAKER_' . $i;
604 $data['dynfield_' . $cdf->getId()] =(($i == 2 || $i ==5) ? '20200513' : $data['date_crea_adh']);
605 $members_list[$data['fingerprint']] = $data;
606 }
607 $count_before = 10;
608 $count_after = 10;
609
610 $this->doImportFileTest($fields, $file_name, $flash_messages, $members_list, $count_before, $count_after);
611 }
612 }