Merge branch 'MDL-51128-master' of git://github.com/junpataleta/moodle
[moodle.git] / admin / tool / generator / classes / course_backend.php
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/>.
17 /**
18  * tool_generator course backend code.
19  *
20  * @package tool_generator
21  * @copyright 2013 The Open University
22  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 /**
28  * Backend code for the 'make large course' tool.
29  *
30  * @package tool_generator
31  * @copyright 2013 The Open University
32  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33  */
34 class tool_generator_course_backend extends tool_generator_backend {
35     /**
36      * @var array Number of sections in course
37      */
38     private static $paramsections = array(1, 10, 100, 500, 1000, 2000);
39     /**
40      * @var array Number of assignments in course
41      */
42     private static $paramassignments = array(1, 10, 100, 500, 1000, 2000);
43     /**
44      * @var array Number of Page activities in course
45      */
46     private static $parampages = array(1, 50, 200, 1000, 5000, 10000);
47     /**
48      * @var array Number of students enrolled in course
49      */
50     private static $paramusers = array(1, 100, 1000, 10000, 50000, 100000);
51     /**
52      * Total size of small files: 1KB, 1MB, 10MB, 100MB, 1GB, 2GB.
53      *
54      * @var array Number of small files created in a single file activity
55      */
56     private static $paramsmallfilecount = array(1, 64, 128, 1024, 16384, 32768);
57     /**
58      * @var array Size of small files (to make the totals into nice numbers)
59      */
60     private static $paramsmallfilesize = array(1024, 16384, 81920, 102400, 65536, 65536);
61     /**
62      * Total size of big files: 8KB, 8MB, 80MB, 800MB, 8GB, 16GB.
63      *
64      * @var array Number of big files created as individual file activities
65      */
66     private static $parambigfilecount = array(1, 2, 5, 10, 10, 10);
67     /**
68      * @var array Size of each large file
69      */
70     private static $parambigfilesize = array(8192, 4194304, 16777216, 83886080,
71             858993459, 1717986918);
72     /**
73      * @var array Number of forum discussions
74      */
75     private static $paramforumdiscussions = array(1, 10, 100, 500, 1000, 2000);
76     /**
77      * @var array Number of forum posts per discussion
78      */
79     private static $paramforumposts = array(2, 2, 5, 10, 10, 10);
81     /**
82      * @var string Course shortname
83      */
84     private $shortname;
86     /**
87      * @var string Course fullname.
88      */
89     private $fullname = "";
91     /**
92      * @var string Course summary.
93      */
94     private $summary = "";
96     /**
97      * @var string Course summary format, defaults to FORMAT_HTML.
98      */
99     private $summaryformat = FORMAT_HTML;
101     /**
102      * @var testing_data_generator Data generator
103      */
104     protected $generator;
106     /**
107      * @var stdClass Course object
108      */
109     private $course;
111     /**
112      * @var array Array from test user number (1...N) to userid in database
113      */
114     private $userids;
116     /**
117      * Constructs object ready to create course.
118      *
119      * @param string $shortname Course shortname
120      * @param int $size Size as numeric index
121      * @param bool $fixeddataset To use fixed or random data
122      * @param int|bool $filesizelimit The max number of bytes for a generated file
123      * @param bool $progress True if progress information should be displayed
124      */
125     public function __construct(
126         $shortname,
127         $size,
128         $fixeddataset = false,
129         $filesizelimit = false,
130         $progress = true,
131         $fullname = null,
132         $summary = null,
133         $summaryformat = FORMAT_HTML) {
135         // Set parameters.
136         $this->shortname = $shortname;
138         // We can't allow fullname to be set to an empty string.
139         if (empty($fullname)) {
140             $this->fullname = get_string(
141                 'fullname',
142                 'tool_generator',
143                 array(
144                     'size' => get_string('shortsize_' . $size, 'tool_generator')
145                 )
146             );
147         } else {
148             $this->fullname = $fullname;
149         }
151         // Summary, on the other hand, should be empty-able.
152         if (!is_null($summary)) {
153             $this->summary = $summary;
154             $this->summaryformat = $summaryformat;
155         }
157         parent::__construct($size, $fixeddataset, $filesizelimit, $progress);
158     }
160     /**
161      * Returns the relation between users and course sizes.
162      *
163      * @return array
164      */
165     public static function get_users_per_size() {
166         return self::$paramusers;
167     }
169     /**
170      * Gets a list of size choices supported by this backend.
171      *
172      * @return array List of size (int) => text description for display
173      */
174     public static function get_size_choices() {
175         $options = array();
176         for ($size = self::MIN_SIZE; $size <= self::MAX_SIZE; $size++) {
177             $options[$size] = get_string('coursesize_' . $size, 'tool_generator');
178         }
179         return $options;
180     }
182     /**
183      * Checks that a shortname is available (unused).
184      *
185      * @param string $shortname Proposed course shortname
186      * @return string An error message if the name is unavailable or '' if OK
187      */
188     public static function check_shortname_available($shortname) {
189         global $DB;
190         $fullname = $DB->get_field('course', 'fullname',
191                 array('shortname' => $shortname), IGNORE_MISSING);
192         if ($fullname !== false) {
193             // I wanted to throw an exception here but it is not possible to
194             // use strings from moodle.php in exceptions, and I didn't want
195             // to duplicate the string in tool_generator, so I changed this to
196             // not use exceptions.
197             return get_string('shortnametaken', 'moodle', $fullname);
198         }
199         return '';
200     }
202     /**
203      * Runs the entire 'make' process.
204      *
205      * @return int Course id
206      */
207     public function make() {
208         global $DB, $CFG;
209         require_once($CFG->dirroot . '/lib/phpunit/classes/util.php');
211         raise_memory_limit(MEMORY_EXTRA);
213         if ($this->progress && !CLI_SCRIPT) {
214             echo html_writer::start_tag('ul');
215         }
217         $entirestart = microtime(true);
219         // Start transaction.
220         $transaction = $DB->start_delegated_transaction();
222         // Get generator.
223         $this->generator = phpunit_util::get_data_generator();
225         // Make course.
226         $this->course = $this->create_course();
227         $this->create_users();
228         $this->create_assignments();
229         $this->create_pages();
230         $this->create_small_files();
231         $this->create_big_files();
232         $this->create_forum();
234         // Log total time.
235         $this->log('coursecompleted', round(microtime(true) - $entirestart, 1));
237         if ($this->progress && !CLI_SCRIPT) {
238             echo html_writer::end_tag('ul');
239         }
241         // Commit transaction and finish.
242         $transaction->allow_commit();
243         return $this->course->id;
244     }
246     /**
247      * Creates the actual course.
248      *
249      * @return stdClass Course record
250      */
251     private function create_course() {
252         $this->log('createcourse', $this->shortname);
253         $courserecord = array(
254             'shortname' => $this->shortname,
255             'fullname' => $this->fullname,
256             'numsections' => self::$paramsections[$this->size],
257             'startdate' => usergetmidnight(time())
258         );
259         if (strlen($this->summary) > 0) {
260             $courserecord['summary'] = $this->summary;
261             $courserecord['summary_format'] = $this->summaryformat;
262         }
264         return $this->generator->create_course($courserecord, array('createsections' => true));
265     }
267     /**
268      * Creates a number of user accounts and enrols them on the course.
269      * Note: Existing user accounts that were created by this system are
270      * reused if available.
271      */
272     private function create_users() {
273         global $DB;
275         // Work out total number of users.
276         $count = self::$paramusers[$this->size];
278         // Get existing users in order. We will 'fill up holes' in this up to
279         // the required number.
280         $this->log('checkaccounts', $count);
281         $nextnumber = 1;
282         $rs = $DB->get_recordset_select('user', $DB->sql_like('username', '?'),
283                 array('tool_generator_%'), 'username', 'id, username');
284         foreach ($rs as $rec) {
285             // Extract number from username.
286             $matches = array();
287             if (!preg_match('~^tool_generator_([0-9]{6})$~', $rec->username, $matches)) {
288                 continue;
289             }
290             $number = (int)$matches[1];
292             // Create missing users in range up to this.
293             if ($number != $nextnumber) {
294                 $this->create_user_accounts($nextnumber, min($number - 1, $count));
295             } else {
296                 $this->userids[$number] = (int)$rec->id;
297             }
299             // Stop if we've got enough users.
300             $nextnumber = $number + 1;
301             if ($number >= $count) {
302                 break;
303             }
304         }
305         $rs->close();
307         // Create users from end of existing range.
308         if ($nextnumber <= $count) {
309             $this->create_user_accounts($nextnumber, $count);
310         }
312         // Assign all users to course.
313         $this->log('enrol', $count, true);
315         $enrolplugin = enrol_get_plugin('manual');
316         $instances = enrol_get_instances($this->course->id, true);
317         foreach ($instances as $instance) {
318             if ($instance->enrol === 'manual') {
319                 break;
320             }
321         }
322         if ($instance->enrol !== 'manual') {
323             throw new coding_exception('No manual enrol plugin in course');
324         }
325         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
327         for ($number = 1; $number <= $count; $number++) {
328             // Enrol user.
329             $enrolplugin->enrol_user($instance, $this->userids[$number], $role->id);
330             $this->dot($number, $count);
331         }
333         // Sets the pointer at the beginning to be aware of the users we use.
334         reset($this->userids);
336         $this->end_log();
337     }
339     /**
340      * Creates user accounts with a numeric range.
341      *
342      * @param int $first Number of first user
343      * @param int $last Number of last user
344      */
345     private function create_user_accounts($first, $last) {
346         global $CFG;
348         $this->log('createaccounts', (object)array('from' => $first, 'to' => $last), true);
349         $count = $last - $first + 1;
350         $done = 0;
351         for ($number = $first; $number <= $last; $number++, $done++) {
352             // Work out username with 6-digit number.
353             $textnumber = (string)$number;
354             while (strlen($textnumber) < 6) {
355                 $textnumber = '0' . $textnumber;
356             }
357             $username = 'tool_generator_' . $textnumber;
359             // Create user account.
360             $record = array('username' => $username, 'idnumber' => $number);
362             // We add a user password if it has been specified.
363             if (!empty($CFG->tool_generator_users_password)) {
364                 $record['password'] = $CFG->tool_generator_users_password;
365             }
367             $user = $this->generator->create_user($record);
368             $this->userids[$number] = (int)$user->id;
369             $this->dot($done, $count);
370         }
371         $this->end_log();
372     }
374     /**
375      * Creates a number of Assignment activities.
376      */
377     private function create_assignments() {
378         // Set up generator.
379         $assigngenerator = $this->generator->get_plugin_generator('mod_assign');
381         // Create assignments.
382         $number = self::$paramassignments[$this->size];
383         $this->log('createassignments', $number, true);
384         for ($i = 0; $i < $number; $i++) {
385             $record = array('course' => $this->course);
386             $options = array('section' => $this->get_target_section());
387             $assigngenerator->create_instance($record, $options);
388             $this->dot($i, $number);
389         }
391         $this->end_log();
392     }
394     /**
395      * Creates a number of Page activities.
396      */
397     private function create_pages() {
398         // Set up generator.
399         $pagegenerator = $this->generator->get_plugin_generator('mod_page');
401         // Create pages.
402         $number = self::$parampages[$this->size];
403         $this->log('createpages', $number, true);
404         for ($i = 0; $i < $number; $i++) {
405             $record = array('course' => $this->course);
406             $options = array('section' => $this->get_target_section());
407             $pagegenerator->create_instance($record, $options);
408             $this->dot($i, $number);
409         }
411         $this->end_log();
412     }
414     /**
415      * Creates one resource activity with a lot of small files.
416      */
417     private function create_small_files() {
418         $count = self::$paramsmallfilecount[$this->size];
419         $this->log('createsmallfiles', $count, true);
421         // Create resource with default textfile only.
422         $resourcegenerator = $this->generator->get_plugin_generator('mod_resource');
423         $record = array('course' => $this->course,
424                 'name' => get_string('smallfiles', 'tool_generator'));
425         $options = array('section' => 0);
426         $resource = $resourcegenerator->create_instance($record, $options);
428         // Add files.
429         $fs = get_file_storage();
430         $context = context_module::instance($resource->cmid);
431         $filerecord = array('component' => 'mod_resource', 'filearea' => 'content',
432                 'contextid' => $context->id, 'itemid' => 0, 'filepath' => '/');
433         for ($i = 0; $i < $count; $i++) {
434             $filerecord['filename'] = 'smallfile' . $i . '.dat';
436             // Generate random binary data (different for each file so it
437             // doesn't compress unrealistically).
438             $data = self::get_random_binary($this->limit_filesize(self::$paramsmallfilesize[$this->size]));
440             $fs->create_file_from_string($filerecord, $data);
441             $this->dot($i, $count);
442         }
444         $this->end_log();
445     }
447     /**
448      * Creates a string of random binary data. The start of the string includes
449      * the current time, in an attempt to avoid large-scale repetition.
450      *
451      * @param int $length Number of bytes
452      * @return Random data
453      */
454     private static function get_random_binary($length) {
456         $data = microtime(true);
457         if (strlen($data) > $length) {
458             // Use last digits of data.
459             return substr($data, -$length);
460         }
461         $length -= strlen($data);
462         for ($j = 0; $j < $length; $j++) {
463             $data .= chr(rand(1, 255));
464         }
465         return $data;
466     }
468     /**
469      * Creates a number of resource activities with one big file each.
470      */
471     private function create_big_files() {
472         global $CFG;
474         // Work out how many files and how many blocks to use (up to 64KB).
475         $count = self::$parambigfilecount[$this->size];
476         $filesize = $this->limit_filesize(self::$parambigfilesize[$this->size]);
477         $blocks = ceil($filesize / 65536);
478         $blocksize = floor($filesize / $blocks);
480         $this->log('createbigfiles', $count, true);
482         // Prepare temp area.
483         $tempfolder = make_temp_directory('tool_generator');
484         $tempfile = $tempfolder . '/' . rand();
486         // Create resources and files.
487         $fs = get_file_storage();
488         $resourcegenerator = $this->generator->get_plugin_generator('mod_resource');
489         for ($i = 0; $i < $count; $i++) {
490             // Create resource.
491             $record = array('course' => $this->course,
492                     'name' => get_string('bigfile', 'tool_generator', $i));
493             $options = array('section' => $this->get_target_section());
494             $resource = $resourcegenerator->create_instance($record, $options);
496             // Write file.
497             $handle = fopen($tempfile, 'w');
498             if (!$handle) {
499                 throw new coding_exception('Failed to open temporary file');
500             }
501             for ($j = 0; $j < $blocks; $j++) {
502                 $data = self::get_random_binary($blocksize);
503                 fwrite($handle, $data);
504                 $this->dot($i * $blocks + $j, $count * $blocks);
505             }
506             fclose($handle);
508             // Add file.
509             $context = context_module::instance($resource->cmid);
510             $filerecord = array('component' => 'mod_resource', 'filearea' => 'content',
511                     'contextid' => $context->id, 'itemid' => 0, 'filepath' => '/',
512                     'filename' => 'bigfile' . $i . '.dat');
513             $fs->create_file_from_pathname($filerecord, $tempfile);
514         }
516         unlink($tempfile);
517         $this->end_log();
518     }
520     /**
521      * Creates one forum activity with a bunch of posts.
522      */
523     private function create_forum() {
524         global $DB;
526         $discussions = self::$paramforumdiscussions[$this->size];
527         $posts = self::$paramforumposts[$this->size];
528         $totalposts = $discussions * $posts;
530         $this->log('createforum', $totalposts, true);
532         // Create empty forum.
533         $forumgenerator = $this->generator->get_plugin_generator('mod_forum');
534         $record = array('course' => $this->course,
535                 'name' => get_string('pluginname', 'forum'));
536         $options = array('section' => 0);
537         $forum = $forumgenerator->create_instance($record, $options);
539         // Add discussions and posts.
540         $sofar = 0;
541         for ($i = 0; $i < $discussions; $i++) {
542             $record = array('forum' => $forum->id, 'course' => $this->course->id,
543                     'userid' => $this->get_target_user());
544             $discussion = $forumgenerator->create_discussion($record);
545             $parentid = $DB->get_field('forum_posts', 'id', array('discussion' => $discussion->id), MUST_EXIST);
546             $sofar++;
547             for ($j = 0; $j < $posts - 1; $j++, $sofar++) {
548                 $record = array('discussion' => $discussion->id,
549                         'userid' => $this->get_target_user(), 'parent' => $parentid);
550                 $forumgenerator->create_post($record);
551                 $this->dot($sofar, $totalposts);
552             }
553         }
555         $this->end_log();
556     }
558     /**
559      * Gets a section number.
560      *
561      * Depends on $this->fixeddataset.
562      *
563      * @return int A section number from 1 to the number of sections
564      */
565     private function get_target_section() {
567         if (!$this->fixeddataset) {
568             $key = rand(1, self::$paramsections[$this->size]);
569         } else {
570             // Using section 1.
571             $key = 1;
572         }
574         return $key;
575     }
577     /**
578      * Gets a user id.
579      *
580      * Depends on $this->fixeddataset.
581      *
582      * @return int A user id for a random created user
583      */
584     private function get_target_user() {
586         if (!$this->fixeddataset) {
587             $userid = $this->userids[rand(1, self::$paramusers[$this->size])];
588         } else if ($userid = current($this->userids)) {
589             // Moving pointer to the next user.
590             next($this->userids);
591         } else {
592             // Returning to the beginning if we reached the end.
593             $userid = reset($this->userids);
594         }
596         return $userid;
597     }
599     /**
600      * Restricts the binary file size if necessary
601      *
602      * @param int $length The total length
603      * @return int The limited length if a limit was specified.
604      */
605     private function limit_filesize($length) {
607         // Limit to $this->filesizelimit.
608         if (is_numeric($this->filesizelimit) && $length > $this->filesizelimit) {
609             $length = floor($this->filesizelimit);
610         }
612         return $length;
613     }