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
/**
18
 * Class that holds a tree of availability conditions.
19
 *
20
 * @package core_availability
21
 * @copyright 2014 The Open University
22
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace core_availability;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
/**
30
 * Class that holds a tree of availability conditions.
31
 *
32
 * The structure of this tree in JSON input data is:
33
 *
34
 * { op:'&', c:[] }
35
 *
36
 * where 'op' is one of the OP_xx constants and 'c' is an array of children.
37
 *
38
 * At the root level one of the following additional values must be included:
39
 *
40
 * op '|' or '!&'
41
 *   show:true
42
 *   Boolean value controlling whether a failed match causes the item to
43
 *   display to students with information, or be completely hidden.
44
 * op '&' or '!|'
45
 *   showc:[]
46
 *   Array of same length as c with booleans corresponding to each child; you
47
 *   can make it be hidden or shown depending on which one they fail. (Anything
48
 *   with false takes precedence.)
49
 *
50
 * @package core_availability
51
 * @copyright 2014 The Open University
52
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
53
 */
54
class tree extends tree_node {
55
    /** @var int Operator: AND */
56
    const OP_AND = '&';
57
    /** @var int Operator: OR */
58
    const OP_OR = '|';
59
    /** @var int Operator: NOT(AND) */
60
    const OP_NOT_AND = '!&';
61
    /** @var int Operator: NOT(OR) */
62
    const OP_NOT_OR = '!|';
63
 
64
    /** @var bool True if this tree is at root level */
65
    protected $root;
66
 
67
    /** @var string Operator type (OP_xx constant) */
68
    protected $op;
69
 
70
    /** @var tree_node[] Children in this branch (may be empty array if needed) */
71
    protected $children;
72
 
73
    /**
74
     * Array of 'show information or hide completely' options for each child.
75
     * This array is only set for the root tree if it is in AND or NOT OR mode,
76
     * otherwise it is null.
77
     *
78
     * @var bool[]
79
     */
80
    protected $showchildren;
81
 
82
    /**
83
     * Single 'show information or hide completely' option for tree. This option
84
     * is only set for the root tree if it is in OR or NOT AND mode, otherwise
85
     * it is true.
86
     *
87
     * @var bool
88
     */
89
    protected $show;
90
 
91
    /**
92
     * Display a representation of this tree (used for debugging).
93
     *
94
     * @return string Text representation of tree
95
     */
96
    public function __toString() {
97
        $result = '';
98
        if ($this->root && is_null($this->showchildren)) {
99
            $result .= $this->show ? '+' : '-';
100
        }
101
        $result .= $this->op . '(';
102
        $first = true;
103
        foreach ($this->children as $index => $child) {
104
            if ($first) {
105
                $first = false;
106
            } else {
107
                $result .= ',';
108
            }
109
            if (!is_null($this->showchildren)) {
110
                $result .= $this->showchildren[$index] ? '+' : '-';
111
            }
112
            $result .= (string)$child;
113
        }
114
        $result .= ')';
115
        return $result;
116
    }
117
 
118
    /**
119
     * Decodes availability structure.
120
     *
121
     * This function also validates the retrieved data as follows:
122
     * 1. Data that does not meet the API-defined structure causes a
123
     *    coding_exception (this should be impossible unless there is
124
     *    a system bug or somebody manually hacks the database).
125
     * 2. Data that meets the structure but cannot be implemented (e.g.
126
     *    reference to missing plugin or to module that doesn't exist) is
127
     *    either silently discarded (if $lax is true) or causes a
128
     *    coding_exception (if $lax is false).
129
     *
130
     * @see decode_availability
131
     * @param \stdClass $structure Structure (decoded from JSON)
132
     * @param boolean $lax If true, throw exceptions only for invalid structure
133
     * @param boolean $root If true, this is the root tree
134
     * @return tree Availability tree
135
     * @throws \coding_exception If data is not valid structure
136
     */
137
    public function __construct($structure, $lax = false, $root = true) {
138
        $this->root = $root;
139
 
140
        // Check object.
141
        if (!is_object($structure)) {
142
            throw new \coding_exception('Invalid availability structure (not object)');
143
        }
144
 
145
        // Extract operator.
146
        if (!isset($structure->op)) {
147
            throw new \coding_exception('Invalid availability structure (missing ->op)');
148
        }
149
        $this->op = $structure->op;
150
        if (!in_array($this->op, array(self::OP_AND, self::OP_OR,
151
                self::OP_NOT_AND, self::OP_NOT_OR), true)) {
152
            throw new \coding_exception('Invalid availability structure (unknown ->op)');
153
        }
154
 
155
        // For root tree, get show options.
156
        $this->show = true;
157
        $this->showchildren = null;
158
        if ($root) {
159
            if ($this->op === self::OP_AND || $this->op === self::OP_NOT_OR) {
160
                // Per-child show options.
161
                if (!isset($structure->showc)) {
162
                    throw new \coding_exception(
163
                            'Invalid availability structure (missing ->showc)');
164
                }
165
                if (!is_array($structure->showc)) {
166
                    throw new \coding_exception(
167
                            'Invalid availability structure (->showc not array)');
168
                }
169
                foreach ($structure->showc as $value) {
170
                    if (!is_bool($value)) {
171
                        throw new \coding_exception(
172
                                'Invalid availability structure (->showc value not bool)');
173
                    }
174
                }
175
                // Set it empty now - add corresponding ones later.
176
                $this->showchildren = array();
177
            } else {
178
                // Entire tree show option. (Note: This is because when you use
179
                // OR mode, say you have A OR B, the user does not meet conditions
180
                // for either A or B. A is set to 'show' and B is set to 'hide'.
181
                // But they don't have either, so how do we know which one to do?
182
                // There might as well be only one value.)
183
                if (!isset($structure->show)) {
184
                    throw new \coding_exception(
185
                            'Invalid availability structure (missing ->show)');
186
                }
187
                if (!is_bool($structure->show)) {
188
                    throw new \coding_exception(
189
                            'Invalid availability structure (->show not bool)');
190
                }
191
                $this->show = $structure->show;
192
            }
193
        }
194
 
195
        // Get list of enabled plugins.
196
        $pluginmanager = \core_plugin_manager::instance();
197
        $enabled = $pluginmanager->get_enabled_plugins('availability');
198
 
199
        // For unit tests, also allow the mock plugin type (even though it
200
        // isn't configured in the code as a proper plugin).
201
        if (PHPUNIT_TEST) {
202
            $enabled['mock'] = true;
203
        }
204
 
205
        // Get children.
206
        if (!isset($structure->c)) {
207
            throw new \coding_exception('Invalid availability structure (missing ->c)');
208
        }
209
        if (!is_array($structure->c)) {
210
            throw new \coding_exception('Invalid availability structure (->c not array)');
211
        }
212
        if (is_array($this->showchildren) && count($structure->showc) != count($structure->c)) {
213
            throw new \coding_exception('Invalid availability structure (->c, ->showc mismatch)');
214
        }
215
        $this->children = array();
216
        foreach ($structure->c as $index => $child) {
217
            if (!is_object($child)) {
218
                throw new \coding_exception('Invalid availability structure (child not object)');
219
            }
220
 
221
            // First see if it's a condition. These have a defined type.
222
            if (isset($child->type)) {
223
                // Look for a plugin of this type.
224
                $classname = '\availability_' . $child->type . '\condition';
225
                if (!array_key_exists($child->type, $enabled)) {
226
                    if ($lax) {
227
                        // On load of existing settings, ignore if class
228
                        // doesn't exist.
229
                        continue;
230
                    } else {
231
                        throw new \coding_exception('Unknown condition type: ' . $child->type);
232
                    }
233
                }
234
                $this->children[] = new $classname($child);
235
            } else {
236
                // Not a condition. Must be a subtree.
237
                $this->children[] = new tree($child, $lax, false);
238
            }
239
            if (!is_null($this->showchildren)) {
240
                $this->showchildren[] = $structure->showc[$index];
241
            }
242
        }
243
    }
244
 
245
    public function check_available($not, info $info, $grabthelot, $userid) {
246
        // If there are no children in this group, we just treat it as available.
247
        $information = '';
248
        if (!$this->children) {
249
            return new result(true);
250
        }
251
 
252
        // Get logic flags from operator.
253
        list($innernot, $andoperator) = $this->get_logic_flags($not);
254
 
255
        if ($andoperator) {
256
            $allow = true;
257
        } else {
258
            $allow = false;
259
        }
260
        $failedchildren = array();
261
        $totallyhide = !$this->show;
262
        foreach ($this->children as $index => $child) {
263
            // Check available and get info.
264
            $childresult = $child->check_available(
265
                    $innernot, $info, $grabthelot, $userid);
266
            $childyes = $childresult->is_available();
267
            if (!$childyes) {
268
                $failedchildren[] = $childresult;
269
                if (!is_null($this->showchildren) && !$this->showchildren[$index]) {
270
                    $totallyhide = true;
271
                }
272
            }
273
 
274
            if ($andoperator && !$childyes) {
275
                $allow = false;
276
                // Do not exit loop at this point, as we will still include other info.
277
            } else if (!$andoperator && $childyes) {
278
                // Exit loop since we are going to allow access (from this tree at least).
279
                $allow = true;
280
                break;
281
            }
282
        }
283
 
284
        if ($allow) {
285
            return new result(true);
286
        } else if ($totallyhide) {
287
            return new result(false);
288
        } else {
289
            return new result(false, $this, $failedchildren);
290
        }
291
    }
292
 
293
    public function is_applied_to_user_lists() {
294
        return true;
295
    }
296
 
297
    /**
298
     * Tests against a user list. Users who cannot access the activity due to
299
     * availability restrictions will be removed from the list.
300
     *
301
     * This test ONLY includes conditions which are marked as being applied to
302
     * user lists. For example, group conditions are included but date
303
     * conditions are not included.
304
     *
305
     * The function operates reasonably efficiently i.e. should not do per-user
306
     * database queries. It is however likely to be fairly slow.
307
     *
308
     * @param array $users Array of userid => object
309
     * @param bool $not If tree's parent indicates it's being checked negatively
310
     * @param info $info Info about current context
311
     * @param capability_checker $checker Capability checker
312
     * @return array Filtered version of input array
313
     */
314
    public function filter_user_list(array $users, $not, info $info,
315
            capability_checker $checker) {
316
        // Get logic flags from operator.
317
        list($innernot, $andoperator) = $this->get_logic_flags($not);
318
 
319
        if ($andoperator) {
320
            // For AND, start with the whole result and whittle it down.
321
            $result = $users;
322
        } else {
323
            // For OR, start with nothing.
324
            $result = array();
325
            $anyconditions = false;
326
        }
327
 
328
        // Loop through all valid children.
329
        foreach ($this->children as $index => $child) {
330
            if (!$child->is_applied_to_user_lists()) {
331
                if ($andoperator) {
332
                    continue;
333
                } else {
334
                    // OR condition with one option that doesn't restrict user
335
                    // lists = everyone is allowed.
336
                    $anyconditions = false;
337
                    break;
338
                }
339
            }
340
            $childresult = $child->filter_user_list($users, $innernot, $info, $checker);
341
            if ($andoperator) {
342
                $result = array_intersect_key($result, $childresult);
343
            } else {
344
                // Combine results into array.
345
                foreach ($childresult as $id => $user) {
346
                    $result[$id] = $user;
347
                }
348
                $anyconditions = true;
349
            }
350
        }
351
 
352
        // For OR operator, if there were no conditions just return input.
353
        if (!$andoperator && !$anyconditions) {
354
            return $users;
355
        } else {
356
            return $result;
357
        }
358
    }
359
 
360
    public function get_user_list_sql($not, info $info, $onlyactive) {
361
        global $DB;
362
        // Get logic flags from operator.
363
        list($innernot, $andoperator) = $this->get_logic_flags($not);
364
 
365
        // Loop through all valid children, getting SQL for each.
366
        $childresults = array();
367
        foreach ($this->children as $index => $child) {
368
            if (!$child->is_applied_to_user_lists()) {
369
                if ($andoperator) {
370
                    continue;
371
                } else {
372
                    // OR condition with one option that doesn't restrict user
373
                    // lists = everyone is allowed.
374
                    $childresults = array();
375
                    break;
376
                }
377
            }
378
            $childresult = $child->get_user_list_sql($innernot, $info, $onlyactive);
379
            if ($childresult[0]) {
380
                $childresults[] = $childresult;
381
            } else if (!$andoperator) {
382
                // When using OR operator, if any part doesn't have restrictions,
383
                // then nor does the whole thing.
384
                return array('', array());
385
            }
386
        }
387
 
388
        // If there are no conditions, return null.
389
        if (!$childresults) {
390
            return array('', array());
391
        }
392
        // If there is a single condition, return it.
393
        if (count($childresults) === 1) {
394
            return $childresults[0];
395
        }
396
 
397
        // Combine results using INTERSECT or UNION.
398
        $outsql = null;
399
        $subsql = array();
400
        $outparams = array();
401
        foreach ($childresults as $childresult) {
402
            $subsql[] = $childresult[0];
403
            $outparams = array_merge($outparams, $childresult[1]);
404
        }
405
        if ($andoperator) {
406
            $outsql = $DB->sql_intersect($subsql, 'id');
407
        } else {
408
            $outsql = '(' . join(') UNION (', $subsql) . ')';
409
        }
410
        return array($outsql, $outparams);
411
    }
412
 
413
    public function is_available_for_all($not = false) {
414
        // Get logic flags.
415
        list($innernot, $andoperator) = $this->get_logic_flags($not);
416
 
417
        // No children = always available.
418
        if (!$this->children) {
419
            return true;
420
        }
421
 
422
        // Check children.
423
        foreach ($this->children as $child) {
424
            $innerall = $child->is_available_for_all($innernot);
425
            if ($andoperator) {
426
                // When there is an AND operator, then any child that results
427
                // in unavailable status would cause the whole thing to be
428
                // unavailable.
429
                if (!$innerall) {
430
                    return false;
431
                }
432
            } else {
433
                // When there is an OR operator, then any child which must only
434
                // be available means the whole thing must be available.
435
                if ($innerall) {
436
                    return true;
437
                }
438
            }
439
        }
440
 
441
        // If we get to here then for an AND operator that means everything must
442
        // be available. From OR it means that everything must be possibly
443
        // not available.
444
        return $andoperator;
445
    }
446
 
447
    /**
448
     * Gets full information about this tree (including all children) as HTML
449
     * for display to staff.
450
     *
451
     * @param info $info Information about location of condition tree
452
     * @throws \coding_exception If you call on a non-root tree
453
     * @return string HTML data (empty string if none)
454
     */
455
    public function get_full_information(info $info) {
456
        if (!$this->root) {
457
            throw new \coding_exception('Only supported on root item');
458
        }
459
        return $this->get_full_information_recursive(false, $info, null, true);
460
    }
461
 
462
    /**
463
     * Gets information about this tree corresponding to the given result
464
     * object. (In other words, only conditions which the student actually
465
     * fails will be shown - and nothing if display is turned off.)
466
     *
467
     * @param info $info Information about location of condition tree
468
     * @param result $result Result object
469
     * @throws \coding_exception If you call on a non-root tree
470
     * @return string HTML data (empty string if none)
471
     */
472
    public function get_result_information(info $info, result $result) {
473
        if (!$this->root) {
474
            throw new \coding_exception('Only supported on root item');
475
        }
476
        return $this->get_full_information_recursive(false, $info, $result, true);
477
    }
478
 
479
    /**
480
     * Gets information about this tree (including all or selected children) as
481
     * HTML for display to staff or student.
482
     *
483
     * @param bool $not True if there is a NOT in effect
484
     * @param info $info Information about location of condition tree
485
     * @param result|null $result Result object if this is a student display, else null
486
     * @param bool $root True if this is the root item
487
     * @param bool $hidden Staff display; true if this tree has show=false (from parent)
488
     * @return string|renderable Information to render
489
     */
490
    protected function get_full_information_recursive(
491
            $not, info $info, ?result $result, $root, $hidden = false) {
492
        // Get list of children - either full list, or those which are shown.
493
        $children = $this->children;
494
        $staff = true;
495
        if ($result) {
496
            $children = $result->filter_nodes($children);
497
            $staff = false;
498
        }
499
 
500
        // If no children, return empty string.
501
        if (!$children) {
502
            return '';
503
        }
504
 
505
        list($innernot, $andoperator) = $this->get_logic_flags($not);
506
 
507
        // If there is only one child, don't bother displaying this tree
508
        // (AND and OR makes no difference). Recurse to the child if a tree,
509
        // otherwise display directly.
510
        if (count ($children) === 1) {
511
            $child = reset($children);
512
            if ($this->root && is_null($result)) {
513
                if (is_null($this->showchildren)) {
514
                    $childhidden = !$this->show;
515
                } else {
516
                    $childhidden = !$this->showchildren[0];
517
                }
518
            } else {
519
                $childhidden = $hidden;
520
            }
521
            if ($child instanceof tree) {
522
                return $child->get_full_information_recursive(
523
                        $innernot, $info, $result, $root, $childhidden);
524
            } else {
525
                if ($root) {
526
                    $result = $child->get_standalone_description($staff, $innernot, $info);
527
                } else {
528
                    $result = $child->get_description($staff, $innernot, $info);
529
                }
530
                if ($childhidden) {
531
                    $result .= ' ' . get_string('hidden_marker', 'availability');
532
                }
533
                return $result;
534
            }
535
        }
536
 
537
        // Multiple children, so prepare child messages (recursive).
538
        $items = array();
539
        $index = 0;
540
        foreach ($children as $child) {
541
            // Work out if this node is hidden (staff view only).
542
            $childhidden = $this->root && is_null($result) &&
543
                    !is_null($this->showchildren) && !$this->showchildren[$index];
544
            if ($child instanceof tree) {
545
                $items[] = $child->get_full_information_recursive(
546
                        $innernot, $info, $result, false, $childhidden);
547
            } else {
548
                $childdescription = $child->get_description($staff, $innernot, $info);
549
                if ($childhidden) {
550
                    $childdescription .= ' ' . get_string('hidden_marker', 'availability');
551
                }
552
                $items[] = $childdescription;
553
            }
554
            $index++;
555
        }
556
 
557
        // If showing output to staff, and root is set to hide completely,
558
        // then include this information in the message.
559
        if ($this->root) {
560
            $treehidden = !$this->show && is_null($result);
561
        } else {
562
            $treehidden = $hidden;
563
        }
564
 
565
        // Format output for display.
566
        return new \core_availability_multiple_messages($root, $andoperator, $treehidden, $items);
567
    }
568
 
569
    /**
570
     * Converts the operator for the tree into two flags used for computing
571
     * the result.
572
     *
573
     * The 2 flags are $innernot (whether to set $not when calling for children)
574
     * and $andoperator (whether to use AND or OR operator to combine children).
575
     *
576
     * @param bool $not Not flag passed to this tree
577
     * @return array Array of the 2 flags ($innernot, $andoperator)
578
     */
579
    public function get_logic_flags($not) {
580
        // Work out which type of logic to use for the group.
581
        switch($this->op) {
582
            case self::OP_AND:
583
            case self::OP_OR:
584
                $negative = false;
585
                break;
586
            case self::OP_NOT_AND:
587
            case self::OP_NOT_OR:
588
                $negative = true;
589
                break;
590
            default:
591
                throw new \coding_exception('Unknown operator');
592
        }
593
        switch($this->op) {
594
            case self::OP_AND:
595
            case self::OP_NOT_AND:
596
                $andoperator = true;
597
                break;
598
            case self::OP_OR:
599
            case self::OP_NOT_OR:
600
                $andoperator = false;
601
                break;
602
            default:
603
                throw new \coding_exception('Unknown operator');
604
        }
605
 
606
        // Select NOT (or not) for children. It flips if this is a 'not' group.
607
        $innernot = $negative ? !$not : $not;
608
 
609
        // Select operator to use for this group. If flips for negative, because:
610
        // NOT (a AND b) = (NOT a) OR (NOT b)
611
        // NOT (a OR b) = (NOT a) AND (NOT b).
612
        if ($innernot) {
613
            $andoperator = !$andoperator;
614
        }
615
        return array($innernot, $andoperator);
616
    }
617
 
618
    public function save() {
619
        $result = new \stdClass();
620
        $result->op = $this->op;
621
        // Only root tree has the 'show' options.
622
        if ($this->root) {
623
            if ($this->op === self::OP_AND || $this->op === self::OP_NOT_OR) {
624
                $result->showc = $this->showchildren;
625
            } else {
626
                $result->show = $this->show;
627
            }
628
        }
629
        $result->c = array();
630
        foreach ($this->children as $child) {
631
            $result->c[] = $child->save();
632
        }
633
        return $result;
634
    }
635
 
636
    /**
637
     * Checks whether this tree is empty (contains no children).
638
     *
639
     * @return boolean True if empty
640
     */
641
    public function is_empty() {
642
        return count($this->children) === 0;
643
    }
644
 
645
    /**
646
     * Recursively gets all children of a particular class (you can use a base
647
     * class to get all conditions, or a specific class).
648
     *
649
     * @param string $classname Full class name e.g. core_availability\condition
650
     * @return array Array of nodes of that type (flattened, not a tree any more)
651
     */
652
    public function get_all_children($classname) {
653
        $result = array();
654
        $this->recursive_get_all_children($classname, $result);
655
        return $result;
656
    }
657
 
658
    /**
659
     * Internal function that implements get_all_children efficiently.
660
     *
661
     * @param string $classname Full class name e.g. core_availability\condition
662
     * @param array $result Output array of nodes
663
     */
664
    protected function recursive_get_all_children($classname, array &$result) {
665
        foreach ($this->children as $child) {
666
            if (is_a($child, $classname)) {
667
                $result[] = $child;
668
            }
669
            if ($child instanceof tree) {
670
                $child->recursive_get_all_children($classname, $result);
671
            }
672
        }
673
    }
674
 
675
    public function update_after_restore($restoreid, $courseid,
676
            \base_logger $logger, $name) {
677
        $changed = false;
678
        foreach ($this->children as $index => $child) {
679
            if ($child->include_after_restore($restoreid, $courseid, $logger, $name,
680
                    info::get_restore_task($restoreid))) {
681
                $thischanged = $child->update_after_restore($restoreid, $courseid,
682
                        $logger, $name);
683
                $changed = $changed || $thischanged;
684
            } else {
685
                unset($this->children[$index]);
686
                unset($this->showchildren[$index]);
687
                $this->showchildren = !is_null($this->showchildren) ? array_values($this->showchildren) : null;
688
                $changed = true;
689
            }
690
        }
691
        return $changed;
692
    }
693
 
694
    public function update_dependency_id($table, $oldid, $newid) {
695
        $changed = false;
696
        foreach ($this->children as $child) {
697
            $thischanged = $child->update_dependency_id($table, $oldid, $newid);
698
            $changed = $changed || $thischanged;
699
        }
700
        return $changed;
701
    }
702
 
703
    /**
704
     * Returns a JSON object which corresponds to a tree.
705
     *
706
     * Intended for unit testing, as normally the JSON values are constructed
707
     * by JavaScript code.
708
     *
709
     * This function generates 'nested' (i.e. not root-level) trees.
710
     *
711
     * @param array $children Array of JSON objects from component children
712
     * @param string $op Operator (tree::OP_xx)
713
     * @return stdClass JSON object
714
     * @throws coding_exception If you get parameters wrong
715
     */
716
    public static function get_nested_json(array $children, $op = self::OP_AND) {
717
 
718
        // Check $op and work out its type.
719
        switch($op) {
720
            case self::OP_AND:
721
            case self::OP_NOT_OR:
722
            case self::OP_OR:
723
            case self::OP_NOT_AND:
724
                break;
725
            default:
726
                throw new \coding_exception('Invalid $op');
727
        }
728
 
729
        // Do simple tree.
730
        $result = new \stdClass();
731
        $result->op = $op;
732
        $result->c = $children;
733
        return $result;
734
    }
735
 
736
    /**
737
     * Returns a JSON object which corresponds to a tree at root level.
738
     *
739
     * Intended for unit testing, as normally the JSON values are constructed
740
     * by JavaScript code.
741
     *
742
     * The $show parameter can be a boolean for all OP_xx options. For OP_AND
743
     * and OP_NOT_OR where you have individual show options, you can specify
744
     * a boolean (same for all) or an array.
745
     *
746
     * @param array $children Array of JSON objects from component children
747
     * @param string $op Operator (tree::OP_xx)
748
     * @param bool|array $show Whether 'show' option is turned on (see above)
749
     * @return stdClass JSON object ready for encoding
750
     * @throws coding_exception If you get parameters wrong
751
     */
752
    public static function get_root_json(array $children, $op = self::OP_AND, $show = true) {
753
 
754
        // Get the basic object.
755
        $result = self::get_nested_json($children, $op);
756
 
757
        // Check $op type.
758
        switch($op) {
759
            case self::OP_AND:
760
            case self::OP_NOT_OR:
761
                $multishow = true;
762
                break;
763
            case self::OP_OR:
764
            case self::OP_NOT_AND:
765
                $multishow = false;
766
                break;
767
        }
768
 
769
        // Add show options depending on operator.
770
        if ($multishow) {
771
            if (is_bool($show)) {
772
                $result->showc = array_pad(array(), count($result->c), $show);
773
            } else if (is_array($show)) {
774
                // The JSON will break if anything isn't an actual bool, so check.
775
                foreach ($show as $item) {
776
                    if (!is_bool($item)) {
777
                        throw new \coding_exception('$show array members must be bool');
778
                    }
779
                }
780
                // Check the size matches.
781
                if (count($show) != count($result->c)) {
782
                    throw new \coding_exception('$show array size does not match $children');
783
                }
784
                $result->showc = $show;
785
            } else {
786
                throw new \coding_exception('$show must be bool or array');
787
            }
788
        } else {
789
            if (!is_bool($show)) {
790
                throw new \coding_exception('For this operator, $show must be bool');
791
            }
792
            $result->show = $show;
793
        }
794
 
795
        return $result;
796
    }
797
}