Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
namespace mod_data;
18
 
19
use coding_exception;
20
use dml_exception;
21
use mod_data\local\importer\csv_entries_importer;
22
use moodle_exception;
23
use zip_archive;
24
 
25
/**
26
 * Unit tests for import.php.
27
 *
28
 * @package    mod_data
29
 * @category   test
30
 * @covers     \mod_data\local\importer\entries_importer
31
 * @covers     \mod_data\local\importer\csv_entries_importer
32
 * @copyright  2019 Tobias Reischmann
33
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34
 */
35
final class entries_import_test extends \advanced_testcase {
36
 
37
    /**
38
     * Set up function.
39
     */
40
    protected function setUp(): void {
41
        parent::setUp();
42
 
43
        global $CFG;
44
        require_once($CFG->dirroot . '/mod/data/lib.php');
45
        require_once($CFG->dirroot . '/lib/datalib.php');
46
        require_once($CFG->dirroot . '/lib/csvlib.class.php');
47
        require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
48
        require_once($CFG->dirroot . '/mod/data/tests/generator/lib.php');
49
    }
50
 
51
    /**
52
     * Get the test data.
53
     * In this instance we are setting up database records to be used in the unit tests.
54
     *
55
     * @return array
56
     */
57
    protected function get_test_data(): array {
58
        $this->resetAfterTest(true);
59
 
60
        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
61
        $course = $this->getDataGenerator()->create_course();
62
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
63
        $this->setUser($teacher);
64
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student', array('username' => 'student'));
65
 
66
        $data = $generator->create_instance(array('course' => $course->id));
67
        $cm = get_coursemodule_from_instance('data', $data->id);
68
 
69
        // Add fields.
70
        $fieldrecord = new \stdClass();
71
        $fieldrecord->name = 'ID'; // Identifier of the records for testing.
72
        $fieldrecord->type = 'number';
73
        $generator->create_field($fieldrecord, $data);
74
 
75
        $fieldrecord->name = 'Param2';
76
        $fieldrecord->type = 'text';
77
        $generator->create_field($fieldrecord, $data);
78
 
79
        $fieldrecord->name = 'filefield';
80
        $fieldrecord->type = 'file';
81
        $generator->create_field($fieldrecord, $data);
82
 
83
        $fieldrecord->name = 'picturefield';
84
        $fieldrecord->type = 'picture';
85
        $generator->create_field($fieldrecord, $data);
86
 
87
        return [
88
            'teacher' => $teacher,
89
            'student' => $student,
90
            'data' => $data,
91
            'cm' => $cm,
92
        ];
93
    }
94
 
95
    /**
96
     * Test uploading entries for a data instance without userdata.
97
     *
98
     * @throws dml_exception
99
     */
100
    public function test_import(): void {
101
        [
102
            'data' => $data,
103
            'cm' => $cm,
104
            'teacher' => $teacher,
105
        ] = $this->get_test_data();
106
 
107
        $importer = new csv_entries_importer(__DIR__ . '/fixtures/test_data_import.csv',
108
            'test_data_import.csv');
109
        $importer->import_csv($cm, $data, 'UTF-8', 'comma');
110
 
111
        // No userdata is present in the file: Fallback is to assign the uploading user as author.
112
        $expecteduserids = array();
113
        $expecteduserids[1] = $teacher->id;
114
        $expecteduserids[2] = $teacher->id;
115
 
116
        $records = $this->get_data_records($data->id);
117
        $this->assertCount(2, $records);
118
        foreach ($records as $record) {
119
            $identifier = $record->items['ID']->content;
120
            $this->assertEquals($expecteduserids[$identifier], $record->userid);
121
        }
122
    }
123
 
124
    /**
125
     * Test uploading entries for a data instance with userdata.
126
     *
127
     * At least one entry has an identifiable user, which is assigned as author.
128
     *
129
     * @throws dml_exception
130
     */
131
    public function test_import_with_userdata(): void {
132
        [
133
            'data' => $data,
134
            'cm' => $cm,
135
            'teacher' => $teacher,
136
            'student' => $student,
137
        ] = $this->get_test_data();
138
 
139
        $importer = new csv_entries_importer(__DIR__ . '/fixtures/test_data_import_with_userdata.csv',
140
            'test_data_import_with_userdata.csv');
141
        $importer->import_csv($cm, $data, 'UTF-8', 'comma');
142
 
143
        $expecteduserids = array();
144
        $expecteduserids[1] = $student->id; // User student exists and is assigned as author.
145
        $expecteduserids[2] = $teacher->id; // User student2 does not exist. Fallback is the uploading user.
146
 
147
        $records = $this->get_data_records($data->id);
148
        $this->assertCount(2, $records);
149
        foreach ($records as $record) {
150
            $identifier = $record->items['ID']->content;
151
            $this->assertEquals($expecteduserids[$identifier], $record->userid);
152
        }
153
    }
154
 
155
    /**
156
     * Test uploading entries for a data instance with userdata and a defined field 'Username'.
157
     *
158
     * This should test the corner case, in which a user has defined a data fields, which has the same name
159
     * as the current lang string for username. In that case, the first Username entry is used for the field.
160
     * The second one is used to identify the author.
161
     *
162
     * @throws coding_exception
163
     * @throws dml_exception
164
     */
165
    public function test_import_with_field_username(): void {
166
        [
167
            'data' => $data,
168
            'cm' => $cm,
169
            'teacher' => $teacher,
170
            'student' => $student,
171
        ] = $this->get_test_data();
172
        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
173
 
174
        // Add username field.
175
        $fieldrecord = new \stdClass();
176
        $fieldrecord->name = 'Username';
177
        $fieldrecord->type = 'text';
178
        $generator->create_field($fieldrecord, $data);
179
 
180
        $importer = new csv_entries_importer(__DIR__ . '/fixtures/test_data_import_with_field_username.csv',
181
            'test_data_import_with_field_username.csv');
182
        $importer->import_csv($cm, $data, 'UTF-8', 'comma');
183
 
184
        $expecteduserids = array();
185
        $expecteduserids[1] = $student->id; // User student exists and is assigned as author.
186
        $expecteduserids[2] = $teacher->id; // User student2 does not exist. Fallback is the uploading user.
187
        $expecteduserids[3] = $student->id; // User student exists and is assigned as author.
188
 
189
        $expectedcontent = array();
190
        $expectedcontent[1] = array(
191
            'Username' => 'otherusername1',
192
            'Param2' => 'My first entry',
193
        );
194
        $expectedcontent[2] = array(
195
            'Username' => 'otherusername2',
196
            'Param2' => 'My second entry',
197
        );
198
        $expectedcontent[3] = array(
199
            'Username' => 'otherusername3',
200
            'Param2' => 'My third entry',
201
        );
202
 
203
        $records = $this->get_data_records($data->id);
204
        $this->assertCount(3, $records);
205
        foreach ($records as $record) {
206
            $identifier = $record->items['ID']->content;
207
            $this->assertEquals($expecteduserids[$identifier], $record->userid);
208
 
209
            foreach ($expectedcontent[$identifier] as $field => $value) {
210
                $this->assertEquals($value, $record->items[$field]->content,
211
                    "The value of field \"$field\" for the record at position \"$identifier\" " .
212
                    "which is \"{$record->items[$field]->content}\" does not match the expected value \"$value\".");
213
            }
214
        }
215
    }
216
 
217
    /**
218
     * Test uploading entries for a data instance with a field 'Username' but only one occurrence in the csv file.
219
     *
220
     * This should test the corner case, in which a user has defined a data fields, which has the same name
221
     * as the current lang string for username. In that case, the only Username entry is used for the field.
222
     * The author should not be set.
223
     *
224
     * @throws coding_exception
225
     * @throws dml_exception
226
     */
227
    public function test_import_with_field_username_without_userdata(): void {
228
        [
229
            'data' => $data,
230
            'cm' => $cm,
231
            'teacher' => $teacher,
232
            'student' => $student,
233
        ] = $this->get_test_data();
234
        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
235
 
236
        // Add username field.
237
        $fieldrecord = new \stdClass();
238
        $fieldrecord->name = 'Username';
239
        $fieldrecord->type = 'text';
240
        $generator->create_field($fieldrecord, $data);
241
 
242
        $importer = new csv_entries_importer(__DIR__ . '/fixtures/test_data_import_with_userdata.csv',
243
            'test_data_import_with_userdata.csv');
244
        $importer->import_csv($cm, $data, 'UTF-8', 'comma');
245
 
246
        // No userdata is present in the file: Fallback is to assign the uploading user as author.
247
        $expecteduserids = array();
248
        $expecteduserids[1] = $teacher->id;
249
        $expecteduserids[2] = $teacher->id;
250
 
251
        $expectedcontent = array();
252
        $expectedcontent[1] = array(
253
            'Username' => 'student',
254
            'Param2' => 'My first entry',
255
        );
256
        $expectedcontent[2] = array(
257
            'Username' => 'student2',
258
            'Param2' => 'My second entry',
259
        );
260
 
261
        $records = $this->get_data_records($data->id);
262
        $this->assertCount(2, $records);
263
        foreach ($records as $record) {
264
            $identifier = $record->items['ID']->content;
265
            $this->assertEquals($expecteduserids[$identifier], $record->userid);
266
 
267
            foreach ($expectedcontent[$identifier] as $field => $value) {
268
                $this->assertEquals($value, $record->items[$field]->content,
269
                    "The value of field \"$field\" for the record at position \"$identifier\" " .
270
                    "which is \"{$record->items[$field]->content}\" does not match the expected value \"$value\".");
271
            }
272
        }
273
    }
274
 
275
    /**
276
     * Data provider for {@see test_import_without_approved}
277
     *
278
     * @return array[]
279
     */
280
    public static function import_without_approved_provider(): array {
281
        return [
282
            'Teacher can approve entries' => ['teacher', [1, 1]],
283
            'Student cannot approve entries' => ['student', [0, 0]],
284
        ];
285
    }
286
 
287
    /**
288
     * Test importing file without approved status column
289
     *
290
     * @param string $user
291
     * @param int[] $expected
292
     *
293
     * @dataProvider import_without_approved_provider
294
     */
295
    public function test_import_without_approved(string $user, array $expected): void {
296
        $testdata = $this->get_test_data();
297
        ['data' => $data, 'cm' => $cm] = $testdata;
298
 
299
        $this->setUser($testdata[$user]);
300
 
301
        $importer = new csv_entries_importer(__DIR__ . '/fixtures/test_data_import.csv', 'test_data_import.csv');
302
        $importer->import_csv($cm, $data, 'UTF-8', 'comma');
303
 
304
        $records = $this->get_data_records($data->id);
305
        $this->assertEquals($expected, array_column($records, 'approved'));
306
    }
307
 
308
    /**
309
     * Data provider for {@see test_import_with_approved}
310
     *
311
     * @return array[]
312
     */
313
    public static function import_with_approved_provider(): array {
314
        return [
315
            'Teacher can approve entries' => ['teacher', [1, 0]],
316
            'Student cannot approve entries' => ['student', [0, 0]],
317
        ];
318
    }
319
 
320
    /**
321
     * Test importing file with approved status column
322
     *
323
     * @param string $user
324
     * @param int[] $expected
325
     *
326
     * @dataProvider import_with_approved_provider
327
     */
328
    public function test_import_with_approved(string $user, array $expected): void {
329
        $testdata = $this->get_test_data();
330
        ['data' => $data, 'cm' => $cm] = $testdata;
331
 
332
        $this->setUser($testdata[$user]);
333
 
334
        $importer = new csv_entries_importer(__DIR__ . '/fixtures/test_data_import_with_approved.csv',
335
            'test_data_import_with_approved.csv');
336
        $importer->import_csv($cm, $data, 'UTF-8', 'comma');
337
 
338
        $records = $this->get_data_records($data->id);
339
        $this->assertEquals($expected, array_column($records, 'approved'));
340
    }
341
 
342
    /**
343
     * Tests the import including files from a zip archive.
344
     */
345
    public function test_import_with_files(): void {
346
        [
347
            'data' => $data,
348
            'cm' => $cm,
349
        ] = $this->get_test_data();
350
 
351
        $importer = new csv_entries_importer(__DIR__ . '/fixtures/test_data_import_with_files.zip',
352
            'test_data_import_with_files.zip');
353
        $importer->import_csv($cm, $data, 'UTF-8', 'comma');
354
 
355
        $records = $this->get_data_records($data->id);
356
        $ziparchive = new zip_archive();
357
        $ziparchive->open(__DIR__ . '/fixtures/test_data_import_with_files.zip');
358
 
359
        $importedcontent = array_values($records)[0]->items;
360
        $this->assertEquals(17, $importedcontent['ID']->content);
361
        $this->assertEquals('samplefile.png', $importedcontent['filefield']->content);
362
        $this->assertEquals('samplepicture.png', $importedcontent['picturefield']->content);
363
 
364
        // We now check if content of imported file from zip content is identical to the content of the file
365
        // stored in the mod_data record in the field 'filefield'.
366
        $fileindex = array_values(array_map(fn($file) => $file->index,
367
            array_filter($ziparchive->list_files(), fn($file) => $file->pathname === 'files/samplefile.png')))[0];
368
        $filestream = $ziparchive->get_stream($fileindex);
369
        $filefield = data_get_field_from_name('filefield', $data);
370
        $filefieldfilecontent = fread($filestream, $ziparchive->get_info($fileindex)->size);
371
        $this->assertEquals($filefield->get_file(array_keys($records)[0])->get_content(),
372
            $filefieldfilecontent);
373
        fclose($filestream);
374
 
375
        // We now check if content of imported picture from zip content is identical to the content of the picture file
376
        // stored in the mod_data record in the field 'picturefield'.
377
        $fileindex = array_values(array_map(fn($file) => $file->index,
378
            array_filter($ziparchive->list_files(), fn($file) => $file->pathname === 'files/samplepicture.png')))[0];
379
        $filestream = $ziparchive->get_stream($fileindex);
380
        $filefield = data_get_field_from_name('picturefield', $data);
381
        $filefieldfilecontent = fread($filestream, $ziparchive->get_info($fileindex)->size);
382
        $this->assertEquals($filefield->get_file(array_keys($records)[0])->get_content(),
383
            $filefieldfilecontent);
384
        fclose($filestream);
385
        $this->assertCount(1, $importer->get_added_records_messages());
386
        $ziparchive->close();
387
    }
388
 
389
    /**
390
     * Tests the import including files from a zip archive.
391
     */
392
    public function test_import_with_files_missing_file(): void {
393
        [
394
            'data' => $data,
395
            'cm' => $cm,
396
        ] = $this->get_test_data();
397
 
398
        $importer = new csv_entries_importer(__DIR__ . '/fixtures/test_data_import_with_files_missing_file.zip',
399
            'test_data_import_with_files_missing_file.zip');
400
        $importer->import_csv($cm, $data, 'UTF-8', 'comma');
401
 
402
        $records = $this->get_data_records($data->id);
403
        $ziparchive = new zip_archive();
404
        $ziparchive->open(__DIR__ . '/fixtures/test_data_import_with_files_missing_file.zip');
405
 
406
        $importedcontent = array_values($records)[0]->items;
407
        $this->assertEquals(17, $importedcontent['ID']->content);
408
        $this->assertFalse(isset($importedcontent['filefield']));
409
        $this->assertEquals('samplepicture.png', $importedcontent['picturefield']->content);
410
        $this->assertCount(1, $importer->get_added_records_messages());
411
        $ziparchive->close();
412
    }
413
 
414
    /**
415
     * Returns the records of the data instance.
416
     *
417
     * Each records has an item entry, which contains all fields associated with this item.
418
     * Each fields has the parameters name, type and content.
419
     *
420
     * @param int $dataid Id of the data instance.
421
     * @return array The records of the data instance.
422
     * @throws dml_exception
423
     */
424
    private function get_data_records(int $dataid): array {
425
        global $DB;
426
 
427
        $records = $DB->get_records('data_records', ['dataid' => $dataid]);
428
        foreach ($records as $record) {
429
            $sql = 'SELECT f.name, f.type, con.content FROM
430
                {data_content} con JOIN {data_fields} f ON con.fieldid = f.id
431
                WHERE con.recordid = :recordid';
432
            $items = $DB->get_records_sql($sql, array('recordid' => $record->id));
433
            $record->items = $items;
434
        }
435
        return $records;
436
    }
437
 
438
    /**
439
     * Tests if the amount of imported records is counted properly.
440
     *
441
     * @dataProvider get_added_record_messages_provider
442
     * @param string $datafilecontent the content of the datafile to test as string
443
     * @param int $expectedcount the expected count of messages depending on the datafile content
444
     */
445
    public function test_get_added_record_messages(string $datafilecontent, int $expectedcount): void {
446
        [
447
            'data' => $data,
448
            'cm' => $cm,
449
        ] = $this->get_test_data();
450
 
451
        // First we need to create the zip file from the provided data.
452
        $tmpdir = make_request_directory();
453
        $datafile = $tmpdir . '/entries_import_test_datafile_tmp_' . time() . '.csv';
454
        file_put_contents($datafile, $datafilecontent);
455
 
456
        $importer = new csv_entries_importer($datafile, 'testdatafile.csv');
457
        $importer->import_csv($cm, $data, 'UTF-8', 'comma');
458
        $this->assertEquals($expectedcount, count($importer->get_added_records_messages()));
459
    }
460
 
461
    /**
462
     * Data provider method for self::test_get_added_record_messages.
463
     *
464
     * @return array data for testing
465
     */
466
    public function get_added_record_messages_provider(): array {
467
        return [
468
            'only header' => [
469
                'datafilecontent' => 'ID,Param2,filefield,picturefield' . PHP_EOL,
470
                'expectedcount' => 0 // One line is being assumed to be the header.
471
            ],
472
            'one record' => [
473
                'datafilecontent' => 'ID,Param2,filefield,picturefield' . PHP_EOL
474
                    . '5,"some short text",testfilename.pdf,testpicture.png',
475
                'expectedcount' => 1
476
            ],
477
            'two records' => [
478
                'datafilecontent' => 'ID,Param2,filefield,picturefield' . PHP_EOL
479
                    . '5,"some short text",testfilename.pdf,testpicture.png' . PHP_EOL
480
                    . '3,"other text",testfilename2.pdf,testpicture2.png',
481
                'expectedcount' => 2
482
            ],
483
        ];
484
    }
485
}