You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							326 lines
						
					
					
						
							15 KiB
						
					
					
				
			
		
		
	
	
							326 lines
						
					
					
						
							15 KiB
						
					
					
				<?php
 | 
						|
/* For licensing terms, see /license.txt */
 | 
						|
/**
 | 
						|
 * This script removes duplicated tests and questions created
 | 
						|
 * through something gone wrong in the course backup/copy process.
 | 
						|
 * It identifies duplicate tests and questions by title and
 | 
						|
 * makes sure no results are associated with the duplicate test, and
 | 
						|
 * that the duplicate test is not used in a learning path.
 | 
						|
 * This script should be located inside the tests/scripts/ folder to work.
 | 
						|
 * It can be run more than one time as it will only ever affect orphan
 | 
						|
 * questions and duplicate tests.
 | 
						|
 * If you have a very large number of tests, we recommend you temporarily
 | 
						|
 * comment out the api_item_property_update() calls in Exercise::delete() and
 | 
						|
 * Question::delete().
 | 
						|
 * Chances are there is not even a registry of those tests there in the
 | 
						|
 * first place (they were probably duplicated through a short process) and
 | 
						|
 * this is where most of the time is spent during deletion.
 | 
						|
 * @author Yannick Warnier <yannick.warnier@beeznest.com>
 | 
						|
 */
 | 
						|
exit; //remove this line to execute from the command line
 | 
						|
use ChamiloSession as Session;
 | 
						|
 | 
						|
ini_set('memory_limit', '256M');
 | 
						|
 | 
						|
if (PHP_SAPI !== 'cli') {
 | 
						|
    die('This script can only be executed from the command line');
 | 
						|
}
 | 
						|
 | 
						|
require_once __DIR__.'/../../main/inc/global.inc.php';
 | 
						|
 | 
						|
$tests = [];
 | 
						|
 | 
						|
$debug = false;
 | 
						|
$_user['user_id'] = 1;
 | 
						|
Session::write('_user', $_user);
 | 
						|
 | 
						|
echo "[".time()."] Querying courses\n";
 | 
						|
$sql = "SELECT id, code FROM course order by id";
 | 
						|
 | 
						|
$resCourse = Database::query($sql);
 | 
						|
if ($resCourse === false) {
 | 
						|
    exit('Could not find any course'.PHP_EOL);
 | 
						|
}
 | 
						|
$countCourses = Database::num_rows($resCourse);
 | 
						|
echo "[".time()."] Found $countCourses courses".PHP_EOL;
 | 
						|
 | 
						|
$duplicateTestsCount = 0;
 | 
						|
$originalTestsCount = 0;
 | 
						|
$deletedTestsCount = 0;
 | 
						|
$deletedQuestionsCount = 0;
 | 
						|
$usedQuestionIds = [];
 | 
						|
$questionsInTests = [];
 | 
						|
$testsWithTracking = 0;
 | 
						|
$testsInLP = 0;
 | 
						|
 | 
						|
// Get the questions that are still used
 | 
						|
$sql = "SELECT question_id FROM c_quiz_rel_question WHERE exercice_id != -1";
 | 
						|
$res = Database::query($sql);
 | 
						|
if (Database::num_rows($res) > 0) {
 | 
						|
    while ($row = Database::fetch_assoc($res)) {
 | 
						|
        if (!empty($usedQuestionIds[$row['question_id']])) {
 | 
						|
            // Do not delete questions that are otherwise used by original tests
 | 
						|
            continue;
 | 
						|
        }
 | 
						|
        $usedQuestionIds[$row['question_id']] = true;
 | 
						|
    }
 | 
						|
}
 | 
						|
echo "[".time()."] Found ".count($usedQuestionIds)." questions still used in a test.".PHP_EOL;
 | 
						|
 | 
						|
// First, proceed to the deletion of orphan questions, to get rid of
 | 
						|
// those which are not linked to any exercise anyway
 | 
						|
$deletedOrphanQuestionsCount = 0;
 | 
						|
$orphanList = [];
 | 
						|
// Delete the ones marked with exercice_id = -1 in c_quiz_rel_question
 | 
						|
$sql = "SELECT question_id, exercice_id, c_id FROM c_quiz_rel_question WHERE exercice_id = -1";
 | 
						|
$res = Database::query($sql);
 | 
						|
echo "[".time()."] Deleting initial orphan questions from c_quiz_rel_question...".PHP_EOL;
 | 
						|
if (Database::num_rows($res) > 0) {
 | 
						|
    while ($row = Database::fetch_assoc($res)) {
 | 
						|
        if (!empty($usedQuestionIds[$row['question_id']])) {
 | 
						|
            // Do not delete questions that are otherwise used by original tests
 | 
						|
            continue;
 | 
						|
        }
 | 
						|
        $sqlDelete = "DELETE FROM c_quiz_rel_question
 | 
						|
            WHERE question_id = ".$row['question_id']."
 | 
						|
            AND exercice_id = -1
 | 
						|
            AND c_id = ".$row['c_id'];
 | 
						|
        $resDelete = Database::query($sqlDelete);
 | 
						|
        /*
 | 
						|
        // This type of question duplicate didn't seem to register in
 | 
						|
        // c_item_property, so we don't need to update it either.
 | 
						|
        api_item_property_update(
 | 
						|
            ['real_id' => $row['c_id']],
 | 
						|
            TOOL_QUIZ,
 | 
						|
            $row['question_id'],
 | 
						|
            'QuizQuestionDeleted',
 | 
						|
            $_user['user_id']
 | 
						|
        );
 | 
						|
        */
 | 
						|
        Event::addEvent(
 | 
						|
            LOG_QUESTION_REMOVED_FROM_QUIZ,
 | 
						|
            LOG_QUESTION_ID,
 | 
						|
            $row['question_id']
 | 
						|
        );
 | 
						|
        if ($debug) {
 | 
						|
            echo $row['question_id'].', ';
 | 
						|
        }
 | 
						|
        $deletedOrphanQuestionsCount++;
 | 
						|
        $orphanList[$row['question_id']] = true;
 | 
						|
    }
 | 
						|
}
 | 
						|
echo PHP_EOL;
 | 
						|
echo "[".time()."] Removed ".count($orphanList)." question references with test=-1 from c_quiz_rel_question.".PHP_EOL;
 | 
						|
 | 
						|
// Delete the questions in c_quiz_question that do not exist in
 | 
						|
// c_quiz_rel_question at all (so they are not used in any case).
 | 
						|
$localOrphans = 0;
 | 
						|
 | 
						|
echo "[".time()."] Really deleting orphan questions...".PHP_EOL;
 | 
						|
$sql = "SELECT qq.iid, qq.c_id, c.directory from c_quiz_question qq, course c WHERE qq.iid NOT IN (SELECT DISTINCT(question_id) FROM c_quiz_rel_question) and c.id = qq.c_id";
 | 
						|
$res = Database::query($sql);
 | 
						|
$num = Database::num_rows($res);
 | 
						|
if ($num > 0) {
 | 
						|
    if ($debug) {
 | 
						|
        echo "Found $num questions to delete (if not used)...".PHP_EOL;
 | 
						|
    }
 | 
						|
    while ($row = Database::fetch_assoc($res)) {
 | 
						|
        if (!empty($usedQuestonIds[$row['iid']])) {
 | 
						|
            // Do not delete questions that are otherwise used by original tests
 | 
						|
            continue;
 | 
						|
        }
 | 
						|
        $sql = "DELETE FROM c_quiz_answer
 | 
						|
                    WHERE question_id = ".$row['iid'];
 | 
						|
        Database::query($sql);
 | 
						|
 | 
						|
        // remove the category of this question in the question_rel_category table
 | 
						|
        $sql = "DELETE FROM c_quiz_question_rel_category
 | 
						|
                    WHERE
 | 
						|
                        c_id = ".$row['c_id']." AND
 | 
						|
                        question_id = ".$row['iid'];
 | 
						|
        Database::query($sql);
 | 
						|
 | 
						|
        // Add extra fields.
 | 
						|
        $extraField = new ExtraFieldValue('question');
 | 
						|
        $extraField->deleteValuesByItem($row['iid']);
 | 
						|
 | 
						|
        $sql = "DELETE FROM c_quiz_question
 | 
						|
                    WHERE iid = ".$row['iid'];
 | 
						|
        Database::query($sql);
 | 
						|
        /*
 | 
						|
        // This type of question duplicate didn't seem to register in
 | 
						|
        // c_item_property, so we don't need to update it either.
 | 
						|
        api_item_property_update(
 | 
						|
            ['real_id' => $row['c_id']],
 | 
						|
            TOOL_QUIZ,
 | 
						|
            $row['question_id'],
 | 
						|
            'QuizQuestionDeleted',
 | 
						|
            $_user['user_id']
 | 
						|
        );
 | 
						|
        */
 | 
						|
        Event::addEvent(
 | 
						|
            LOG_QUESTION_DELETED,
 | 
						|
            LOG_QUESTION_ID,
 | 
						|
            $row['iid']
 | 
						|
        );
 | 
						|
 | 
						|
        if ($debug) {
 | 
						|
            echo $row['iid'].', ';
 | 
						|
        }
 | 
						|
        unset($question);
 | 
						|
        $deletedOrphanQuestionsCount++;
 | 
						|
        $localOrphans++;
 | 
						|
        $orphanList[$row['iid']] = true;
 | 
						|
    }
 | 
						|
}
 | 
						|
echo PHP_EOL;
 | 
						|
echo "[".time()."] Removed ".$localOrphans." questions that were not in c_quiz_rel_question anymore.".PHP_EOL;
 | 
						|
 | 
						|
// Search for duplicate tests, by looking for tests that have the exact same
 | 
						|
// title in the same course
 | 
						|
echo "Iterating on courses: ";
 | 
						|
while ($course = Database::fetch_assoc($resCourse)) {
 | 
						|
    $course['real_id'] = $course['id'];
 | 
						|
    if ($debug) {
 | 
						|
        echo $course['id'].'..(';
 | 
						|
    }
 | 
						|
    $sql2 = "SELECT iid, title FROM c_quiz WHERE c_id = ".$course['id']." ORDER BY title, iid";
 | 
						|
    $res2 = Database::query($sql2);
 | 
						|
    if ($res2 === false) {
 | 
						|
        die("Error querying tests in course code ".$course['code'].": ".Database::error($res2)."\n");
 | 
						|
    }
 | 
						|
 | 
						|
    $lastTestTitle = '';
 | 
						|
    $lastOriginalTestId = 0;
 | 
						|
    if (Database::num_rows($res2) > 0) {
 | 
						|
        while ($test = Database::fetch_assoc($res2)) {
 | 
						|
            // Simply get the questions for all the tests queried
 | 
						|
            $sqlTestQuestions = "SELECT question_id from c_quiz_rel_question WHERE c_id = ".$course['id']." AND exercice_id = ".$test['iid'];
 | 
						|
            $resTestQuestions = Database::query($sqlTestQuestions);
 | 
						|
            if (Database::num_rows($resTestQuestions) > 0) {
 | 
						|
                while ($rowTestQuestions = Database::fetch_assoc($resTestQuestions)) {
 | 
						|
                    $questionsInTests[$rowTestQuestions['question_id']] = true;
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            if ($lastTestTitle != $test['title']) {
 | 
						|
                //echo "New title, new test serie in course ".$course['id'].": ".$test['title'].PHP_EOL;
 | 
						|
                // The title is different -> moving on to another test, but
 | 
						|
                // recording questions' IDs just in case
 | 
						|
                $lastTestTitle = $test['title'];
 | 
						|
                $lastOriginalTestId = $test['iid'];
 | 
						|
                $originalTestsCount++;
 | 
						|
                $sql2b = "SELECT question_id FROM c_quiz_rel_question WHERE c_id = ".$course['id']." AND exercice_id = ".$test['iid'];
 | 
						|
                $res2b = Database::query($sql2b);
 | 
						|
                if (Database::num_rows($res2b) > 0) {
 | 
						|
                    while ($row2b = Database::fetch_assoc($res2b)) {
 | 
						|
                        // Store the question iid in the index to avoid duplicates
 | 
						|
                        // This might have several hundred thousand records, make it concise
 | 
						|
                        $usedQuestionIds[$row2b['question_id']] = true;
 | 
						|
                        $questionsInTests[$row2b['question_id']] = true;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            } else {
 | 
						|
                // A likely duplicate...
 | 
						|
                // Only bother if the test's internal ID is higher than the
 | 
						|
                // last original test ID, which means this (duplicate) test
 | 
						|
                // has been created *after* the original.
 | 
						|
                if ($lastOriginalTestId < $test['iid']) {
 | 
						|
                    // Check if some student took the test (despite it being
 | 
						|
                    // of duplicate title)
 | 
						|
                    $sql3 = "SELECT exe_id FROM track_e_exercises WHERE c_id = ".$course['id']." AND exe_exo_id = ".$test['iid'];
 | 
						|
                    $res3 = Database::query($sql3);
 | 
						|
                    if (0 === Database::num_rows($res3)) {
 | 
						|
                        // No results in the logs. Likely to be removed, but
 | 
						|
                        // check if included in a LP
 | 
						|
                        $sql4 = "SELECT lp_id FROM c_lp_item WHERE c_id = ".$course['id']." AND item_type = 'quiz' AND ref = ".$test['iid'];
 | 
						|
                        $res4 = Database::query($sql4);
 | 
						|
                        if (0 === Database::num_rows($res4)) {
 | 
						|
                            // Not included in any LP. Delete.
 | 
						|
                            $sql5 = "SELECT iid, question_id FROM c_quiz_rel_question WHERE c_id = ".$course['id']." AND exercice_id = ".$test['iid'];
 | 
						|
                            $res5 = Database::query($sql5);
 | 
						|
                            $num5 = Database::num_rows($res5);
 | 
						|
                            // delete questions
 | 
						|
                            if ($num5 > 0) {
 | 
						|
                                while ($row5 = Database::fetch_assoc($res5)) {
 | 
						|
                                    $questionsInTests[$row5['question_id']] = true;
 | 
						|
                                    $deletedQuestionsCount++;
 | 
						|
                                    // questions will be disabled during the
 | 
						|
                                    // test deletion below and can be deleted
 | 
						|
                                    // through a second run
 | 
						|
                                }
 | 
						|
                            }
 | 
						|
                            $deletedQuestionsCount += $num5;
 | 
						|
                            // delete test
 | 
						|
                            $exercise = new Exercise($course['id']);
 | 
						|
                            if ($exercise->read($test['iid'])) {
 | 
						|
                                // Delete the test and mark questions as orphan if only used there
 | 
						|
                                $exercise->delete(true);
 | 
						|
                                if ($debug) {
 | 
						|
                                    echo $test['iid'].', ';
 | 
						|
                                }
 | 
						|
                                $deletedTestsCount++;
 | 
						|
                            }
 | 
						|
                            unset($exercise);
 | 
						|
                        } else {
 | 
						|
                            //echo "Found test ".$test['iid']." included in a learning path in ".$course['code'].". Not deleting.".PHP_EOL;
 | 
						|
                            $sql2b = "SELECT question_id FROM c_quiz_rel_question WHERE c_id = ".$course['id']." AND exercice_id = ".$test['iid'];
 | 
						|
                            $res2b = Database::query($sql2b);
 | 
						|
                            if (Database::num_rows($res2b) > 0) {
 | 
						|
                                while ($row2b = Database::fetch_assoc($res2b)) {
 | 
						|
                                    // Store the question iid in the index to avoid duplicates
 | 
						|
                                    // This might have several hundred thousand records, make it concise
 | 
						|
                                    $usedQuestionIds[$row2b['question_id']] = true;
 | 
						|
                                    $questionsInTests[$row2b['question_id']] = true;
 | 
						|
                                }
 | 
						|
                            }
 | 
						|
                            $testsInLP++;
 | 
						|
                        }
 | 
						|
                    } else {
 | 
						|
                        // else there are results, so do not delete
 | 
						|
                        //echo "Found results for test ".$test['iid']." in course ".$course['code'].". Not deleting.".PHP_EOL;
 | 
						|
                        $sql2b = "SELECT question_id FROM c_quiz_rel_question WHERE c_id = ".$course['id']." AND exercice_id = ".$test['iid'];
 | 
						|
                        $res2b = Database::query($sql2b);
 | 
						|
                        if (Database::num_rows($res2b) > 0) {
 | 
						|
                            while ($row2b = Database::fetch_assoc($res2b)) {
 | 
						|
                                // Store the question iid in the index to avoid duplicates
 | 
						|
                                // This might have several hundred thousand records, make it concise
 | 
						|
                                $usedQuestionIds[$row2b['question_id']] = true;
 | 
						|
                                $questionsInTests[$row2b['question_id']] = true;
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
                        $testsWithTracking++;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                $duplicateTestsCount++;
 | 
						|
            }
 | 
						|
        } // end while on c_quiz
 | 
						|
    }
 | 
						|
    if ($debug) {
 | 
						|
        echo ') ';
 | 
						|
    }
 | 
						|
} // end while on course
 | 
						|
echo PHP_EOL;
 | 
						|
 | 
						|
echo "[".time()."] Cleaning up 'new' orphans...".PHP_EOL;
 | 
						|
// Now clean up any question left that is not inside $usedQuestionIds
 | 
						|
$sql = "SELECT iid, c_id FROM c_quiz_question ORDER BY iid";
 | 
						|
$res = Database::query($sql);
 | 
						|
$localCount = 0;
 | 
						|
if (Database::num_rows($res) > 0) {
 | 
						|
    while ($row = Database::fetch_assoc($res)) {
 | 
						|
        if (empty($usedQuestionIds[$row['iid']])) {
 | 
						|
            // If this question wasn't used anywhere, delete it
 | 
						|
            $question = Question::read($row['iid'], ['real_id' => $row['c_id']]);
 | 
						|
            $question->delete(0, false);
 | 
						|
            $deletedQuestionsCount++;
 | 
						|
            $localCount++;
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
echo "[".time()."] Done cleaning $localCount new orphan questions.".PHP_EOL;
 | 
						|
echo "Found $originalTestsCount original tests and $duplicateTestsCount duplicate tests...".PHP_EOL;
 | 
						|
echo "but $testsWithTracking had results and $testsInLP were included in learning paths.".PHP_EOL;
 | 
						|
echo "Deleted $deletedTestsCount ($duplicateTestsCount - $testsWithTracking - $testsInLP) tests and $deletedQuestionsCount questions.".PHP_EOL;
 | 
						|
echo count($usedQuestionIds)." questions were still used".PHP_EOL;
 | 
						|
 |