Proyectos de Subversion Moodle

Rev

Rev 11 | Mostrar el archivo completo | | | Autoría | Ultima modificación | Ver Log |

Rev 11 Rev 1441
Línea 231... Línea 231...
231
 * 1/ All questions are deleted for this question category.
231
 * 1/ All questions are deleted for this question category.
232
 * 2/ Any questions that can't be deleted are moved to a new category
232
 * 2/ Any questions that can't be deleted are moved to a new category
233
 * NOTE: this function is called from lib/db/upgrade.php
233
 * NOTE: this function is called from lib/db/upgrade.php
234
 *
234
 *
235
 * @param object|core_course_category $category course category object
235
 * @param object|core_course_category $category course category object
-
 
236
 * @param bool $coursedeletion Is the course this category is under being deleted? If so, move saved questions to the site course.
236
 */
237
 */
237
function question_category_delete_safe($category): void {
238
function question_category_delete_safe($category, bool $coursedeletion = false): void {
238
    global $DB;
239
    global $DB;
239
    $criteria = ['questioncategoryid' => $category->id];
240
    $criteria = ['questioncategoryid' => $category->id];
240
    $context = context::instance_by_id($category->contextid, IGNORE_MISSING);
241
    $context = context::instance_by_id($category->contextid, IGNORE_MISSING);
241
    $rescue = null; // See the code around the call to question_save_from_deletion.
242
    $rescue = null; // See the code around the call to question_save_from_deletion.
Línea 264... Línea 265...
264
            foreach ($versions as $key => $version) {
265
            foreach ($versions as $key => $version) {
265
                $questionids[$key] = $version;
266
                $questionids[$key] = $version;
266
            }
267
            }
267
        }
268
        }
268
        if (!empty($questionids)) {
269
        if (!empty($questionids)) {
269
            $parentcontextid = SYSCONTEXTID;
-
 
270
            $name = get_string('unknown', 'question');
270
            $name = get_string('unknown', 'question');
271
            if ($context !== false) {
271
            if ($context !== false) {
272
                $name = $context->get_context_name();
272
                $name = $context->get_context_name();
273
                $parentcontext = $context->get_parent_context();
273
                $parentcontext = $context->get_course_context(false);
274
                if ($parentcontext) {
-
 
275
                    $parentcontextid = $parentcontext->id;
274
                $course = ($parentcontext && !$coursedeletion) ? get_course($parentcontext->instanceid) : get_site();
276
                }
-
 
277
            }
275
            }
-
 
276
            $qbank = core_question\local\bank\question_bank_helper::get_default_open_instance_system_type($course, true);
278
            question_save_from_deletion(array_keys($questionids), $parentcontextid, $name, $rescue);
277
            question_save_from_deletion(array_keys($questionids), $qbank->context->id, $name, $rescue);
279
        }
278
        }
280
    }
279
    }
Línea 281... Línea 280...
281
 
280
 
282
    // Now delete the category.
281
    // Now delete the category.
Línea 422... Línea 421...
422
 
421
 
423
/**
422
/**
424
 * All question categories and their questions are deleted for this context id.
423
 * All question categories and their questions are deleted for this context id.
425
 *
424
 *
-
 
425
 * @param int $contextid The contextid to delete question categories from
426
 * @param int $contextid The contextid to delete question categories from
426
 * @param bool $coursedeletion Are we calling this as part of deleting the course the context is under?
427
 * @return array only returns an empty array for backwards compatibility.
427
 * @return array only returns an empty array for backwards compatibility.
428
 */
428
 */
429
function question_delete_context($contextid): array {
429
function question_delete_context($contextid, bool $coursedeletion = false): array {
Línea 430... Línea 430...
430
    global $DB;
430
    global $DB;
431
 
431
 
432
    $fields = 'id, parent, name, contextid';
432
    $fields = 'id, parent, name, contextid';
433
    if ($categories = $DB->get_records('question_categories', ['contextid' => $contextid], 'parent', $fields)) {
433
    if ($categories = $DB->get_records('question_categories', ['contextid' => $contextid], 'parent', $fields)) {
434
        // Sort categories following their tree (parent-child) relationships this will make the feedback more readable.
434
        // Sort categories following their tree (parent-child) relationships this will make the feedback more readable.
435
        $categories = sort_categories_by_tree($categories);
435
        $categories = sort_categories_by_tree($categories);
436
        foreach ($categories as $category) {
436
        foreach ($categories as $category) {
437
            question_category_delete_safe($category);
437
            question_category_delete_safe($category, $coursedeletion);
438
        }
438
        }
439
    }
439
    }
Línea 440... Línea 440...
440
    return [];
440
    return [];
441
}
-
 
442
 
-
 
443
/**
-
 
444
 * All question categories and their questions are deleted for this course.
-
 
445
 *
-
 
446
 * @param stdClass $course an object representing the activity
-
 
447
 * @param bool $notused this argument is not used any more. Kept for backwards compatibility.
-
 
448
 * @return bool always true.
-
 
449
 */
-
 
450
function question_delete_course($course, $notused = false): bool {
-
 
451
    $coursecontext = context_course::instance($course->id);
-
 
452
    question_delete_context($coursecontext->id);
-
 
453
    return true;
-
 
454
}
-
 
455
 
-
 
456
/**
-
 
457
 * Category is about to be deleted,
-
 
458
 * 1/ All question categories and their questions are deleted for this course category.
-
 
459
 * 2/ All questions are moved to new category
-
 
460
 *
-
 
461
 * @param stdClass|core_course_category $category course category object
-
 
462
 * @param stdClass|core_course_category $newcategory empty means everything deleted, otherwise id of
-
 
463
 *      category where content moved
-
 
464
 * @param bool $notused this argument is no longer used. Kept for backwards compatibility.
-
 
465
 * @return boolean
-
 
466
 */
-
 
467
function question_delete_course_category($category, $newcategory, $notused=false): bool {
-
 
468
    global $DB;
-
 
469
 
-
 
470
    $context = context_coursecat::instance($category->id);
-
 
471
    if (empty($newcategory)) {
-
 
472
        question_delete_context($context->id);
-
 
473
 
-
 
474
    } else {
-
 
475
        // Move question categories to the new context.
-
 
476
        if (!$newcontext = context_coursecat::instance($newcategory->id)) {
-
 
477
            return false;
-
 
478
        }
-
 
479
 
-
 
480
        // Only move question categories if there is any question category at all!
-
 
481
        if ($topcategory = question_get_top_category($context->id)) {
-
 
482
            $newtopcategory = question_get_top_category($newcontext->id, true);
-
 
483
 
-
 
484
            question_move_category_to_context($topcategory->id, $context->id, $newcontext->id);
-
 
485
            $DB->set_field('question_categories', 'parent', $newtopcategory->id, ['parent' => $topcategory->id]);
-
 
486
            // Now delete the top category.
-
 
487
            $DB->delete_records('question_categories', ['id' => $topcategory->id]);
-
 
488
        }
-
 
489
    }
-
 
490
 
-
 
491
    return true;
-
 
492
}
441
}
493
 
442
 
494
/**
443
/**
495
 * Creates a new category to save the questions in use.
444
 * Creates a new category to save the questions in use.
496
 *
445
 *
Línea 502... Línea 451...
502
 * @return mixed false on
451
 * @return mixed false on
503
 */
452
 */
504
function question_save_from_deletion($questionids, $newcontextid, $oldplace, $newcategory = null) {
453
function question_save_from_deletion($questionids, $newcontextid, $oldplace, $newcategory = null) {
505
    global $DB;
454
    global $DB;
Línea -... Línea 455...
-
 
455
 
-
 
456
    $newcontext = context::instance_by_id($newcontextid);
-
 
457
    if ($newcontext->contextlevel !== CONTEXT_MODULE) {
-
 
458
        throw new moodle_exception("Invalid contextlevel: {$newcontext->contextlevel} for \$newcontextid {$newcontextid}");
-
 
459
    }
506
 
460
 
507
    // Make a category in the parent context to move the questions to.
461
    // Make a category in the parent context to move the questions to.
508
    if (is_null($newcategory)) {
462
    if (is_null($newcategory)) {
509
        $newcategory = new stdClass();
463
        $newcategory = new stdClass();
510
        $newcategory->parent = question_get_top_category($newcontextid, true)->id;
464
        $newcategory->parent = question_get_top_category($newcontextid, true)->id;
Línea 527... Línea 481...
527
/**
481
/**
528
 * All question categories and their questions are deleted for this activity.
482
 * All question categories and their questions are deleted for this activity.
529
 *
483
 *
530
 * @param object $cm the course module object representing the activity
484
 * @param object $cm the course module object representing the activity
531
 * @param bool $notused the argument is not used any more. Kept for backwards compatibility.
485
 * @param bool $notused the argument is not used any more. Kept for backwards compatibility.
-
 
486
 * @param bool $coursedeletion Are we calling this as part of deleting the course the activity belongs to?
532
 * @return boolean
487
 * @return boolean
533
 */
488
 */
534
function question_delete_activity($cm, $notused = false): bool {
489
function question_delete_activity($cm, $notused = false, bool $coursedeletion = false): bool {
535
    $modcontext = context_module::instance($cm->id);
490
    $modcontext = context_module::instance($cm->id);
536
    question_delete_context($modcontext->id);
491
    question_delete_context($modcontext->id, $coursedeletion);
537
    return true;
492
    return true;
538
}
493
}
Línea 539... Línea 494...
539
 
494
 
540
/**
495
/**
541
 * This function will handle moving all tag instances to a new context for a
496
 * This function will handle moving all tag instances to a new context for a
542
 * given list of questions.
497
 * given list of questions.
543
 *
-
 
544
 * Questions can be tagged in up to two contexts:
-
 
545
 * 1.) The context the question exists in.
-
 
546
 * 2.) The course context (if the question context is a higher context.
-
 
547
 *     E.g. course category context or system context.
-
 
548
 *
-
 
549
 * This means a question that exists in a higher context (e.g. course cat or
-
 
550
 * system context) may have multiple groups of tags in any number of child
-
 
551
 * course contexts.
-
 
552
 *
-
 
553
 * Questions in the course category context can be move "down" a context level
-
 
554
 * into one of their child course contexts or activity contexts which affects the
-
 
555
 * availability of that question in other courses / activities.
-
 
556
 *
-
 
557
 * In this case it makes the questions no longer available in the other course or
-
 
558
 * activity contexts so we need to make sure that the tag instances in those other
-
 
559
 * contexts are removed.
-
 
560
 *
498
 *
561
 * @param stdClass[] $questions The list of question being moved (must include
499
 * @param stdClass[] $questions The list of question being moved (must include
562
 *                              the id and contextid)
500
 *                              the id and contextid)
563
 * @param context $newcontext The Moodle context the questions are being moved to
501
 * @param context $newcontext The Moodle context the questions are being moved to, must be module context.
564
 */
502
 */
565
function question_move_question_tags_to_new_context(array $questions, context $newcontext): void {
-
 
-
 
503
function question_move_question_tags_to_new_context(array $questions, context $newcontext): void {
566
    // If the questions are moving to a new course/activity context then we need to
504
 
567
    // find any existing tag instances from any unavailable course contexts and
505
    if ($newcontext->contextlevel !== CONTEXT_MODULE) {
568
    // delete them because they will no longer be applicable (we don't support
-
 
569
    // tagging questions across courses).
506
        debugging("Invalid contextlevel: {$newcontext->contextlevel}", DEBUG_DEVELOPER);
-
 
507
    }
570
    $instancestodelete = [];
508
 
571
    $instancesfornewcontext = [];
-
 
572
    $newcontextparentids = $newcontext->get_parent_context_ids();
509
    $instancesfornewcontext = [];
573
    $questionids = array_map(function($question) {
510
    $questionids = array_map(function($question) {
574
        return $question->id;
511
        return $question->id;
575
    }, $questions);
512
    }, $questions);
Línea 580... Línea 517...
580
 
517
 
581
        foreach ($tagobjects as $tagobject) {
518
        foreach ($tagobjects as $tagobject) {
582
            $tagid = $tagobject->taginstanceid;
519
            $tagid = $tagobject->taginstanceid;
583
            $tagcontextid = $tagobject->taginstancecontextid;
520
            $tagcontextid = $tagobject->taginstancecontextid;
584
            $istaginnewcontext = $tagcontextid == $newcontext->id;
-
 
Línea 585... Línea 521...
585
            $istaginquestioncontext = $tagcontextid == $question->contextid;
521
            $istaginnewcontext = $tagcontextid == $newcontext->id;
586
 
522
 
587
            if ($istaginnewcontext) {
523
            if ($istaginnewcontext) {
588
                // This tag instance is already in the correct context so we can
524
                // This tag instance is already in the correct context so we can
589
                // ignore it.
525
                // ignore it.
Línea 590... Línea -...
590
                continue;
-
 
591
            }
-
 
592
 
-
 
593
            if ($istaginquestioncontext) {
-
 
594
                // This tag instance is in the question context so it needs to be
-
 
595
                // updated.
-
 
596
                $instancesfornewcontext[] = $tagid;
-
 
597
                continue;
-
 
598
            }
-
 
599
 
-
 
600
            // These tag instances are in neither the new context nor the
-
 
601
            // question context so we need to determine what to do based on
-
 
602
            // the context they are in and the new question context.
-
 
603
            $tagcontext = context::instance_by_id($tagcontextid);
-
 
604
            $tagcoursecontext = $tagcontext->get_course_context(false);
-
 
605
            // The tag is in a course context if get_course_context() returns
-
 
606
            // itself.
-
 
607
            $istaginstancecontextcourse = !empty($tagcoursecontext)
-
 
608
                && $tagcontext->id == $tagcoursecontext->id;
-
 
609
 
-
 
610
            if ($istaginstancecontextcourse) {
-
 
611
                // If the tag instance is in a course context we need to add some
-
 
612
                // special handling.
-
 
613
                $tagcontextparentids = $tagcontext->get_parent_context_ids();
-
 
614
                $isnewcontextaparent = in_array($newcontext->id, $tagcontextparentids);
-
 
615
                $isnewcontextachild = in_array($tagcontext->id, $newcontextparentids);
-
 
616
 
-
 
617
                if ($isnewcontextaparent) {
-
 
618
                    // If the tag instance is a course context tag and the new
-
 
619
                    // context is still a parent context to the tag context then
-
 
620
                    // we can leave this tag where it is.
-
 
621
                    continue;
-
 
622
                } else if ($isnewcontextachild) {
-
 
623
                    // If the new context is a child context (e.g. activity) of this
-
 
624
                    // tag instance then we should move all of this tag instance
-
 
625
                    // down into the activity context along with the question.
-
 
626
                    $instancesfornewcontext[] = $tagid;
-
 
627
                } else {
-
 
628
                    // If the tag is in a course context that is no longer a parent
-
 
629
                    // or child of the new context then this tag instance should be
-
 
630
                    // removed.
-
 
631
                    $instancestodelete[] = $tagid;
-
 
632
                }
-
 
633
            } else {
-
 
634
                // This is a catch all for any tag instances not in the question
-
 
635
                // context or a course context. These tag instances should be
526
                continue;
636
                // updated to the new context id. This will clean up old invalid
-
 
637
                // data.
527
            }
638
                $instancesfornewcontext[] = $tagid;
528
 
Línea 639... Línea -...
639
            }
-
 
640
        }
-
 
641
    }
-
 
642
 
-
 
643
    if (!empty($instancestodelete)) {
-
 
644
        // Delete any course context tags that may no longer be valid.
529
            $instancesfornewcontext[] = $tagid;
645
        core_tag_tag::delete_instances_by_id($instancestodelete);
530
        }
646
    }
531
    }
647
 
532
 
648
    if (!empty($instancesfornewcontext)) {
533
    if (!empty($instancesfornewcontext)) {
Línea 780... Línea 665...
780
    global $DB;
665
    global $DB;
Línea 781... Línea 666...
781
 
666
 
782
    if ($delete || $oldcontextid !== $newcontextid) {
667
    if ($delete || $oldcontextid !== $newcontextid) {
783
        $setreferences = $DB->get_recordset('question_set_references', ['questionscontextid' => $oldcontextid]);
668
        $setreferences = $DB->get_recordset('question_set_references', ['questionscontextid' => $oldcontextid]);
784
        foreach ($setreferences as $setreference) {
669
        foreach ($setreferences as $setreference) {
785
            $filter = json_decode($setreference->filtercondition);
670
            $filter = json_decode($setreference->filtercondition, true);
786
            if (isset($filter->questioncategoryid)) {
671
            if (isset($filter['questioncategoryid'])) {
-
 
672
                $filter = question_reference_manager::convert_legacy_set_reference_filter_condition($filter);
787
                if ((int)$filter->questioncategoryid === $oldcategoryid) {
673
            }
788
                    $setreference->questionscontextid = $newcontextid;
674
            $setreference->questionscontextid = $newcontextid;
789
                    if ($oldcategoryid !== $newcatgoryid) {
675
            if (
790
                        $filter->questioncategoryid = $newcatgoryid;
676
                (int)$filter['filter']['category']['values'][0] === $oldcategoryid
791
                        $setreference->filtercondition = json_encode($filter);
677
                && $oldcategoryid !== $newcatgoryid
792
                    }
678
            ) {
793
                    $DB->update_record('question_set_references', $setreference);
679
                $filter['filter']['category']['values'][0] = $newcatgoryid;
794
                }
680
                $setreference->filtercondition = json_encode($filter);
-
 
681
            }
795
            }
682
            $DB->update_record('question_set_references', $setreference);
796
        }
683
        }
797
        $setreferences->close();
684
        $setreferences->close();
798
    }
685
    }
Línea 807... Línea 694...
807
 * @param integer $newcontextid the new context id.
694
 * @param integer $newcontextid the new context id.
808
 */
695
 */
809
function question_move_category_to_context($categoryid, $oldcontextid, $newcontextid): void {
696
function question_move_category_to_context($categoryid, $oldcontextid, $newcontextid): void {
810
    global $DB;
697
    global $DB;
Línea -... Línea 698...
-
 
698
 
-
 
699
    $newcontext = context::instance_by_id($newcontextid);
-
 
700
    if ($newcontext->contextlevel !== CONTEXT_MODULE) {
-
 
701
        debugging("Invalid contextlevel: {$newcontext->contextlevel}, must use CONTEXT_MODULE", DEBUG_DEVELOPER);
-
 
702
    }
811
 
703
 
812
    $questions = [];
704
    $questions = [];
813
    $sql = "SELECT q.id, q.qtype
705
    $sql = "SELECT q.id, q.qtype
814
              FROM {question} q
706
              FROM {question} q
815
              JOIN {question_versions} qv ON qv.questionid = q.id
707
              JOIN {question_versions} qv ON qv.questionid = q.id
816
              JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
708
              JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
Línea 817... Línea 709...
817
             WHERE qbe.questioncategoryid = ?";
709
             WHERE qbe.questioncategoryid = ?";
818
 
710
 
-
 
711
    $questionids = $DB->get_records_sql_menu($sql, [$categoryid]);
-
 
712
    foreach ($questionids as $questionid => $qtype) {
-
 
713
 
-
 
714
        // If the question type is invalid, use "missingtype" so we have a valid qtype to call move_files() on.
-
 
715
        if (!\question_bank::is_qtype_installed($qtype)) {
-
 
716
            $qtype = 'missingtype';
819
    $questionids = $DB->get_records_sql_menu($sql, [$categoryid]);
717
        }
820
    foreach ($questionids as $questionid => $qtype) {
718
 
821
        question_bank::get_qtype($qtype)->move_files($questionid, $oldcontextid, $newcontextid);
719
        question_bank::get_qtype($qtype)->move_files($questionid, $oldcontextid, $newcontextid);
Línea 822... Línea 720...
822
        // Purge this question from the cache.
720
        // Purge this question from the cache.
823
        question_bank::notify_question_edited($questionid);
721
        question_bank::notify_question_edited($questionid);
824
 
722
 
825
        $questions[] = (object) [
723
        $questions[] = (object) [
826
            'id' => $questionid,
724
            'id' => $questionid,
Línea 827... Línea -...
827
            'contextid' => $oldcontextid
-
 
828
        ];
725
            'contextid' => $oldcontextid
Línea 829... Línea 726...
829
    }
726
        ];
830
 
727
    }
-
 
728
 
831
    $newcontext = context::instance_by_id($newcontextid);
729
    question_move_question_tags_to_new_context($questions, $newcontext);
832
    question_move_question_tags_to_new_context($questions, $newcontext);
730
 
833
 
731
    $subcatids = $DB->get_records_menu('question_categories', ['parent' => $categoryid], '', 'id,1');
834
    $subcatids = $DB->get_records_menu('question_categories', ['parent' => $categoryid], '', 'id,1');
732
    foreach ($subcatids as $subcatid => $notused) {
Línea 935... Línea 833...
935
 * Private function to factor common code out of get_question_options().
833
 * Private function to factor common code out of get_question_options().
936
 *
834
 *
937
 * @param object $question the question to tidy.
835
 * @param object $question the question to tidy.
938
 * @param stdClass $category The question_categories record for the given $question.
836
 * @param stdClass $category The question_categories record for the given $question.
939
 * @param \core_tag_tag[]|null $tagobjects The tags for the given $question.
837
 * @param \core_tag_tag[]|null $tagobjects The tags for the given $question.
940
 * @param stdClass[]|null $filtercourses The courses to filter the course tags by.
838
 * @param stdClass[]|null $filtercourses deprecated argument and should not be used
941
 */
839
 */
942
function _tidy_question($question, $category, array $tagobjects = null, array $filtercourses = null): void {
840
function _tidy_question($question, $category, ?array $tagobjects = null, ?array $filtercourses = null): void {
-
 
841
 
-
 
842
    if ($filtercourses !== null) {
-
 
843
        debugging("Filtercourses is a deprecated argument in " . __FUNCTION__, DEBUG_DEVELOPER);
-
 
844
    }
-
 
845
 
943
    // Convert numeric fields to float. This prevents these being displayed as 1.0000000.
846
    // Convert numeric fields to float. This prevents these being displayed as 1.0000000.
944
    $question->defaultmark += 0;
847
    $question->defaultmark += 0;
945
    $question->penalty += 0;
848
    $question->penalty += 0;
Línea 946... Línea 849...
946
 
849
 
Línea 953... Línea 856...
953
 
856
 
954
    // Add any tags we have been passed.
857
    // Add any tags we have been passed.
955
    if (!is_null($tagobjects)) {
858
    if (!is_null($tagobjects)) {
956
        $categorycontext = context::instance_by_id($category->contextid);
859
        $categorycontext = context::instance_by_id($category->contextid);
957
        $sortedtagobjects = question_sort_tags($tagobjects, $categorycontext, $filtercourses);
-
 
958
        $question->coursetagobjects = $sortedtagobjects->coursetagobjects;
-
 
959
        $question->coursetags = $sortedtagobjects->coursetags;
860
        $sortedtagobjects = question_sort_tags($tagobjects, $categorycontext, $filtercourses);
960
        $question->tagobjects = $sortedtagobjects->tagobjects;
861
        $question->tagobjects = $sortedtagobjects->tagobjects;
961
        $question->tags = $sortedtagobjects->tags;
862
        $question->tags = $sortedtagobjects->tags;
Línea 962... Línea 863...
962
    }
863
    }
Línea 978... Línea 879...
978
 * question object.
879
 * question object.
979
 *
880
 *
980
 * @param mixed $questions Either an array of question objects to be updated
881
 * @param mixed $questions Either an array of question objects to be updated
981
 *         or just a single question object
882
 *         or just a single question object
982
 * @param bool $loadtags load the question tags from the tags table. Optional, default false.
883
 * @param bool $loadtags load the question tags from the tags table. Optional, default false.
983
 * @param stdClass[] $filtercourses The courses to filter the course tags by.
884
 * @param stdClass[] $filtercourses deprecated argument and should not be used
984
 * @return bool Indicates success or failure.
885
 * @return bool Indicates success or failure.
985
 */
886
 */
986
function get_question_options(&$questions, $loadtags = false, $filtercourses = null) {
887
function get_question_options(&$questions, $loadtags = false, $filtercourses = null) {
987
    global $DB;
888
    global $DB;
Línea -... Línea 889...
-
 
889
 
-
 
890
    if ($filtercourses !== null) {
-
 
891
        debugging("Filtercourses is a deprecated argument in " . __FUNCTION__, DEBUG_DEVELOPER);
-
 
892
    }
988
 
893
 
989
    $questionlist = is_array($questions) ? $questions : [$questions];
894
    $questionlist = is_array($questions) ? $questions : [$questions];
990
    $categoryids = [];
895
    $categoryids = [];
Línea 991... Línea 896...
991
    $questionids = [];
896
    $questionids = [];
Línea 1037... Línea 942...
1037
 * This function also search tag instances that may have a context id that don't match either a course or
942
 * This function also search tag instances that may have a context id that don't match either a course or
1038
 * question context and fix the data setting the correct context id.
943
 * question context and fix the data setting the correct context id.
1039
 *
944
 *
1040
 * @param \core_tag_tag[] $tagobjects The tags for the given $question.
945
 * @param \core_tag_tag[] $tagobjects The tags for the given $question.
1041
 * @param stdClass $categorycontext The question categories context.
946
 * @param stdClass $categorycontext The question categories context.
1042
 * @param stdClass[]|null $filtercourses The courses to filter the course tags by.
947
 * @param stdClass[]|null $filtercourses deprecated argument and should not be used.
1043
 * @return stdClass $sortedtagobjects Sorted tag objects.
948
 * @return stdClass $sortedtagobjects Sorted tag objects.
1044
 */
949
 */
1045
function question_sort_tags($tagobjects, $categorycontext, $filtercourses = null): stdClass {
950
function question_sort_tags($tagobjects, $categorycontext, $filtercourses = null): stdClass {
Línea 1046... Línea 951...
1046
 
951
 
1047
    // Questions can have two sets of tag instances. One set at the
952
    if ($filtercourses !== null) {
1048
    // course context level and another at the context the question
953
        debugging("Filtercourses is a deprecated argument in " . __FUNCTION__, DEBUG_DEVELOPER);
-
 
954
    }
1049
    // belongs to (e.g. course category, system etc).
955
 
1050
    $sortedtagobjects = new stdClass();
-
 
1051
    $sortedtagobjects->coursetagobjects = [];
-
 
1052
    $sortedtagobjects->coursetags = [];
956
    $sortedtagobjects = new stdClass();
1053
    $sortedtagobjects->tagobjects = [];
957
    $sortedtagobjects->tagobjects = [];
1054
    $sortedtagobjects->tags = [];
958
    $sortedtagobjects->tags = [];
1055
    $taginstanceidstonormalise = [];
-
 
1056
    $filtercoursecontextids = [];
-
 
1057
    $hasfiltercourses = !empty($filtercourses);
-
 
1058
 
-
 
1059
    if ($hasfiltercourses) {
-
 
1060
        // If we're being asked to filter the course tags by a set of courses
-
 
1061
        // then get the context ids to filter below.
-
 
1062
        $filtercoursecontextids = array_map(function($course) {
-
 
1063
            $coursecontext = context_course::instance($course->id);
-
 
1064
            return $coursecontext->id;
-
 
1065
        }, $filtercourses);
-
 
Línea 1066... Línea 959...
1066
    }
959
    $taginstanceidstonormalise = [];
1067
 
960
 
1068
    foreach ($tagobjects as $tagobject) {
961
    foreach ($tagobjects as $tagobject) {
1069
        $tagcontextid = $tagobject->taginstancecontextid;
-
 
1070
        $tagcontext = context::instance_by_id($tagcontextid);
-
 
1071
        $tagcoursecontext = $tagcontext->get_course_context(false);
-
 
1072
        // This is a course tag if the tag context is a course context which
-
 
1073
        // doesn't match the question's context. Any tag in the question context
-
 
1074
        // is not considered a course tag, it belongs to the question.
-
 
1075
        $iscoursetag = $tagcoursecontext
-
 
1076
            && $tagcontext->id == $tagcoursecontext->id
-
 
1077
            && $tagcontext->id != $categorycontext->id;
-
 
1078
 
-
 
1079
        if ($iscoursetag) {
-
 
1080
            // Any tag instance in a course context level is considered a course tag.
-
 
1081
            if (!$hasfiltercourses || in_array($tagcontextid, $filtercoursecontextids)) {
-
 
1082
                // Add the tag to the list of course tags if we aren't being
-
 
1083
                // asked to filter or if this tag is in the list of courses
-
 
1084
                // we're being asked to filter by.
-
 
1085
                $sortedtagobjects->coursetagobjects[] = $tagobject;
-
 
1086
                $sortedtagobjects->coursetags[$tagobject->id] = $tagobject->get_display_name();
-
 
1087
            }
-
 
1088
        } else {
962
        $tagcontextid = $tagobject->taginstancecontextid;
1089
            // All non course context level tag instances or tags in the question
963
        $tagcontext = context::instance_by_id($tagcontextid);
1090
            // context belong to the context that the question was created in.
964
            // All tag instances belong to the context that the question was created in.
Línea 1091... Línea 965...
1091
            $sortedtagobjects->tagobjects[] = $tagobject;
965
            $sortedtagobjects->tagobjects[] = $tagobject;
1092
            $sortedtagobjects->tags[$tagobject->id] = $tagobject->get_display_name();
966
            $sortedtagobjects->tags[$tagobject->id] = $tagobject->get_display_name();
Línea 1100... Línea 974...
1100
                $taginstanceidstonormalise[] = $tagobject->taginstanceid;
974
                $taginstanceidstonormalise[] = $tagobject->taginstanceid;
1101
                // Update the object properties to reflect the DB update that will
975
                // Update the object properties to reflect the DB update that will
1102
                // happen below.
976
                // happen below.
1103
                $tagobject->taginstancecontextid = $categorycontext->id;
977
                $tagobject->taginstancecontextid = $categorycontext->id;
1104
            }
978
            }
1105
        }
979
 
1106
    }
980
    }
Línea 1107... Línea 981...
1107
 
981
 
1108
    if (!empty($taginstanceidstonormalise)) {
982
    if (!empty($taginstanceidstonormalise)) {
1109
        // If we found any tag instances with incorrect context id data then we can
983
        // If we found any tag instances with incorrect context id data then we can
Línea 1176... Línea 1050...
1176
    }
1050
    }
1177
    return $children;
1051
    return $children;
1178
}
1052
}
Línea 1179... Línea 1053...
1179
 
1053
 
1180
/**
1054
/**
1181
 * Get the default category for the context.
1055
 * Get the default category for the context. Optionally create one if it does not exist.
1182
 *
1056
 *
-
 
1057
 * @param int $contextid a context id.
1183
 * @param integer $contextid a context id.
1058
 * @param bool $createifnotexists create the default catagory if it does not exist.
1184
 * @return object|bool the default question category for that context, or false if none.
1059
 * @return stdClass|bool the default question category for that context, or false if none.
1185
 */
1060
 */
1186
function question_get_default_category($contextid) {
1061
function question_get_default_category($contextid, bool $createifnotexists = false) {
-
 
1062
    global $DB;
1187
    global $DB;
1063
 
1188
    $category = $DB->get_records_select('question_categories', 'contextid = ? AND parent <> 0',
1064
    $context = \core\context::instance_by_id($contextid);
1189
                                        [$contextid], 'id', '*', 0, 1);
1065
    if ($context->contextlevel !== CONTEXT_MODULE) {
-
 
1066
        debugging(
1190
    if (!empty($category)) {
1067
            "Invalid context level {$context->contextlevel} for default category. Please use CONTEXT_MODULE",
1191
        return reset($category);
1068
            DEBUG_DEVELOPER
1192
    } else {
1069
        );
1193
        return false;
1070
        return false;
-
 
1071
    }
-
 
1072
 
-
 
1073
    $defaultcats = $DB->get_records_select('question_categories', 'contextid = ? AND parent <> 0', [$contextid], 'id', '*', 0, 1);
-
 
1074
 
-
 
1075
    $defaultcat = reset($defaultcats);
-
 
1076
 
-
 
1077
    if (empty($defaultcat) && $createifnotexists) {
-
 
1078
 
-
 
1079
        // We need to make a top category first if it doesn't exist.
-
 
1080
        $topcategory = question_get_top_category($context->id, true);
-
 
1081
 
-
 
1082
        // We don't have one, so we need to make one.
-
 
1083
        $defaultcat = new stdClass();
-
 
1084
        $contextname = $context->get_context_name(false, true);
-
 
1085
        // Max length of name field is 255.
-
 
1086
        $defaultcat->name = shorten_text(get_string('defaultfor', 'question', $contextname), 255);
-
 
1087
        $defaultcat->info = get_string('defaultinfofor', 'question', $contextname);
-
 
1088
        $defaultcat->contextid = $context->id;
-
 
1089
        $defaultcat->parent = $topcategory->id;
-
 
1090
        // By default, all categories get this number, and are sorted alphabetically.
-
 
1091
        $defaultcat->sortorder = 999;
-
 
1092
        $defaultcat->stamp = make_unique_id_code();
-
 
1093
        $defaultcat->id = $DB->insert_record('question_categories', $defaultcat);
-
 
1094
    }
-
 
1095
 
1194
    }
1096
    return $defaultcat;
Línea 1195... Línea 1097...
1195
}
1097
}
1196
 
1098
 
1197
/**
1099
/**
Línea 1204... Línea 1106...
1204
 */
1106
 */
1205
function question_get_top_category($contextid, $create = false) {
1107
function question_get_top_category($contextid, $create = false) {
1206
    global $DB;
1108
    global $DB;
1207
    $category = $DB->get_record('question_categories', ['contextid' => $contextid, 'parent' => 0]);
1109
    $category = $DB->get_record('question_categories', ['contextid' => $contextid, 'parent' => 0]);
Línea -... Línea 1110...
-
 
1110
 
-
 
1111
    $context = context::instance_by_id($contextid);
-
 
1112
    if ($context->contextlevel !== CONTEXT_MODULE) {
-
 
1113
        debugging(
-
 
1114
            "Invalid context level: {$context->contextlevel} for question_get_top_category, must be CONTEXT_MODULE",
-
 
1115
            DEBUG_DEVELOPER
-
 
1116
        );
-
 
1117
        return false;
-
 
1118
    }
1208
 
1119
 
1209
    if (!$category && $create) {
1120
    if (!$category && $create) {
1210
        // We need to make one.
1121
        // We need to make one.
1211
        $category = new stdClass();
1122
        $category = new stdClass();
1212
        $category->name = 'top'; // A non-real name for the top category. It will be localised at the display time.
1123
        $category->name = 'top'; // A non-real name for the top category. It will be localised at the display time.
Línea 1241... Línea 1152...
1241
 
1152
 
1242
    return $topcategories;
1153
    return $topcategories;
Línea 1243... Línea 1154...
1243
}
1154
}
1244
 
-
 
1245
/**
-
 
1246
 * Gets the default category in the most specific context.
-
 
1247
 * If no categories exist yet then default ones are created in all contexts.
-
 
1248
 *
-
 
1249
 * @param array $contexts  The context objects for this context and all parent contexts.
-
 
1250
 * @return object The default category - the category in the course context
-
 
1251
 */
-
 
1252
function question_make_default_categories($contexts): object {
-
 
1253
    global $DB;
-
 
1254
    static $preferredlevels = array(
-
 
1255
        CONTEXT_COURSE => 4,
-
 
1256
        CONTEXT_MODULE => 3,
-
 
1257
        CONTEXT_COURSECAT => 2,
-
 
1258
        CONTEXT_SYSTEM => 1,
-
 
1259
    );
-
 
1260
 
-
 
1261
    $toreturn = null;
-
 
1262
    $preferredness = 0;
-
 
1263
    // If it already exists, just return it.
-
 
1264
    foreach ($contexts as $key => $context) {
-
 
1265
        $topcategory = question_get_top_category($context->id, true);
-
 
1266
        if (!$exists = $DB->record_exists("question_categories",
-
 
1267
                array('contextid' => $context->id, 'parent' => $topcategory->id))) {
-
 
1268
            // Otherwise, we need to make one.
-
 
1269
            $category = new stdClass();
-
 
1270
            $contextname = $context->get_context_name(false, true);
-
 
1271
            // Max length of name field is 255.
-
 
1272
            $category->name = shorten_text(get_string('defaultfor', 'question', $contextname), 255);
-
 
1273
            $category->info = get_string('defaultinfofor', 'question', $contextname);
-
 
1274
            $category->contextid = $context->id;
-
 
1275
            $category->parent = $topcategory->id;
-
 
1276
            // By default, all categories get this number, and are sorted alphabetically.
-
 
1277
            $category->sortorder = 999;
-
 
1278
            $category->stamp = make_unique_id_code();
-
 
1279
            $category->id = $DB->insert_record('question_categories', $category);
-
 
1280
        } else {
-
 
1281
            $category = question_get_default_category($context->id);
-
 
1282
        }
-
 
1283
        $thispreferredness = $preferredlevels[$context->contextlevel];
-
 
1284
        if (has_any_capability(array('moodle/question:usemine', 'moodle/question:useall'), $context)) {
-
 
1285
            $thispreferredness += 10;
-
 
1286
        }
-
 
1287
        if ($thispreferredness > $preferredness) {
-
 
1288
            $toreturn = $category;
-
 
1289
            $preferredness = $thispreferredness;
-
 
1290
        }
-
 
1291
    }
-
 
1292
 
-
 
1293
    if (!is_null($toreturn)) {
-
 
1294
        $toreturn = clone($toreturn);
-
 
1295
    }
-
 
1296
    return $toreturn;
-
 
1297
}
-
 
1298
 
1155
 
1299
/**
1156
/**
1300
 * Get the list of categories.
1157
 * Get the list of categories.
1301
 *
1158
 *
1302
 * @param int $categoryid
1159
 * @param int $categoryid
Línea 1420... Línea 1277...
1420
/**
1277
/**
1421
 * Check capability on category.
1278
 * Check capability on category.
1422
 *
1279
 *
1423
 * @param int|stdClass|question_definition $questionorid object or id.
1280
 * @param int|stdClass|question_definition $questionorid object or id.
1424
 *      If an object is passed, it should include ->contextid and ->createdby.
1281
 *      If an object is passed, it should include ->contextid and ->createdby.
1425
 * @param string $cap 'add', 'edit', 'view', 'use', 'move' or 'tag'.
1282
 * @param string $cap 'add', 'edit', 'view', 'use', 'move', or 'tag'.
1426
 * @param int $notused no longer used.
1283
 * @param int $notused no longer used.
1427
 * @return bool this user has the capability $cap for this question $question?
1284
 * @return bool this user has the capability $cap for this question $question?
1428
 */
1285
 */
1429
function question_has_capability_on($questionorid, $cap, $notused = -1): bool {
1286
function question_has_capability_on($questionorid, $cap, $notused = -1): bool {
1430
    global $USER, $DB;
1287
    global $USER, $DB;
Línea 1511... Línea 1368...
1511
function question_edit_url($context) {
1368
function question_edit_url($context) {
1512
    global $CFG, $SITE;
1369
    global $CFG, $SITE;
1513
    if (!has_any_capability(question_get_question_capabilities(), $context)) {
1370
    if (!has_any_capability(question_get_question_capabilities(), $context)) {
1514
        return false;
1371
        return false;
1515
    }
1372
    }
-
 
1373
 
-
 
1374
    if ($context->contextlevel !== CONTEXT_MODULE) {
-
 
1375
        debugging(
-
 
1376
            "Invalid contextlevel: {$context->contextlevel} provided for question_edit_url, must be CONTEXT_MODULE",
-
 
1377
            DEBUG_DEVELOPER
-
 
1378
        );
-
 
1379
        return false;
-
 
1380
    }
-
 
1381
 
1516
    $baseurl = $CFG->wwwroot . '/question/edit.php?';
1382
    $baseurl = $CFG->wwwroot . '/question/edit.php?';
1517
    $defaultcategory = question_get_default_category($context->id);
1383
    $defaultcategory = question_get_default_category($context->id, true);
1518
    if ($defaultcategory) {
1384
    if ($defaultcategory) {
1519
        $baseurl .= 'cat=' . $defaultcategory->id . ',' . $context->id . '&amp;';
1385
        $baseurl .= 'cat=' . $defaultcategory->id . ',' . $context->id . '&amp;';
1520
    }
1386
    }
1521
    switch ($context->contextlevel) {
1387
    switch ($context->contextlevel) {
1522
        case CONTEXT_SYSTEM:
1388
        case CONTEXT_SYSTEM:
Línea 1529... Línea 1395...
1529
            return $baseurl . 'courseid=' . $context->instanceid;
1395
            return $baseurl . 'courseid=' . $context->instanceid;
1530
        case CONTEXT_MODULE:
1396
        case CONTEXT_MODULE:
1531
            return $baseurl . 'cmid=' . $context->instanceid;
1397
            return $baseurl . 'cmid=' . $context->instanceid;
1532
    }
1398
    }
Línea -... Línea 1399...
-
 
1399
 
1533
 
1400
    return $baseurl . 'cmid=' . $context->instanceid;
Línea 1534... Línea 1401...
1534
}
1401
}
1535
 
1402
 
1536
/**
1403
/**
Línea 1545... Línea 1412...
1545
 * @return navigation_node Returns the question branch that was added
1412
 * @return navigation_node Returns the question branch that was added
1546
 */
1413
 */
1547
function question_extend_settings_navigation(navigation_node $navigationnode, $context, $baseurl = '/question/edit.php') {
1414
function question_extend_settings_navigation(navigation_node $navigationnode, $context, $baseurl = '/question/edit.php') {
1548
    global $PAGE;
1415
    global $PAGE;
Línea 1549... Línea 1416...
1549
 
1416
 
-
 
1417
    $iscourse = $context->contextlevel === CONTEXT_COURSE;
-
 
1418
 
1550
    if ($context->contextlevel == CONTEXT_COURSE) {
1419
    if ($iscourse) {
1551
        $params = ['courseid' => $context->instanceid];
1420
        $params = ['courseid' => $context->instanceid];
1552
    } else if ($context->contextlevel == CONTEXT_MODULE) {
1421
    } else if ($context->contextlevel == CONTEXT_MODULE) {
1553
        $params = ['cmid' => $context->instanceid];
1422
        $params = ['cmid' => $context->instanceid];
1554
    } else {
1423
    } else {
Línea 1557... Línea 1426...
1557
 
1426
 
1558
    if (($cat = $PAGE->url->param('cat')) && preg_match('~\d+,\d+~', $cat)) {
1427
    if (($cat = $PAGE->url->param('cat')) && preg_match('~\d+,\d+~', $cat)) {
1559
        $params['cat'] = $cat;
1428
        $params['cat'] = $cat;
Línea 1560... Línea 1429...
1560
    }
1429
    }
1561
 
1430
 
Línea 1562... Línea 1431...
1562
    $questionnode = $navigationnode->add(get_string('questionbank', 'question'),
1431
    $questionnode = $navigationnode->add(get_string($iscourse ? 'questionbank_plural' : 'questionbank', 'question'),
1563
            new moodle_url($baseurl, $params), navigation_node::TYPE_CONTAINER, null, 'questionbank');
1432
            new moodle_url($baseurl, $params), navigation_node::TYPE_CONTAINER, null, 'questionbank');
1564
 
1433
 
Línea 1690... Línea 1559...
1690
 * @param int $itemid item ID
1559
 * @param int $itemid item ID
1691
 * @param array $options options
1560
 * @param array $options options
1692
 * @return string
1561
 * @return string
1693
 */
1562
 */
1694
function question_rewrite_question_urls($text, $file, $contextid, $component, $filearea,
1563
function question_rewrite_question_urls($text, $file, $contextid, $component, $filearea,
1695
                                        array $ids, $itemid, array $options=null): string {
1564
                                        array $ids, $itemid, ?array $options=null): string {
Línea 1696... Línea 1565...
1696
 
1565
 
1697
    $idsstr = '';
1566
    $idsstr = '';
1698
    if (!empty($ids)) {
1567
    if (!empty($ids)) {
1699
        $idsstr .= implode('/', $ids);
1568
        $idsstr .= implode('/', $ids);