Merge branch 'MDL-51128-master' of git://github.com/junpataleta/moodle
[moodle.git] / admin / tool / generator / classes / course_backend.php
CommitLineData
8cac8d3e
DM
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/**
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 */
24
25defined('MOODLE_INTERNAL') || die();
26
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 */
34class 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);
f0d2cb3e
RL
39 /**
40 * @var array Number of assignments in course
41 */
42 private static $paramassignments = array(1, 10, 100, 500, 1000, 2000);
8cac8d3e
DM
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);
80
81 /**
82 * @var string Course shortname
83 */
84 private $shortname;
85
343b11a3
JC
86 /**
87 * @var string Course fullname.
88 */
89 private $fullname = "";
90
91 /**
92 * @var string Course summary.
93 */
94 private $summary = "";
95
96 /**
97 * @var string Course summary format, defaults to FORMAT_HTML.
98 */
99 private $summaryformat = FORMAT_HTML;
100
8cac8d3e
DM
101 /**
102 * @var testing_data_generator Data generator
103 */
104 protected $generator;
105
106 /**
107 * @var stdClass Course object
108 */
109 private $course;
110
111 /**
112 * @var array Array from test user number (1...N) to userid in database
113 */
114 private $userids;
115
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
53e6d79c 122 * @param int|bool $filesizelimit The max number of bytes for a generated file
8cac8d3e 123 * @param bool $progress True if progress information should be displayed
8cac8d3e 124 */
343b11a3
JC
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) {
8cac8d3e
DM
134
135 // Set parameters.
136 $this->shortname = $shortname;
728e2728
JC
137
138 // We can't allow fullname to be set to an empty string.
139 if (empty($fullname)) {
343b11a3
JC
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 }
728e2728
JC
150
151 // Summary, on the other hand, should be empty-able.
343b11a3
JC
152 if (!is_null($summary)) {
153 $this->summary = $summary;
154 $this->summaryformat = $summaryformat;
155 }
8cac8d3e 156
53e6d79c 157 parent::__construct($size, $fixeddataset, $filesizelimit, $progress);
8cac8d3e
DM
158 }
159
217a5f97
DM
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 }
168
8cac8d3e
DM
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 }
181
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 }
201
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');
210
211 raise_memory_limit(MEMORY_EXTRA);
212
213 if ($this->progress && !CLI_SCRIPT) {
214 echo html_writer::start_tag('ul');
215 }
216
217 $entirestart = microtime(true);
218
219 // Start transaction.
220 $transaction = $DB->start_delegated_transaction();
221
222 // Get generator.
223 $this->generator = phpunit_util::get_data_generator();
224
225 // Make course.
226 $this->course = $this->create_course();
227 $this->create_users();
f0d2cb3e 228 $this->create_assignments();
8cac8d3e
DM
229 $this->create_pages();
230 $this->create_small_files();
231 $this->create_big_files();
232 $this->create_forum();
233
234 // Log total time.
235 $this->log('coursecompleted', round(microtime(true) - $entirestart, 1));
236
237 if ($this->progress && !CLI_SCRIPT) {
238 echo html_writer::end_tag('ul');
239 }
240
241 // Commit transaction and finish.
242 $transaction->allow_commit();
243 return $this->course->id;
244 }
245
246 /**
247 * Creates the actual course.
248 *
249 * @return stdClass Course record
250 */
251 private function create_course() {
252 $this->log('createcourse', $this->shortname);
343b11a3
JC
253 $courserecord = array(
254 'shortname' => $this->shortname,
255 'fullname' => $this->fullname,
cef93f97
JP
256 'numsections' => self::$paramsections[$this->size],
257 'startdate' => usergetmidnight(time())
343b11a3
JC
258 );
259 if (strlen($this->summary) > 0) {
260 $courserecord['summary'] = $this->summary;
261 $courserecord['summary_format'] = $this->summaryformat;
262 }
263
8cac8d3e
DM
264 return $this->generator->create_course($courserecord, array('createsections' => true));
265 }
266
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;
274
275 // Work out total number of users.
276 $count = self::$paramusers[$this->size];
277
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];
291
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 }
298
299 // Stop if we've got enough users.
300 $nextnumber = $number + 1;
301 if ($number >= $count) {
302 break;
303 }
304 }
305 $rs->close();
306
307 // Create users from end of existing range.
308 if ($nextnumber <= $count) {
309 $this->create_user_accounts($nextnumber, $count);
310 }
311
312 // Assign all users to course.
313 $this->log('enrol', $count, true);
314
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);
326
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 }
332
333 // Sets the pointer at the beginning to be aware of the users we use.
334 reset($this->userids);
335
336 $this->end_log();
337 }
338
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) {
217a5f97
DM
346 global $CFG;
347
8cac8d3e
DM
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;
358
359 // Create user account.
e3b31733 360 $record = array('username' => $username, 'idnumber' => $number);
217a5f97
DM
361
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 }
366
8cac8d3e
DM
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 }
373
f0d2cb3e
RL
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');
380
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 }
390
391 $this->end_log();
392 }
393
8cac8d3e
DM
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');
400
401 // Create pages.
402 $number = self::$parampages[$this->size];
403 $this->log('createpages', $number, true);
1325d493 404 for ($i = 0; $i < $number; $i++) {
7cff3faa 405 $record = array('course' => $this->course);
8cac8d3e
DM
406 $options = array('section' => $this->get_target_section());
407 $pagegenerator->create_instance($record, $options);
408 $this->dot($i, $number);
409 }
410
411 $this->end_log();
412 }
413
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);
420
421 // Create resource with default textfile only.
422 $resourcegenerator = $this->generator->get_plugin_generator('mod_resource');
7cff3faa 423 $record = array('course' => $this->course,
8cac8d3e
DM
424 'name' => get_string('smallfiles', 'tool_generator'));
425 $options = array('section' => 0);
426 $resource = $resourcegenerator->create_instance($record, $options);
427
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';
435
436 // Generate random binary data (different for each file so it
437 // doesn't compress unrealistically).
53e6d79c 438 $data = self::get_random_binary($this->limit_filesize(self::$paramsmallfilesize[$this->size]));
8cac8d3e
DM
439
440 $fs->create_file_from_string($filerecord, $data);
441 $this->dot($i, $count);
442 }
443
444 $this->end_log();
445 }
446
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) {
53e6d79c 455
8cac8d3e
DM
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);
1325d493 462 for ($j = 0; $j < $length; $j++) {
8cac8d3e
DM
463 $data .= chr(rand(1, 255));
464 }
465 return $data;
466 }
467
468 /**
469 * Creates a number of resource activities with one big file each.
470 */
471 private function create_big_files() {
472 global $CFG;
473
474 // Work out how many files and how many blocks to use (up to 64KB).
475 $count = self::$parambigfilecount[$this->size];
53e6d79c
DM
476 $filesize = $this->limit_filesize(self::$parambigfilesize[$this->size]);
477 $blocks = ceil($filesize / 65536);
478 $blocksize = floor($filesize / $blocks);
8cac8d3e
DM
479
480 $this->log('createbigfiles', $count, true);
481
482 // Prepare temp area.
483 $tempfolder = make_temp_directory('tool_generator');
484 $tempfile = $tempfolder . '/' . rand();
485
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.
7cff3faa 491 $record = array('course' => $this->course,
8cac8d3e
DM
492 'name' => get_string('bigfile', 'tool_generator', $i));
493 $options = array('section' => $this->get_target_section());
494 $resource = $resourcegenerator->create_instance($record, $options);
495
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);
507
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 }
515
516 unlink($tempfile);
517 $this->end_log();
518 }
519
520 /**
521 * Creates one forum activity with a bunch of posts.
522 */
523 private function create_forum() {
524 global $DB;
525
526 $discussions = self::$paramforumdiscussions[$this->size];
527 $posts = self::$paramforumposts[$this->size];
528 $totalposts = $discussions * $posts;
529
530 $this->log('createforum', $totalposts, true);
531
532 // Create empty forum.
533 $forumgenerator = $this->generator->get_plugin_generator('mod_forum');
7cff3faa 534 $record = array('course' => $this->course,
8cac8d3e
DM
535 'name' => get_string('pluginname', 'forum'));
536 $options = array('section' => 0);
537 $forum = $forumgenerator->create_instance($record, $options);
538
539 // Add discussions and posts.
540 $sofar = 0;
1325d493 541 for ($i = 0; $i < $discussions; $i++) {
8cac8d3e
DM
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++;
1325d493 547 for ($j = 0; $j < $posts - 1; $j++, $sofar++) {
8cac8d3e
DM
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 }
554
555 $this->end_log();
556 }
557
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() {
566
567 if (!$this->fixeddataset) {
568 $key = rand(1, self::$paramsections[$this->size]);
569 } else {
570 // Using section 1.
571 $key = 1;
572 }
573
574 return $key;
575 }
576
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() {
585
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 }
595
596 return $userid;
597 }
598
53e6d79c
DM
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) {
606
607 // Limit to $this->filesizelimit.
608 if (is_numeric($this->filesizelimit) && $length > $this->filesizelimit) {
609 $length = floor($this->filesizelimit);
610 }
611
612 return $length;
613 }
614
8cac8d3e 615}