PageRenderTime 78ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/tests/tablelib_test.php

https://github.com/dongsheng/moodle
PHP | 791 lines | 520 code | 143 blank | 128 comment | 5 complexity | a7963fd34c0096b798d169359a271e42 MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, GPL-3.0, Apache-2.0, LGPL-2.1
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Test tablelib.
  18. *
  19. * @package core
  20. * @category phpunit
  21. * @copyright 2013 Damyon Wiese <damyon@moodle.com>
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. defined('MOODLE_INTERNAL') || die();
  25. global $CFG;
  26. require_once($CFG->libdir . '/tablelib.php');
  27. require_once($CFG->libdir . '/tests/fixtures/testable_flexible_table.php');
  28. /**
  29. * Test some of tablelib.
  30. *
  31. * @package core
  32. * @category phpunit
  33. * @copyright 2013 Damyon Wiese <damyon@moodle.com>
  34. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35. */
  36. class core_tablelib_testcase extends advanced_testcase {
  37. protected function generate_columns($cols) {
  38. $columns = array();
  39. foreach (range(0, $cols - 1) as $j) {
  40. array_push($columns, 'column' . $j);
  41. }
  42. return $columns;
  43. }
  44. protected function generate_headers($cols) {
  45. $columns = array();
  46. foreach (range(0, $cols - 1) as $j) {
  47. array_push($columns, 'Column ' . $j);
  48. }
  49. return $columns;
  50. }
  51. protected function generate_data($rows, $cols) {
  52. $data = array();
  53. foreach (range(0, $rows - 1) as $i) {
  54. $row = array();
  55. foreach (range(0, $cols - 1) as $j) {
  56. $val = 'row ' . $i . ' col ' . $j;
  57. $row['column' . $j] = $val;
  58. }
  59. array_push($data, $row);
  60. }
  61. return $data;
  62. }
  63. /**
  64. * Create a table with properties as passed in params, add data and output html.
  65. *
  66. * @param string[] $columns
  67. * @param string[] $headers
  68. * @param bool $sortable
  69. * @param bool $collapsible
  70. * @param string[] $suppress
  71. * @param string[] $nosorting
  72. * @param (array|object)[] $data
  73. * @param int $pagesize
  74. */
  75. protected function run_table_test($columns, $headers, $sortable, $collapsible, $suppress, $nosorting, $data, $pagesize) {
  76. $table = $this->create_and_setup_table($columns, $headers, $sortable, $collapsible, $suppress, $nosorting);
  77. $table->pagesize($pagesize, count($data));
  78. foreach ($data as $row) {
  79. $table->add_data_keyed($row);
  80. }
  81. $table->finish_output();
  82. }
  83. /**
  84. * Create a table with properties as passed in params.
  85. *
  86. * @param string[] $columns
  87. * @param string[] $headers
  88. * @param bool $sortable
  89. * @param bool $collapsible
  90. * @param string[] $suppress
  91. * @param string[] $nosorting
  92. * @return flexible_table
  93. */
  94. protected function create_and_setup_table($columns, $headers, $sortable, $collapsible, $suppress, $nosorting) {
  95. $table = new flexible_table('tablelib_test');
  96. $table->define_columns($columns);
  97. $table->define_headers($headers);
  98. $table->define_baseurl('/invalid.php');
  99. $table->sortable($sortable);
  100. $table->collapsible($collapsible);
  101. foreach ($suppress as $column) {
  102. $table->column_suppress($column);
  103. }
  104. foreach ($nosorting as $column) {
  105. $table->no_sorting($column);
  106. }
  107. $table->setup();
  108. return $table;
  109. }
  110. public function test_empty_table() {
  111. $this->expectOutputRegex('/' . get_string('nothingtodisplay') . '/');
  112. $this->run_table_test(
  113. array('column1', 'column2'), // Columns.
  114. array('Column 1', 'Column 2'), // Headers.
  115. true, // Sortable.
  116. false, // Collapsible.
  117. array(), // Suppress columns.
  118. array(), // No sorting.
  119. array(), // Data.
  120. 10 // Page size.
  121. );
  122. }
  123. public function test_has_next_pagination() {
  124. $data = $this->generate_data(11, 2);
  125. $columns = $this->generate_columns(2);
  126. $headers = $this->generate_headers(2);
  127. // Search for pagination controls containing 'page-link"\saria-label="Next"'.
  128. $this->expectOutputRegex('/page-link"\saria-label="Next page"/');
  129. $this->run_table_test(
  130. $columns,
  131. $headers,
  132. true,
  133. false,
  134. array(),
  135. array(),
  136. $data,
  137. 10
  138. );
  139. }
  140. public function test_has_hide() {
  141. $data = $this->generate_data(11, 2);
  142. $columns = $this->generate_columns(2);
  143. $headers = $this->generate_headers(2);
  144. // Search for 'hide' links in the column headers.
  145. $this->expectOutputRegex('/' . get_string('hide') . '/');
  146. $this->run_table_test(
  147. $columns,
  148. $headers,
  149. true,
  150. true,
  151. array(),
  152. array(),
  153. $data,
  154. 10
  155. );
  156. }
  157. public function test_has_not_hide() {
  158. $data = $this->generate_data(11, 2);
  159. $columns = $this->generate_columns(2);
  160. $headers = $this->generate_headers(2);
  161. // Make sure there are no 'hide' links in the headers.
  162. ob_start();
  163. $this->run_table_test(
  164. $columns,
  165. $headers,
  166. true,
  167. false,
  168. array(),
  169. array(),
  170. $data,
  171. 10
  172. );
  173. $output = ob_get_contents();
  174. ob_end_clean();
  175. $this->assertStringNotContainsString(get_string('hide'), $output);
  176. }
  177. public function test_has_sort() {
  178. $data = $this->generate_data(11, 2);
  179. $columns = $this->generate_columns(2);
  180. $headers = $this->generate_headers(2);
  181. // Search for pagination controls containing '1.*2</a>.*Next</a>'.
  182. $this->expectOutputRegex('/' . get_string('sortby') . '/');
  183. $this->run_table_test(
  184. $columns,
  185. $headers,
  186. true,
  187. false,
  188. array(),
  189. array(),
  190. $data,
  191. 10
  192. );
  193. }
  194. public function test_has_not_sort() {
  195. $data = $this->generate_data(11, 2);
  196. $columns = $this->generate_columns(2);
  197. $headers = $this->generate_headers(2);
  198. // Make sure there are no 'Sort by' links in the headers.
  199. ob_start();
  200. $this->run_table_test(
  201. $columns,
  202. $headers,
  203. false,
  204. false,
  205. array(),
  206. array(),
  207. $data,
  208. 10
  209. );
  210. $output = ob_get_contents();
  211. ob_end_clean();
  212. $this->assertStringNotContainsString(get_string('sortby'), $output);
  213. }
  214. public function test_has_not_next_pagination() {
  215. $data = $this->generate_data(10, 2);
  216. $columns = $this->generate_columns(2);
  217. $headers = $this->generate_headers(2);
  218. // Make sure there are no 'Next' links in the pagination.
  219. ob_start();
  220. $this->run_table_test(
  221. $columns,
  222. $headers,
  223. true,
  224. false,
  225. array(),
  226. array(),
  227. $data,
  228. 10
  229. );
  230. $output = ob_get_contents();
  231. ob_end_clean();
  232. $this->assertStringNotContainsString(get_string('next'), $output);
  233. }
  234. public function test_1_col() {
  235. $data = $this->generate_data(100, 1);
  236. $columns = $this->generate_columns(1);
  237. $headers = $this->generate_headers(1);
  238. $this->expectOutputRegex('/row 0 col 0/');
  239. $this->run_table_test(
  240. $columns,
  241. $headers,
  242. true,
  243. false,
  244. array(),
  245. array(),
  246. $data,
  247. 10
  248. );
  249. }
  250. public function test_empty_rows() {
  251. $data = $this->generate_data(1, 5);
  252. $columns = $this->generate_columns(5);
  253. $headers = $this->generate_headers(5);
  254. // Test that we have at least 5 columns generated for each empty row.
  255. $this->expectOutputRegex('/emptyrow.*r9_c4/');
  256. $this->run_table_test(
  257. $columns,
  258. $headers,
  259. true,
  260. false,
  261. array(),
  262. array(),
  263. $data,
  264. 10
  265. );
  266. }
  267. public function test_5_cols() {
  268. $data = $this->generate_data(100, 5);
  269. $columns = $this->generate_columns(5);
  270. $headers = $this->generate_headers(5);
  271. $this->expectOutputRegex('/row 0 col 0/');
  272. $this->run_table_test(
  273. $columns,
  274. $headers,
  275. true,
  276. false,
  277. array(),
  278. array(),
  279. $data,
  280. 10
  281. );
  282. }
  283. public function test_50_cols() {
  284. $data = $this->generate_data(100, 50);
  285. $columns = $this->generate_columns(50);
  286. $headers = $this->generate_headers(50);
  287. $this->expectOutputRegex('/row 0 col 0/');
  288. $this->run_table_test(
  289. $columns,
  290. $headers,
  291. true,
  292. false,
  293. array(),
  294. array(),
  295. $data,
  296. 10
  297. );
  298. }
  299. /**
  300. * Data provider for test_fullname_column
  301. *
  302. * @return array
  303. */
  304. public function fullname_column_provider() {
  305. return [
  306. ['language'],
  307. ['alternatename lastname'],
  308. ['firstname lastnamephonetic'],
  309. ];
  310. }
  311. /**
  312. * Test fullname column observes configured alternate fullname format configuration
  313. *
  314. * @param string $format
  315. * @return void
  316. *
  317. * @dataProvider fullname_column_provider
  318. */
  319. public function test_fullname_column(string $format) {
  320. $this->resetAfterTest();
  321. $this->setAdminUser();
  322. set_config('alternativefullnameformat', $format);
  323. $user = $this->getDataGenerator()->create_user();
  324. $table = $this->create_and_setup_table(['fullname'], [], true, false, [], []);
  325. $this->assertStringContainsString(fullname($user, true), $table->format_row($user)['fullname']);
  326. }
  327. /**
  328. * Test fullname column ignores fullname format configuration for a user with viewfullnames capability prohibited
  329. *
  330. * @param string $format
  331. * @return void
  332. *
  333. * @dataProvider fullname_column_provider
  334. */
  335. public function test_fullname_column_prohibit_viewfullnames(string $format) {
  336. global $DB, $CFG;
  337. $this->resetAfterTest();
  338. set_config('alternativefullnameformat', $format);
  339. $currentuser = $this->getDataGenerator()->create_user();
  340. $this->setUser($currentuser);
  341. // Prohibit the viewfullnames from the default user role.
  342. $userrole = $DB->get_record('role', ['id' => $CFG->defaultuserroleid]);
  343. role_change_permission($userrole->id, context_system::instance(), 'moodle/site:viewfullnames', CAP_PROHIBIT);
  344. $user = $this->getDataGenerator()->create_user();
  345. $table = $this->create_and_setup_table(['fullname'], [], true, false, [], []);
  346. $this->assertStringContainsString(fullname($user, false), $table->format_row($user)['fullname']);
  347. }
  348. public function test_get_row_html() {
  349. $data = $this->generate_data(1, 5);
  350. $columns = $this->generate_columns(5);
  351. $headers = $this->generate_headers(5);
  352. $data = array_keys(array_flip($data[0]));
  353. $table = new flexible_table('tablelib_test');
  354. $table->define_columns($columns);
  355. $table->define_headers($headers);
  356. $table->define_baseurl('/invalid.php');
  357. $row = $table->get_row_html($data);
  358. $this->assertMatchesRegularExpression('/row 0 col 0/', $row);
  359. $this->assertMatchesRegularExpression('/<tr class=""/', $row);
  360. $this->assertMatchesRegularExpression('/<td class="cell c0"/', $row);
  361. }
  362. public function test_persistent_table() {
  363. global $SESSION;
  364. $data = $this->generate_data(5, 5);
  365. $columns = $this->generate_columns(5);
  366. $headers = $this->generate_headers(5);
  367. // Testing without persistence first to verify that the results are different.
  368. $table1 = new flexible_table('tablelib_test');
  369. $table1->define_columns($columns);
  370. $table1->define_headers($headers);
  371. $table1->define_baseurl('/invalid.php');
  372. $table1->sortable(true);
  373. $table1->collapsible(true);
  374. $table1->is_persistent(false);
  375. $_GET['thide'] = 'column0';
  376. $_GET['tsort'] = 'column1';
  377. $_GET['tifirst'] = 'A';
  378. $_GET['tilast'] = 'Z';
  379. foreach ($data as $row) {
  380. $table1->add_data_keyed($row);
  381. }
  382. $table1->setup();
  383. // Clear session data between each new table.
  384. unset($SESSION->flextable);
  385. $table2 = new flexible_table('tablelib_test');
  386. $table2->define_columns($columns);
  387. $table2->define_headers($headers);
  388. $table2->define_baseurl('/invalid.php');
  389. $table2->sortable(true);
  390. $table2->collapsible(true);
  391. $table2->is_persistent(false);
  392. unset($_GET);
  393. foreach ($data as $row) {
  394. $table2->add_data_keyed($row);
  395. }
  396. $table2->setup();
  397. $this->assertNotEquals($table1, $table2);
  398. unset($SESSION->flextable);
  399. // Now testing with persistence to check that the tables are the same.
  400. $table3 = new flexible_table('tablelib_test');
  401. $table3->define_columns($columns);
  402. $table3->define_headers($headers);
  403. $table3->define_baseurl('/invalid.php');
  404. $table3->sortable(true);
  405. $table3->collapsible(true);
  406. $table3->is_persistent(true);
  407. $_GET['thide'] = 'column0';
  408. $_GET['tsort'] = 'column1';
  409. $_GET['tifirst'] = 'A';
  410. $_GET['tilast'] = 'Z';
  411. foreach ($data as $row) {
  412. $table3->add_data_keyed($row);
  413. }
  414. $table3->setup();
  415. unset($SESSION->flextable);
  416. $table4 = new flexible_table('tablelib_test');
  417. $table4->define_columns($columns);
  418. $table4->define_headers($headers);
  419. $table4->define_baseurl('/invalid.php');
  420. $table4->sortable(true);
  421. $table4->collapsible(true);
  422. $table4->is_persistent(true);
  423. unset($_GET);
  424. foreach ($data as $row) {
  425. $table4->add_data_keyed($row);
  426. }
  427. $table4->setup();
  428. $this->assertEquals($table3, $table4);
  429. unset($SESSION->flextable);
  430. // Finally, another test with no persistence, but without clearing the session data.
  431. $table5 = new flexible_table('tablelib_test');
  432. $table5->define_columns($columns);
  433. $table5->define_headers($headers);
  434. $table5->define_baseurl('/invalid.php');
  435. $table5->sortable(true);
  436. $table5->collapsible(true);
  437. $table5->is_persistent(true);
  438. $_GET['thide'] = 'column0';
  439. $_GET['tsort'] = 'column1';
  440. $_GET['tifirst'] = 'A';
  441. $_GET['tilast'] = 'Z';
  442. foreach ($data as $row) {
  443. $table5->add_data_keyed($row);
  444. }
  445. $table5->setup();
  446. $table6 = new flexible_table('tablelib_test');
  447. $table6->define_columns($columns);
  448. $table6->define_headers($headers);
  449. $table6->define_baseurl('/invalid.php');
  450. $table6->sortable(true);
  451. $table6->collapsible(true);
  452. $table6->is_persistent(true);
  453. unset($_GET);
  454. foreach ($data as $row) {
  455. $table6->add_data_keyed($row);
  456. }
  457. $table6->setup();
  458. $this->assertEquals($table5, $table6);
  459. }
  460. /**
  461. * Helper method for preparing tables instances in {@link self::test_can_be_reset()}.
  462. *
  463. * @param string $tableid
  464. * @return testable_flexible_table
  465. */
  466. protected function prepare_table_for_reset_test($tableid) {
  467. global $SESSION;
  468. unset($SESSION->flextable[$tableid]);
  469. $data = $this->generate_data(25, 3);
  470. $columns = array('column0', 'column1', 'column2');
  471. $headers = $this->generate_headers(3);
  472. $table = new testable_flexible_table($tableid);
  473. $table->define_baseurl('/invalid.php');
  474. $table->define_columns($columns);
  475. $table->define_headers($headers);
  476. $table->collapsible(true);
  477. $table->is_persistent(false);
  478. return $table;
  479. }
  480. public function test_can_be_reset() {
  481. // Table in its default state (as if seen for the first time), nothing to reset.
  482. $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
  483. $table->setup();
  484. $this->assertFalse($table->can_be_reset());
  485. // Table in its default state with default sorting defined, nothing to reset.
  486. $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
  487. $table->sortable(true, 'column1', SORT_DESC);
  488. $table->setup();
  489. $this->assertFalse($table->can_be_reset());
  490. // Table explicitly sorted by the default column & direction, nothing to reset.
  491. $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
  492. $table->sortable(true, 'column1', SORT_DESC);
  493. $_GET['tsort'] = 'column1';
  494. $_GET['tdir'] = SORT_DESC;
  495. $table->setup();
  496. unset($_GET['tsort']);
  497. unset($_GET['tdir']);
  498. $this->assertFalse($table->can_be_reset());
  499. // Table explicitly sorted twice by the default column & direction, nothing to reset.
  500. $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
  501. $table->sortable(true, 'column1', SORT_DESC);
  502. $_GET['tsort'] = 'column1';
  503. $_GET['tdir'] = SORT_DESC;
  504. $table->setup();
  505. $table->setup(); // Set up again to simulate the second page request.
  506. unset($_GET['tsort']);
  507. unset($_GET['tdir']);
  508. $this->assertFalse($table->can_be_reset());
  509. // Table sorted by other than default column, can be reset.
  510. $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
  511. $table->sortable(true, 'column1', SORT_DESC);
  512. $_GET['tsort'] = 'column2';
  513. $table->setup();
  514. unset($_GET['tsort']);
  515. $this->assertTrue($table->can_be_reset());
  516. // Table sorted by other than default direction, can be reset.
  517. $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
  518. $table->sortable(true, 'column1', SORT_DESC);
  519. $_GET['tsort'] = 'column1';
  520. $_GET['tdir'] = SORT_ASC;
  521. $table->setup();
  522. unset($_GET['tsort']);
  523. unset($_GET['tdir']);
  524. $this->assertTrue($table->can_be_reset());
  525. // Table sorted by the default column after another sorting previously selected.
  526. // This leads to different ORDER BY than just having a single sort defined, can be reset.
  527. $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
  528. $table->sortable(true, 'column1', SORT_DESC);
  529. $_GET['tsort'] = 'column0';
  530. $table->setup();
  531. $_GET['tsort'] = 'column1';
  532. $table->setup();
  533. unset($_GET['tsort']);
  534. $this->assertTrue($table->can_be_reset());
  535. // Table having some column collapsed, can be reset.
  536. $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
  537. $_GET['thide'] = 'column2';
  538. $table->setup();
  539. unset($_GET['thide']);
  540. $this->assertTrue($table->can_be_reset());
  541. // Table having some column explicitly expanded, nothing to reset.
  542. $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
  543. $_GET['tshow'] = 'column2';
  544. $table->setup();
  545. unset($_GET['tshow']);
  546. $this->assertFalse($table->can_be_reset());
  547. // Table after expanding a collapsed column, nothing to reset.
  548. $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
  549. $_GET['thide'] = 'column0';
  550. $table->setup();
  551. $_GET['tshow'] = 'column0';
  552. $table->setup();
  553. unset($_GET['thide']);
  554. unset($_GET['tshow']);
  555. $this->assertFalse($table->can_be_reset());
  556. // Table with some name filtering enabled, can be reset.
  557. $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
  558. $_GET['tifirst'] = 'A';
  559. $table->setup();
  560. unset($_GET['tifirst']);
  561. $this->assertTrue($table->can_be_reset());
  562. }
  563. /**
  564. * Test export in CSV format
  565. */
  566. public function test_table_export() {
  567. $table = new flexible_table('tablelib_test_export');
  568. $table->define_baseurl('/invalid.php');
  569. $table->define_columns(['c1', 'c2', 'c3']);
  570. $table->define_headers(['Col1', 'Col2', 'Col3']);
  571. ob_start();
  572. $table->is_downloadable(true);
  573. $table->is_downloading('csv');
  574. $table->setup();
  575. $table->add_data(['column0' => 'a', 'column1' => 'b', 'column2' => 'c']);
  576. $output = ob_get_contents();
  577. ob_end_clean();
  578. $this->assertEquals("Col1,Col2,Col3\na,b,c\n", substr($output, 3));
  579. }
  580. /**
  581. * Test the initials functionality.
  582. *
  583. * @dataProvider initials_provider
  584. * @param string|null $getvalue
  585. * @param string|null $setvalue
  586. * @param string|null $finalvalue
  587. */
  588. public function test_initials_first_set(?string $getvalue, ?string $setvalue, ?string $finalvalue): void {
  589. global $_GET;
  590. $this->resetAfterTest(true);
  591. $table = new flexible_table('tablelib_test');
  592. $user = $this->getDataGenerator()->create_user();
  593. $table->define_columns(['fullname']);
  594. $table->define_headers(['Fullname']);
  595. $table->define_baseurl('/invalid.php');
  596. $table->initialbars(true);
  597. if ($getvalue !== null) {
  598. $_GET['tifirst'] = $getvalue;
  599. }
  600. if ($setvalue !== null) {
  601. $table->set_first_initial($setvalue);
  602. }
  603. $table->setup();
  604. $this->assertEquals($finalvalue, $table->get_initial_first());
  605. }
  606. /**
  607. * Test the initials functionality.
  608. *
  609. * @dataProvider initials_provider
  610. * @param string|null $getvalue
  611. * @param string|null $setvalue
  612. * @param string|null $finalvalue
  613. */
  614. public function test_initials_last_set(?string $getvalue, ?string $setvalue, ?string $finalvalue): void {
  615. global $_GET;
  616. $this->resetAfterTest(true);
  617. $table = new flexible_table('tablelib_test');
  618. $user = $this->getDataGenerator()->create_user();
  619. $table->define_columns(['fullname']);
  620. $table->define_headers(['Fullname']);
  621. $table->define_baseurl('/invalid.php');
  622. $table->initialbars(true);
  623. if ($getvalue !== null) {
  624. $_GET['tilast'] = $getvalue;
  625. }
  626. if ($setvalue !== null) {
  627. $table->set_last_initial($setvalue);
  628. }
  629. $table->setup();
  630. $this->assertEquals($finalvalue, $table->get_initial_last());
  631. }
  632. /**
  633. * Data for testing initials providers.
  634. *
  635. * @return array
  636. */
  637. public function initials_provider(): array {
  638. return [
  639. [null, null, null],
  640. ['A', null, 'A'],
  641. ['Z', null, 'Z'],
  642. [null, 'A', 'A'],
  643. [null, 'Z', 'Z'],
  644. ['A', 'Z', 'Z'],
  645. ['Z', 'A', 'A'],
  646. ];
  647. }
  648. }