From 5d6fb4316dc10dac2d5e42a5d3b35686a9143595 Mon Sep 17 00:00:00 2001 From: Sanmugam Kathirvel Date: Fri, 12 Mar 2021 21:39:54 +0530 Subject: [PATCH 1/5] Custom code on moddle to handle event test status --- mod/quiz/startattempt.php | 13 +++++ mod/quiz/view.php | 119 ++++++++++++++++++++++++++++---------- 2 files changed, 103 insertions(+), 29 deletions(-) mode change 100755 => 100644 mod/quiz/view.php diff --git a/mod/quiz/startattempt.php b/mod/quiz/startattempt.php index 7d2398eb..933ddf15 100755 --- a/mod/quiz/startattempt.php +++ b/mod/quiz/startattempt.php @@ -110,5 +110,18 @@ $attempt = quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $lastattempt); +/* Custom code start */ +/** +* When the user startattempt the quiz updating the it's status from 0 => 1 +*/ + +global $USER; +include('spoken-config.php'); + +$sql = "update training_eventteststatus set part_status = 1, mdlattempt_id = ".$attempt->id." where mdlemail = '".$USER->email."' and part_status = 0 and mdlcourse_id = ".$cm->course." and mdlquiz_id = ".$attempt->quiz; + +$result = $mysqli->query($sql); +/* Custom code start */ + // Redirect to the attempt page. redirect($quizobj->attempt_url($attempt->id, $page)); diff --git a/mod/quiz/view.php b/mod/quiz/view.php old mode 100755 new mode 100644 index 4f8c0861..e423fb32 --- a/mod/quiz/view.php +++ b/mod/quiz/view.php @@ -187,45 +187,105 @@ // Determine wheter a start attempt button should be displayed. $viewobj->quizhasquestions = $quizobj->has_questions(); -$viewobj->preventmessages = array(); -if (!$viewobj->quizhasquestions) { - $viewobj->buttontext = ''; -} else { - if ($unfinished) { - if ($canattempt) { - $viewobj->buttontext = get_string('continueattemptquiz', 'quiz'); - } else if ($canpreview) { - $viewobj->buttontext = get_string('continuepreview', 'quiz'); + + + + +/* Custom code start */ +#spoken db connection +global $USER; +include('spoken-config.php'); + +/* Update status to 2 once user finish the test */ +if(!empty($viewobj->attempts)) { + foreach($viewobj->attempts as $a){ + + if ('finished' === $a->state) { + + $sql = "select mdlcourse_id, mdlquiz_id, mdlattempt_id from training_eventteststatus where mdlemail = '".$USER->email."' and mdlcourse_id = ".$quiz->course." and mdlquiz_id = ".$quiz->id." and mdlattempt_id=".$a->id." and part_status = 1"; + + $result = $mysqli->query($sql); + $count = $result->num_rows; + + if ($count) { + $sql = "update training_eventteststatus set part_status = 2 where mdlemail = '".$USER->email."' and mdlcourse_id = ".$quiz->course." and mdlquiz_id = ".$quiz->id." and mdlattempt_id=".$a->id." and part_status = 1"; + $result = $mysqli->query($sql); + } } + } +} +$count = 0; + +/** +* When the user try to attempt the quiz very first time +*/ + +if(empty($viewobj->attempts)){ + + $sql = "select id from training_eventteststatus where mdlemail = '".$USER->email."' and mdlcourse_id = ".$quiz->course." and mdlquiz_id = ".$quiz->id." and part_status = 0"; + $result = $mysqli->query($sql); + $count = $result->num_rows; + +/** +* When the user try to do re-attempt +*/ + +}else{ + foreach($viewobj->attempts as $a){ + $sql = "select id from training_eventteststatus where mdlemail = '".$USER->email."' and mdlcourse_id = ".$quiz->course." and mdlquiz_id=".$a->quiz." and mdlattempt_id=".$a->id." and part_status <= 2"; + + $result = $mysqli->query($sql); + $count = $result->num_rows; + } +} + +if($count){ + /* original data start */ + + $viewobj->preventmessages = array(); + if (!$viewobj->quizhasquestions) { + $viewobj->buttontext = ''; } else { - if ($canattempt) { - $viewobj->preventmessages = $viewobj->accessmanager->prevent_new_attempt( - $viewobj->numattempts, $viewobj->lastfinishedattempt); - if ($viewobj->preventmessages) { - $viewobj->buttontext = ''; - } else if ($viewobj->numattempts == 0) { - $viewobj->buttontext = get_string('attemptquiznow', 'quiz'); - } else { - $viewobj->buttontext = get_string('reattemptquiz', 'quiz'); + if ($unfinished) { + if ($canattempt) { + $viewobj->buttontext = get_string('continueattemptquiz', 'quiz'); + } else if ($canpreview) { + $viewobj->buttontext = get_string('continuepreview', 'quiz'); } - } else if ($canpreview) { - $viewobj->buttontext = get_string('previewquiznow', 'quiz'); + } else { + if ($canattempt) { + $viewobj->preventmessages = $viewobj->accessmanager->prevent_new_attempt( + $viewobj->numattempts, $viewobj->lastfinishedattempt); + if ($viewobj->preventmessages) { + $viewobj->buttontext = ''; + } else if ($viewobj->numattempts == 0) { + $viewobj->buttontext = get_string('attemptquiznow', 'quiz'); + } else { + $viewobj->buttontext = get_string('reattemptquiz', 'quiz'); + } + + } else if ($canpreview) { + $viewobj->buttontext = get_string('previewquiznow', 'quiz'); + } } - } - // If, so far, we think a button should be printed, so check if they will be - // allowed to access it. - if ($viewobj->buttontext) { - if (!$viewobj->moreattempts) { - $viewobj->buttontext = ''; - } else if ($canattempt - && $viewobj->preventmessages = $viewobj->accessmanager->prevent_access()) { - $viewobj->buttontext = ''; + // If, so far, we think a button should be printed, so check if they will be + // allowed to access it. + if ($viewobj->buttontext) { + if (!$viewobj->moreattempts) { + $viewobj->buttontext = ''; + } else if ($canattempt + && $viewobj->preventmessages = $viewobj->accessmanager->prevent_access()) { + $viewobj->buttontext = ''; + } } } + /* original data end */ +} else{ + $viewobj->preventmessages = array("Your training attendance is not marked."); } $viewobj->showbacktocourse = ($viewobj->buttontext === '' && @@ -245,3 +305,4 @@ } echo $OUTPUT->footer(); + From 21e72e1524b7e8838513a489ce1fbb86569c9c68 Mon Sep 17 00:00:00 2001 From: Ganesh Mohite Date: Mon, 15 Mar 2021 11:21:11 +0530 Subject: [PATCH 2/5] adding condition to display quiz --- mod/quiz/view.php.bkp | 247 +++++++ question/type/multichoiceset/.travis.yml | 76 ++ question/type/multichoiceset/CHANGES.txt | 14 + question/type/multichoiceset/README.md | 45 ++ .../multichoiceset/backup/moodle1/lib.php | 110 +++ ...ckup_qtype_multichoiceset_plugin.class.php | 84 +++ ...tore_qtype_multichoiceset_plugin.class.php | 178 +++++ .../multichoiceset/classes/output/mobile.php | 58 ++ .../classes/privacy/provider.php | 46 ++ .../multichoiceset/combinable/combinable.php | 138 ++++ .../multichoiceset/combinable/renderer.php | 106 +++ question/type/multichoiceset/db/install.xml | 27 + question/type/multichoiceset/db/mobile.php | 46 ++ question/type/multichoiceset/db/upgrade.php | 288 ++++++++ .../type/multichoiceset/db/upgradelib.php | 164 +++++ .../edit_multichoiceset_form.php | 223 ++++++ .../lang/en/qtype_multichoiceset.php | 37 + .../lang/es/qtype_multichoiceset.php | 36 + question/type/multichoiceset/lib.php | 49 ++ .../multichoiceset/mobile/multichoiceset.html | 24 + .../multichoiceset/mobile/multichoiceset.js | 61 ++ question/type/multichoiceset/phpunit.xml | 37 + question/type/multichoiceset/pix/icon.gif | Bin 0 -> 204 bytes question/type/multichoiceset/pix/icon.svg | 64 ++ question/type/multichoiceset/question.php | 73 ++ question/type/multichoiceset/questiontype.php | 689 ++++++++++++++++++ question/type/multichoiceset/styles.css | 77 ++ .../multichoiceset/tests/behat/add.feature | 38 + .../tests/behat/backup_and_restore.feature | 48 ++ .../multichoiceset/tests/behat/edit.feature | 36 + .../multichoiceset/tests/behat/export.feature | 35 + .../multichoiceset/tests/behat/import.feature | 30 + .../tests/behat/preview.feature | 56 ++ .../fixtures/qtype_sample_multichoiceset.xml | 72 ++ question/type/multichoiceset/tests/helper.php | 245 +++++++ .../multichoiceset/tests/question_test.php | 142 ++++ .../tests/questiontype_test.php | 88 +++ .../multichoiceset/tests/walkthrough_test.php | 123 ++++ question/type/multichoiceset/version.php | 38 + 39 files changed, 3948 insertions(+) create mode 100755 mod/quiz/view.php.bkp create mode 100644 question/type/multichoiceset/.travis.yml create mode 100644 question/type/multichoiceset/CHANGES.txt create mode 100644 question/type/multichoiceset/README.md create mode 100644 question/type/multichoiceset/backup/moodle1/lib.php create mode 100644 question/type/multichoiceset/backup/moodle2/backup_qtype_multichoiceset_plugin.class.php create mode 100644 question/type/multichoiceset/backup/moodle2/restore_qtype_multichoiceset_plugin.class.php create mode 100644 question/type/multichoiceset/classes/output/mobile.php create mode 100644 question/type/multichoiceset/classes/privacy/provider.php create mode 100644 question/type/multichoiceset/combinable/combinable.php create mode 100644 question/type/multichoiceset/combinable/renderer.php create mode 100644 question/type/multichoiceset/db/install.xml create mode 100644 question/type/multichoiceset/db/mobile.php create mode 100644 question/type/multichoiceset/db/upgrade.php create mode 100644 question/type/multichoiceset/db/upgradelib.php create mode 100644 question/type/multichoiceset/edit_multichoiceset_form.php create mode 100644 question/type/multichoiceset/lang/en/qtype_multichoiceset.php create mode 100644 question/type/multichoiceset/lang/es/qtype_multichoiceset.php create mode 100644 question/type/multichoiceset/lib.php create mode 100644 question/type/multichoiceset/mobile/multichoiceset.html create mode 100644 question/type/multichoiceset/mobile/multichoiceset.js create mode 100644 question/type/multichoiceset/phpunit.xml create mode 100644 question/type/multichoiceset/pix/icon.gif create mode 100644 question/type/multichoiceset/pix/icon.svg create mode 100644 question/type/multichoiceset/question.php create mode 100644 question/type/multichoiceset/questiontype.php create mode 100644 question/type/multichoiceset/styles.css create mode 100644 question/type/multichoiceset/tests/behat/add.feature create mode 100644 question/type/multichoiceset/tests/behat/backup_and_restore.feature create mode 100644 question/type/multichoiceset/tests/behat/edit.feature create mode 100644 question/type/multichoiceset/tests/behat/export.feature create mode 100644 question/type/multichoiceset/tests/behat/import.feature create mode 100644 question/type/multichoiceset/tests/behat/preview.feature create mode 100644 question/type/multichoiceset/tests/fixtures/qtype_sample_multichoiceset.xml create mode 100644 question/type/multichoiceset/tests/helper.php create mode 100644 question/type/multichoiceset/tests/question_test.php create mode 100644 question/type/multichoiceset/tests/questiontype_test.php create mode 100644 question/type/multichoiceset/tests/walkthrough_test.php create mode 100644 question/type/multichoiceset/version.php diff --git a/mod/quiz/view.php.bkp b/mod/quiz/view.php.bkp new file mode 100755 index 00000000..4f8c0861 --- /dev/null +++ b/mod/quiz/view.php.bkp @@ -0,0 +1,247 @@ +. + +/** + * This page is the entry page into the quiz UI. Displays information about the + * quiz to students and teachers, and lets students see their previous attempts. + * + * @package mod_quiz + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(__DIR__ . '/../../config.php'); +require_once($CFG->libdir.'/gradelib.php'); +require_once($CFG->dirroot.'/mod/quiz/locallib.php'); +require_once($CFG->libdir . '/completionlib.php'); +require_once($CFG->dirroot . '/course/format/lib.php'); + +$id = optional_param('id', 0, PARAM_INT); // Course Module ID, or ... +$q = optional_param('q', 0, PARAM_INT); // Quiz ID. + +if ($id) { + if (!$cm = get_coursemodule_from_id('quiz', $id)) { + print_error('invalidcoursemodule'); + } + if (!$course = $DB->get_record('course', array('id' => $cm->course))) { + print_error('coursemisconf'); + } +} else { + if (!$quiz = $DB->get_record('quiz', array('id' => $q))) { + print_error('invalidquizid', 'quiz'); + } + if (!$course = $DB->get_record('course', array('id' => $quiz->course))) { + print_error('invalidcourseid'); + } + if (!$cm = get_coursemodule_from_instance("quiz", $quiz->id, $course->id)) { + print_error('invalidcoursemodule'); + } +} + +// Check login and get context. +require_login($course, false, $cm); +$context = context_module::instance($cm->id); +require_capability('mod/quiz:view', $context); + +// Cache some other capabilities we use several times. +$canattempt = has_capability('mod/quiz:attempt', $context); +$canreviewmine = has_capability('mod/quiz:reviewmyattempts', $context); +$canpreview = has_capability('mod/quiz:preview', $context); + +// Create an object to manage all the other (non-roles) access rules. +$timenow = time(); +$quizobj = quiz::create($cm->instance, $USER->id); +$accessmanager = new quiz_access_manager($quizobj, $timenow, + has_capability('mod/quiz:ignoretimelimits', $context, null, false)); +$quiz = $quizobj->get_quiz(); + +// Trigger course_module_viewed event and completion. +quiz_view($quiz, $course, $cm, $context); + +// Initialize $PAGE, compute blocks. +$PAGE->set_url('/mod/quiz/view.php', array('id' => $cm->id)); + +// Create view object which collects all the information the renderer will need. +$viewobj = new mod_quiz_view_object(); +$viewobj->accessmanager = $accessmanager; +$viewobj->canreviewmine = $canreviewmine || $canpreview; + +// Get this user's attempts. +$attempts = quiz_get_user_attempts($quiz->id, $USER->id, 'finished', true); +$lastfinishedattempt = end($attempts); +$unfinished = false; +$unfinishedattemptid = null; +if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) { + $attempts[] = $unfinishedattempt; + + // If the attempt is now overdue, deal with that - and pass isonline = false. + // We want the student notified in this case. + $quizobj->create_attempt_object($unfinishedattempt)->handle_if_time_expired(time(), false); + + $unfinished = $unfinishedattempt->state == quiz_attempt::IN_PROGRESS || + $unfinishedattempt->state == quiz_attempt::OVERDUE; + if (!$unfinished) { + $lastfinishedattempt = $unfinishedattempt; + } + $unfinishedattemptid = $unfinishedattempt->id; + $unfinishedattempt = null; // To make it clear we do not use this again. +} +$numattempts = count($attempts); + +$viewobj->attempts = $attempts; +$viewobj->attemptobjs = array(); +foreach ($attempts as $attempt) { + $viewobj->attemptobjs[] = new quiz_attempt($attempt, $quiz, $cm, $course, false); +} + +// Work out the final grade, checking whether it was overridden in the gradebook. +if (!$canpreview) { + $mygrade = quiz_get_best_grade($quiz, $USER->id); +} else if ($lastfinishedattempt) { + // Users who can preview the quiz don't get a proper grade, so work out a + // plausible value to display instead, so the page looks right. + $mygrade = quiz_rescale_grade($lastfinishedattempt->sumgrades, $quiz, false); +} else { + $mygrade = null; +} + +$mygradeoverridden = false; +$gradebookfeedback = ''; + +$grading_info = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $USER->id); +if (!empty($grading_info->items)) { + $item = $grading_info->items[0]; + if (isset($item->grades[$USER->id])) { + $grade = $item->grades[$USER->id]; + + if ($grade->overridden) { + $mygrade = $grade->grade + 0; // Convert to number. + $mygradeoverridden = true; + } + if (!empty($grade->str_feedback)) { + $gradebookfeedback = $grade->str_feedback; + } + } +} + +$title = $course->shortname . ': ' . format_string($quiz->name); +$PAGE->set_title($title); +$PAGE->set_heading($course->fullname); +$output = $PAGE->get_renderer('mod_quiz'); + +// Print table with existing attempts. +if ($attempts) { + // Work out which columns we need, taking account what data is available in each attempt. + list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts); + + $viewobj->attemptcolumn = $quiz->attempts != 1; + + $viewobj->gradecolumn = $someoptions->marks >= question_display_options::MARK_AND_MAX && + quiz_has_grades($quiz); + $viewobj->markcolumn = $viewobj->gradecolumn && ($quiz->grade != $quiz->sumgrades); + $viewobj->overallstats = $lastfinishedattempt && $alloptions->marks >= question_display_options::MARK_AND_MAX; + + $viewobj->feedbackcolumn = quiz_has_feedback($quiz) && $alloptions->overallfeedback; +} + +$viewobj->timenow = $timenow; +$viewobj->numattempts = $numattempts; +$viewobj->mygrade = $mygrade; +$viewobj->moreattempts = $unfinished || + !$accessmanager->is_finished($numattempts, $lastfinishedattempt); +$viewobj->mygradeoverridden = $mygradeoverridden; +$viewobj->gradebookfeedback = $gradebookfeedback; +$viewobj->lastfinishedattempt = $lastfinishedattempt; +$viewobj->canedit = has_capability('mod/quiz:manage', $context); +$viewobj->editurl = new moodle_url('/mod/quiz/edit.php', array('cmid' => $cm->id)); +$viewobj->backtocourseurl = new moodle_url('/course/view.php', array('id' => $course->id)); +$viewobj->startattempturl = $quizobj->start_attempt_url(); + +if ($accessmanager->is_preflight_check_required($unfinishedattemptid)) { + $viewobj->preflightcheckform = $accessmanager->get_preflight_check_form( + $viewobj->startattempturl, $unfinishedattemptid); +} +$viewobj->popuprequired = $accessmanager->attempt_must_be_in_popup(); +$viewobj->popupoptions = $accessmanager->get_popup_options(); + +// Display information about this quiz. +$viewobj->infomessages = $viewobj->accessmanager->describe_rules(); +if ($quiz->attempts != 1) { + $viewobj->infomessages[] = get_string('gradingmethod', 'quiz', + quiz_get_grading_option_name($quiz->grademethod)); +} + +// Determine wheter a start attempt button should be displayed. +$viewobj->quizhasquestions = $quizobj->has_questions(); +$viewobj->preventmessages = array(); +if (!$viewobj->quizhasquestions) { + $viewobj->buttontext = ''; + +} else { + if ($unfinished) { + if ($canattempt) { + $viewobj->buttontext = get_string('continueattemptquiz', 'quiz'); + } else if ($canpreview) { + $viewobj->buttontext = get_string('continuepreview', 'quiz'); + } + + } else { + if ($canattempt) { + $viewobj->preventmessages = $viewobj->accessmanager->prevent_new_attempt( + $viewobj->numattempts, $viewobj->lastfinishedattempt); + if ($viewobj->preventmessages) { + $viewobj->buttontext = ''; + } else if ($viewobj->numattempts == 0) { + $viewobj->buttontext = get_string('attemptquiznow', 'quiz'); + } else { + $viewobj->buttontext = get_string('reattemptquiz', 'quiz'); + } + + } else if ($canpreview) { + $viewobj->buttontext = get_string('previewquiznow', 'quiz'); + } + } + + // If, so far, we think a button should be printed, so check if they will be + // allowed to access it. + if ($viewobj->buttontext) { + if (!$viewobj->moreattempts) { + $viewobj->buttontext = ''; + } else if ($canattempt + && $viewobj->preventmessages = $viewobj->accessmanager->prevent_access()) { + $viewobj->buttontext = ''; + } + } +} + +$viewobj->showbacktocourse = ($viewobj->buttontext === '' && + course_get_format($course)->has_view_page()); + +echo $OUTPUT->header(); + +if (isguestuser()) { + // Guests can't do a quiz, so offer them a choice of logging in or going back. + echo $output->view_page_guest($course, $quiz, $cm, $context, $viewobj->infomessages); +} else if (!isguestuser() && !($canattempt || $canpreview + || $viewobj->canreviewmine)) { + // If they are not enrolled in this course in a good enough role, tell them to enrol. + echo $output->view_page_notenrolled($course, $quiz, $cm, $context, $viewobj->infomessages); +} else { + echo $output->view_page($course, $quiz, $cm, $context, $viewobj); +} + +echo $OUTPUT->footer(); diff --git a/question/type/multichoiceset/.travis.yml b/question/type/multichoiceset/.travis.yml new file mode 100644 index 00000000..f52e2025 --- /dev/null +++ b/question/type/multichoiceset/.travis.yml @@ -0,0 +1,76 @@ +language: php + +os: linux +dist: xenial + +services: + - mysql + - postgresql + +addons: + firefox: "47.0.1" + apt: + packages: + - openjdk-8-jre-headless + - chromium-chromedriver + +cache: + directories: + - $HOME/.composer/cache + - $HOME/.npm + +matrix: + include: + - php: 7.3 + env: + - MOODLE_BRANCH=master + - DB=mysqli + + - php: 7.3 + env: + - MOODLE_BRANCH=MOODLE_38_STABLE + - DB=pgsql + - CHECK_GRUNT=yes + + - php: 7.2 + env: + - MOODLE_BRANCH=MOODLE_37_STABLE + - DB=mysqli + + - php: 7.1 + env: + - MOODLE_BRANCH=MOODLE_36_STABLE + - DB=pgsql + - NODE=8.9 + +before_install: + - phpenv config-rm xdebug.ini + + - if [ -z $CHECK_GRUNT ]; then + export CHECK_GRUNT=no; + fi + + - if [ -z $NODE ]; then + export NODE=14; + fi + - nvm install $NODE + - nvm use $NODE + + - cd ../.. + - composer create-project -n --no-dev --prefer-dist moodlerooms/moodle-plugin-ci ci ^2 + - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" + +install: + - moodle-plugin-ci install + +script: + - moodle-plugin-ci phplint + - moodle-plugin-ci phpcpd + - moodle-plugin-ci phpmd + - moodle-plugin-ci codechecker + - moodle-plugin-ci validate + - moodle-plugin-ci savepoints + - moodle-plugin-ci mustache + - moodle-plugin-ci grunt || [ "$CHECK_GRUNT" = 'no' ] + - moodle-plugin-ci phpunit + - moodle-plugin-ci behat diff --git a/question/type/multichoiceset/CHANGES.txt b/question/type/multichoiceset/CHANGES.txt new file mode 100644 index 00000000..c0b004f4 --- /dev/null +++ b/question/type/multichoiceset/CHANGES.txt @@ -0,0 +1,14 @@ +Release notes +------------- + +Date Version Comment +2020/11/20 1.7.1 Add support for standard instruction flag in XML import/export. +2020/11/17 1.7.0 Add support for displaying standard instructions. +2020/11/16 1.6.7 Fix 3.9 answer vertical alignment issue by copying CSS from standard multichoice question. +2020/07/10 1.6.6 Fix 3.9 presentation issue (solution provided by James Garrett). +2019/05/08 1.6.5 Add support for combinable question type, improve error recovery and testing (provided by J-M Vedrine) +2019/04/05 1.6.4 Add support for Spanish (International) (provided by Carlos Pasqualini) +2019/01/29 1.6.3 Add support for mobile app 3.6 (provided by Zvonko Martinovic) +2019/01/08 1.6.2 Fix handling of default numbering style +2018/02/05 1.6.1 Fix presentation of answers in Boost theme, fix some PHPDocs warnings +2018/01/31 1.6 Support Moodle mobile app diff --git a/question/type/multichoiceset/README.md b/question/type/multichoiceset/README.md new file mode 100644 index 00000000..e8ba46c0 --- /dev/null +++ b/question/type/multichoiceset/README.md @@ -0,0 +1,45 @@ +All or Nothing Question type +---------------------------- + +This is a multiple-choice, multiple-response question type that was created by +Adriane Boyd (adrianeboyd@gmail.com), later maintained by Jean-Michel Vedrine (vedrine@vedrine.net) +and is now maintained by Eoin Campbell. + +This version can be used with Moodle 3.x versions. + +The official git repository of this question type is now https://github.com/ecampbell/moodle-qtype_multichoiceset + +### Description: + +The all or nothing question is adapted from the existing multichoice question. +The main difference from the standard Moodle multiple choice question type is +in the way that grading works. +The teacher editing interface is slightly modified as when creating the question, the teacher just +indicates which choices are correct. + +### Grading: + +In an all-or-nothing multiple choice question, a respondent can choose one or more answers. +If the chosen answers correspond exactly to the correct choices defined in the question, the respondent gets 100%. +If he/she chooses any incorrect choices or does not select all of the correct choices, the grade is 0%. +Before using this questiontype, teachers must really think if this grading is what they want. + +### Installation + +#### Installation from the Moodle plugins Directory (preferred method) +This question type is available at https://moodle.org/plugins/view/qtype_multichoiceset +Install like any other plugin. + +#### Installation Using Git + +To install using git, type this command in the root of your Moodle install: + + git clone git://github.com/ecampbell/moodle-qtype_multichoiceset.git question/type/multichoiceset + echo '/question/type/multichoiceset' >> .git/info/exclude + +#### Installation From Downloaded zip file + +Alternatively, download the zip from: + https://github.com/ecampbell/moodle-qtype_multichoiceset/archive/master.zip + +unzip it into the question/type folder, and then rename the new folder to multichoiceset. diff --git a/question/type/multichoiceset/backup/moodle1/lib.php b/question/type/multichoiceset/backup/moodle1/lib.php new file mode 100644 index 00000000..0caf8e17 --- /dev/null +++ b/question/type/multichoiceset/backup/moodle1/lib.php @@ -0,0 +1,110 @@ +. + +/** + * Backup handler for Moodle 1.x Multichoiceset questions + * + * @package qtype_multichoiceset + * @copyright 2011 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * multichoiceset question type conversion handler + * + * @copyright 2011 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class moodle1_qtype_multichoiceset_handler extends moodle1_qtype_handler { + + /** + * Return the subpaths within a question + * + * @return array + */ + public function get_question_subpaths() { + return array( + 'ANSWERS/ANSWER', + 'MULTICHOICESET', + ); + } + + /** + * Appends the multichoiceset specific information to the question + * + * @param array $data the question data + * @param array $raw unused + */ + public function process_question(array $data, array $raw) { + + // Convert and write the answers first. + if (isset($data['answers'])) { + $this->write_answers($data['answers'], $this->pluginname); + } + + // Convert and write the multichoiceset. + if (!isset($data['multichoiceset'])) { + // This should never happen, but it can do if the 1.9 site contained + // corrupt data. + $data['multichoiceset'] = array(array( + 'shuffleanswers' => 1, + 'correctfeedback' => '', + 'correctfeedbackformat' => FORMAT_HTML, + 'incorrectfeedback' => '', + 'incorrectfeedbackformat' => FORMAT_HTML, + 'answernumbering' => 'abc', + 'shownumcorrect' => 0 + 'showstandardinstruction' => 0 + )); + } + $this->write_multichoiceset($data['multichoiceset'], $data['oldqtextformat']); + } + + /** + * Converts the multichoiceset info and writes it into question XML + * + * @param array $multichoicesets the grouped structure + * @param int $oldqtextformat - (see moodle1_question_bank_handler::process_question()) + */ + protected function write_multichoiceset(array $multichoicesets, $oldqtextformat) { + global $CFG; + + // The grouped array is supposed to have just one element - let us use foreach anyway + // just to be sure we do not loose anything. + foreach ($multichoicesets as $multichoiceset) { + // Append an artificial 'id' attribute (is not included in moodle.xml). + $multichoiceset['id'] = $this->converter->get_nextid(); + + // Replay the upgrade step 2009021801. + $multichoiceset['correctfeedbackformat'] = 0; + $multichoiceset['incorrectfeedbackformat'] = 0; + + if ($CFG->texteditors !== 'textarea' and $oldqtextformat == FORMAT_MOODLE) { + $multichoiceset['correctfeedback'] = text_to_html($multichoiceset['correctfeedback'], false, false, true); + $multichoiceset['correctfeedbackformat'] = FORMAT_HTML; + $multichoiceset['incorrectfeedback'] = text_to_html($multichoiceset['incorrectfeedback'], false, false, true); + $multichoiceset['incorrectfeedbackformat'] = FORMAT_HTML; + } else { + $multichoiceset['correctfeedbackformat'] = $oldqtextformat; + $multichoiceset['incorrectfeedbackformat'] = $oldqtextformat; + } + + $this->write_xml('multichoiceset', $multichoiceset, array('/multichoiceset/id')); + } + } +} diff --git a/question/type/multichoiceset/backup/moodle2/backup_qtype_multichoiceset_plugin.class.php b/question/type/multichoiceset/backup/moodle2/backup_qtype_multichoiceset_plugin.class.php new file mode 100644 index 00000000..7e84357a --- /dev/null +++ b/question/type/multichoiceset/backup/moodle2/backup_qtype_multichoiceset_plugin.class.php @@ -0,0 +1,84 @@ +. + +/** + * Backup handler for Moodle 2.x/3.x Multichoiceset questions + * + * @package qtype_multichoiceset + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Provides the information to backup multichoiceset questions + * + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_qtype_multichoiceset_plugin extends backup_qtype_plugin { + + /** + * Returns the qtype information to attach to question element + */ + protected function define_question_plugin_structure() { + + // Define the virtual plugin element with the condition to fulfill. + $plugin = $this->get_plugin_element(null, '../../qtype', 'multichoiceset'); + + // Create one standard named plugin element (the visible container). + $pluginwrapper = new backup_nested_element($this->get_recommended_name()); + + // Connect the visible container ASAP. + $plugin->add_child($pluginwrapper); + + // This qtype uses standard question_answers, add them here + // to the tree before any other information that will use them. + $this->add_question_question_answers($pluginwrapper); + + // Now create the qtype own structures. + $multichoiceset = new backup_nested_element('multichoiceset', array('id'), array( + 'layout', 'shuffleanswers', + 'correctfeedback', 'correctfeedbackformat', + 'incorrectfeedback', 'incorrectfeedbackformat', 'answernumbering', + 'shownumcorrect', 'showstandardinstruction')); + + // Now the own qtype tree. + $pluginwrapper->add_child($multichoiceset); + + // Set source to populate the data. + $multichoiceset->set_source_table('qtype_multichoiceset_options', array('questionid' => backup::VAR_PARENTID)); + + // Don't need to annotate ids nor files. + + return $plugin; + } + + /** + * Returns one array with filearea => mappingname elements for the qtype + * + * Used by {@link get_components_and_fileareas} to know about all the qtype + * files to be processed both in backup and restore. + */ + public static function get_qtype_fileareas() { + return array( + 'correctfeedback' => 'question_created', + 'incorrectfeedback' => 'question_created'); + } +} diff --git a/question/type/multichoiceset/backup/moodle2/restore_qtype_multichoiceset_plugin.class.php b/question/type/multichoiceset/backup/moodle2/restore_qtype_multichoiceset_plugin.class.php new file mode 100644 index 00000000..12c37852 --- /dev/null +++ b/question/type/multichoiceset/backup/moodle2/restore_qtype_multichoiceset_plugin.class.php @@ -0,0 +1,178 @@ +. + +/** + * Restore handler for Moodle 2.x/3.x Multichoiceset questions + * + * @package qtype_multichoiceset + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Restore plugin class that provides the information needed to restore one multichoiceset qtype + * + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_qtype_multichoiceset_plugin extends restore_qtype_plugin { + + /** + * Returns the paths to be handled by the plugin at question level + */ + protected function define_question_plugin_structure() { + + $paths = array(); + + // This qtype uses question_answers, add them. + $this->add_question_question_answers($paths); + + // Add own qtype stuff. + $elename = 'multichoiceset'; + // We used get_recommended_name() so this works. + $elepath = $this->get_pathfor('/multichoiceset'); + $paths[] = new restore_path_element($elename, $elepath); + + return $paths; // And we return the interesting paths. + } + + /** + * Process the qtype/multichoiceset element + * + * @param stdObject $data question data + * @return void + */ + public function process_multichoiceset($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + // Detect if the question is created or mapped. + $oldquestionid = $this->get_old_parentid('question'); + $newquestionid = $this->get_new_parentid('question'); + $questioncreated = (bool) $this->get_mappingid('question_created', $oldquestionid); + + // If the question has been created by restore, we need to create its + // qtype_multichoiceset_options too. + if ($questioncreated) { + // Adjust some columns. + $data->questionid = $newquestionid; + + // It is possible for old backup files to contain unique key violations. + // We need to check to avoid that. + if (!$DB->record_exists('qtype_multichoiceset_options', array('questionid' => $data->questionid))) { + $newitemid = $DB->insert_record('qtype_multichoiceset_options', $data); + $this->set_mapping('qtype_multichoiceset_options', $oldid, $newitemid); + } + } + } + + /** + * Recode the response to the question + * + * @param int $questionid Question ID + * @param int $sequencenumber Sequence number of question (or attempt?) + * @param array $response re-ordered response + */ + public function recode_response($questionid, $sequencenumber, array $response) { + if (array_key_exists('_order', $response)) { + $response['_order'] = $this->recode_choice_order($response['_order']); + } + return $response; + } + + /** + * Recode the choice order as stored in the response. + * + * @param string $order the original order. + * @return string the recoded order. + */ + protected function recode_choice_order($order) { + $neworder = array(); + foreach (explode(',', $order) as $id) { + if ($newid = $this->get_mappingid('question_answer', $id)) { + $neworder[] = $newid; + } + } + return implode(',', $neworder); + } + + /** + * Given one question_states record, return the answer + * recoded pointing to all the restored stuff for multichoiceset questions + * + * answer are two (hypen speparated) lists of comma separated question_answers + * the first to specify the order of the answers and the second to specify the + * responses. Note the order list (the first one) can be optional + * + * @param stdObject $state the question states record + * @return string the question answers + */ + public function recode_legacy_state_answer($state) { + $answer = $state->answer; + $orderarr = array(); + $responsesarr = array(); + $lists = explode(':', $answer); + // If only 1 list, answer is missing the order list, adjust. + if (count($lists) == 1) { + $lists[1] = $lists[0]; // Here we have the responses. + $lists[0] = ''; // Here we have the order. + } + // Map order. + if (!empty($lists[0])) { + foreach (explode(',', $lists[0]) as $id) { + if ($newid = $this->get_mappingid('question_answer', $id)) { + $orderarr[] = $newid; + } + } + } + // Map responses. + if (!empty($lists[1])) { + foreach (explode(',', $lists[1]) as $id) { + if ($newid = $this->get_mappingid('question_answer', $id)) { + $responsesarr[] = $newid; + } + } + } + // Build the final answer, if not order, only responses. + $result = ''; + if (empty($orderarr)) { + $result = implode(',', $responsesarr); + } else { + $result = implode(',', $orderarr) . ':' . implode(',', $responsesarr); + } + return $result; + } + + /** + * Return the contents of this qtype to be processed by the links decoder + */ + public static function define_decode_contents() { + + $contents = array(); + + $fields = array('correctfeedback', 'incorrectfeedback'); + $contents[] = new restore_decode_content('qtype_multichoiceset_options', + $fields, 'qtype_multichoiceset_options'); + + return $contents; + } +} diff --git a/question/type/multichoiceset/classes/output/mobile.php b/question/type/multichoiceset/classes/output/mobile.php new file mode 100644 index 00000000..4b425253 --- /dev/null +++ b/question/type/multichoiceset/classes/output/mobile.php @@ -0,0 +1,58 @@ +. + +/** + * Mobile output class for question type multichoiceset. + * + * @package qtype_multichoiceset + * @copyright 2018 Zvonko Martinovic + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_multichoiceset\output; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Mobile output class for question type multichoiceset. + * + * @package qtype_multichoiceset + * @copyright 2018 Zvonko Martinovic + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mobile { + + /** + * Returns the gapfill question type for the quiz in the mobile app. + * + * @return void + */ + public static function mobile_get_multichoiceset() { + global $CFG; + // General notes: + // If you have worked on mobile activities, there is no cmid or courseid in $args here. + // This is not equivalent to mod/quiz/attempt.php?attempt=57&cmid=147, rather + // this is just a section of that page, with all the access checking already done for us. + // The full file path is required even though file_get_contents should work with relative paths. + return [ + 'templates' => [[ + 'id' => 'main', + 'html' => file_get_contents($CFG->dirroot . '/question/type/multichoiceset/mobile/multichoiceset.html') + ]], + 'javascript' => file_get_contents($CFG->dirroot . '/question/type/multichoiceset/mobile/multichoiceset.js') + ]; + } +} diff --git a/question/type/multichoiceset/classes/privacy/provider.php b/question/type/multichoiceset/classes/privacy/provider.php new file mode 100644 index 00000000..71cde46d --- /dev/null +++ b/question/type/multichoiceset/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_multichoiceset. + * + * @package qtype_multichoiceset + * @copyright 2019 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_multichoiceset\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_multichoiceset implementing null_provider. + * + * @copyright 2019 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} \ No newline at end of file diff --git a/question/type/multichoiceset/combinable/combinable.php b/question/type/multichoiceset/combinable/combinable.php new file mode 100644 index 00000000..cfe5933b --- /dev/null +++ b/question/type/multichoiceset/combinable/combinable.php @@ -0,0 +1,138 @@ +. + +/** + * Defines the hooks necessary to make the oumultiresponse question type combinable + * + * @package qtype_multichoiceset + * @copyright 2019 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +class qtype_combined_combinable_type_multichoiceset extends qtype_combined_combinable_type_base { + + protected $identifier = 'allornothing'; + + protected function extra_question_properties() { + return array('answernumbering' => 'abc') + $this->combined_feedback_properties(); + } + + protected function extra_answer_properties() { + return array('feedback' => array('text' => '', 'format' => FORMAT_PLAIN)); + } + + public function subq_form_fragment_question_option_fields() { + return array('shuffleanswers' => false); + } + + protected function transform_subq_form_data_to_full($subqdata) { + $data = parent::transform_subq_form_data_to_full($subqdata); + foreach ($data->answer as $anskey => $answer) { + $data->answer[$anskey] = array('text' => $answer['text'], 'format' => $answer['format']); + } + return $this->add_per_answer_properties($data); + } + + protected function third_param_for_default_question_text() { + return 'v'; + } +} + +class qtype_combined_combinable_multichoiceset extends qtype_combined_combinable_accepts_vertical_or_horizontal_layout_param { + + /** + * @param moodleform $combinedform + * @param MoodleQuickForm $mform + * @param $repeatenabled + */ + public function add_form_fragment(moodleform $combinedform, MoodleQuickForm $mform, $repeatenabled) { + $mform->addElement('advcheckbox', $this->form_field_name('shuffleanswers'), get_string('shuffle', 'qtype_gapselect')); + + $answerels = array(); + $answerels[] = $mform->createElement('editor', $this->form_field_name('answer'), + get_string('choiceno', 'qtype_multichoice', '{no}'), ['rows' => 1]); + $mform->setType($this->form_field_name('answer'), PARAM_RAW); + $answerels[] = $mform->createElement('advcheckbox', $this->form_field_name('correctanswer'), + get_string('correct', 'question'), get_string('correct', 'question')); + + $answergroupel = $mform->createElement('group', + $this->form_field_name('answergroup'), + get_string('choiceno', 'qtype_multichoice', '{no}'), + $answerels, null, false); + if ($this->questionrec !== null) { + $countanswers = count($this->questionrec->options->answers); + } else { + $countanswers = 0; + } + + if ($repeatenabled) { + $defaultstartnumbers = QUESTION_NUMANS_START * 2; + $repeatsatstart = max($defaultstartnumbers, $countanswers + QUESTION_NUMANS_ADD); + } else { + $repeatsatstart = $countanswers; + } + + $combinedform->repeat_elements(array($answergroupel), + $repeatsatstart, + array(), + $this->form_field_name('noofchoices'), + $this->form_field_name('morechoices'), + QUESTION_NUMANS_ADD, + get_string('addmorechoiceblanks', 'qtype_gapselect'), + true); + + } + + public function data_to_form($context, $fileoptions) { + $mroptions = array('answer' => array(), 'correctanswer' => array()); + if ($this->questionrec !== null) { + foreach ($this->questionrec->options->answers as $questionrecanswer) { + $mroptions['answer'][]['text'] = $questionrecanswer->answer; + $mroptions['correctanswer'][] = $questionrecanswer->fraction > 0; + } + } + return parent::data_to_form($context, $fileoptions) + $mroptions; + } + + public function validate() { + $errors = array(); + $nonemptyanswerblanks = array(); + foreach ($this->formdata->answer as $anskey => $answer) { + $answer = $answer['text']; + if ('' !== trim($answer)) { + $nonemptyanswerblanks[] = $anskey; + } else if ($this->formdata->correctanswer[$anskey]) { + $errors[$this->form_field_name("answergroup[{$anskey}]")] = get_string('err_correctanswerblank', + 'qtype_multichoiceset'); + } + } + if (count($nonemptyanswerblanks) < 2) { + $errors[$this->form_field_name("answergroup[0]")] = get_string('err_youneedmorechoices', 'qtype_multichoiceset'); + } + if (count(array_filter($this->formdata->correctanswer)) === 0) { + $errors[$this->form_field_name("answergroup[0]")] = get_string('err_nonecorrect', 'qtype_multichoiceset'); + } + return $errors; + } + + public function has_submitted_data() { + return $this->submitted_data_array_not_empty('correctanswer') || + $this->html_field_has_submitted_data($this->form_field_name('answer')) || + parent::has_submitted_data(); + } +} diff --git a/question/type/multichoiceset/combinable/renderer.php b/question/type/multichoiceset/combinable/renderer.php new file mode 100644 index 00000000..2f1fe745 --- /dev/null +++ b/question/type/multichoiceset/combinable/renderer.php @@ -0,0 +1,106 @@ +. + +/** + * Combined question embedded sub question renderer class. + * + * @package qtype_oumultiresponse + * @copyright 2019 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +class qtype_multichoiceset_embedded_renderer extends qtype_renderer + implements qtype_combined_subquestion_renderer_interface { + + public function subquestion(question_attempt $qa, + question_display_options $options, + qtype_combined_combinable_base $subq, + $placeno) { + $question = $subq->question; + $fullresponse = new qtype_combined_response_array_param($qa->get_last_qt_data()); + $response = $fullresponse->for_subq($subq); + + $commonattributes = array( + 'type' => 'checkbox' + ); + + if ($options->readonly) { + $commonattributes['disabled'] = 'disabled'; + } + + $checkboxes = array(); + $feedbackimg = array(); + $classes = array(); + foreach ($question->get_order($qa) as $value => $ansid) { + $inputname = $qa->get_qt_field_name($subq->step_data_name('choice'.$value)); + $ans = $question->answers[$ansid]; + $inputattributes = array(); + $inputattributes['name'] = $inputname; + $inputattributes['value'] = 1; + $inputattributes['id'] = $inputname; + $isselected = $question->is_choice_selected($response, $value); + if ($isselected) { + $inputattributes['checked'] = 'checked'; + } + $hidden = ''; + if (!$options->readonly) { + $hidden = html_writer::empty_tag('input', array( + 'type' => 'hidden', + 'name' => $inputattributes['name'], + 'value' => 0, + )); + } + $cblabel = $question->make_html_inline($question->format_text( + $ans->answer, $ans->answerformat, + $qa, 'question', 'answer', $ansid)); + + $cblabeltag = html_writer::tag('label', $cblabel, array('for' => $inputattributes['id'])); + + $checkboxes[] = $hidden . html_writer::empty_tag('input', $inputattributes + $commonattributes) . $cblabeltag; + + $class = 'r' . ($value % 2); + if ($options->correctness && $isselected) { + $iscbcorrect = ($ans->fraction > 0) ? 1 : 0; + $feedbackimg[] = $this->feedback_image($iscbcorrect); + $class .= ' ' . $this->feedback_class($iscbcorrect); + } else { + $feedbackimg[] = ''; + } + $classes[] = $class; + } + + $cbhtml = ''; + + if ('h' === $subq->get_layout()) { + $inputwraptag = 'span'; + } else { + $inputwraptag = 'div'; + } + + foreach ($checkboxes as $key => $checkbox) { + $cbhtml .= html_writer::tag($inputwraptag, $checkbox . ' ' . $feedbackimg[$key], + array('class' => $classes[$key])) . "\n"; + } + + $result = html_writer::tag($inputwraptag, $cbhtml, array('class' => 'answer')); + + return $result; + } +} diff --git a/question/type/multichoiceset/db/install.xml b/question/type/multichoiceset/db/install.xml new file mode 100644 index 00000000..607e27ba --- /dev/null +++ b/question/type/multichoiceset/db/install.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/question/type/multichoiceset/db/mobile.php b/question/type/multichoiceset/db/mobile.php new file mode 100644 index 00000000..a9911aee --- /dev/null +++ b/question/type/multichoiceset/db/mobile.php @@ -0,0 +1,46 @@ +. + +/** + * qtype_multichoiceset capability definition + * + * @package qtype_multichoiceset + * @copyright 2018 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +$addons = array( + "qtype_multichoiceset" => array( + 'handlers' => array( // Different places where the plugin will display content. + 'multichoiceset' => array( // Handler unique name (alphanumeric). + 'displaydata' => array( + 'title' => 'All-or-Nothing Multiple Choice question', + 'icon' => $CFG->wwwroot . '/question/type/multichoiceset/pix/icon.gif', + 'class' => '', + ), + 'delegate' => 'CoreQuestionDelegate', // Delegate (where to display the link to the plugin). + 'method' => 'mobile_get_multichoiceset', // Main function in \qtype_multichoiceset\output. + 'offlinefunctions' => array( + 'mobile_get_multichoiceset' => array(), // Function that needs to be downloaded for offline use. + ) + ) + ), + 'lang' => array( // Language strings that are used in all the handlers. + array('pluginname', 'qtype_multichoiceset') + ) + ) +); \ No newline at end of file diff --git a/question/type/multichoiceset/db/upgrade.php b/question/type/multichoiceset/db/upgrade.php new file mode 100644 index 00000000..63679176 --- /dev/null +++ b/question/type/multichoiceset/db/upgrade.php @@ -0,0 +1,288 @@ +. + +/** + * All or nothing multiple choice question type upgrade code. + * + * @package qtype_multichoiceset + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Converts the multichoiceset info and writes it into the question.xml + * + * @param int $oldversion the old (i.e. current) version of Moodle + */ +function xmldb_qtype_multichoiceset_upgrade($oldversion) { + global $CFG, $DB; + + $dbman = $DB->get_manager(); + + if ($oldversion < 2011010400) { + + // Define field correctfeedbackformat to be added to question_multichoiceset. + $table = new xmldb_table('question_multichoiceset'); + $field = new xmldb_field('correctfeedbackformat', XMLDB_TYPE_INTEGER, '2', + null, XMLDB_NOTNULL, null, '0', 'correctfeedback'); + + // Conditionally launch add field correctfeedbackformat. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field incorrectfeedbackformat to be added to question_multichoiceset. + $field = new xmldb_field('incorrectfeedbackformat', XMLDB_TYPE_INTEGER, '2', + null, XMLDB_NOTNULL, null, '0', 'incorrectfeedback'); + + // Conditionally launch add field incorrectfeedbackformat. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // In the past, the correctfeedback, partiallycorrectfeedback, + // incorrectfeedback columns were assumed to contain content of the same + // form as questiontextformat. If we are using the HTML editor, then + // convert FORMAT_MOODLE content to FORMAT_HTML. + + // Because this question type was updated later than the core types, + // the available/relevant version dates make it hard to differentiate + // early 2.0 installs from 1.9 updates, hence the extra check for + // the presence of oldquestiontextformat. + $table = new xmldb_table('question'); + $field = new xmldb_field('oldquestiontextformat'); + if ($dbman->field_exists($table, $field)) { + $rs = $DB->get_recordset_sql(' + SELECT qm.*, q.oldquestiontextformat + FROM {question_multichoiceset} qm + JOIN {question} q ON qm.question = q.id'); + foreach ($rs as $record) { + if ($CFG->texteditors !== 'textarea' && $record->oldquestiontextformat == FORMAT_MOODLE) { + $record->correctfeedback = text_to_html($record->correctfeedback, false, false, true); + $record->correctfeedbackformat = FORMAT_HTML; + $record->incorrectfeedback = text_to_html($record->incorrectfeedback, false, false, true); + $record->incorrectfeedbackformat = FORMAT_HTML; + } else { + $record->correctfeedbackformat = $record->oldquestiontextformat; + $record->incorrectfeedbackformat = $record->oldquestiontextformat; + } + $DB->update_record('question_multichoiceset', $record); + } + $rs->close(); + } + // Record that qtype_multichoiceset savepoint reached. + upgrade_plugin_savepoint(true, 2011010400, 'qtype', 'multichoiceset'); + } + + // Add new shownumcorrect field. If this is true, then when the user gets a + // multiple-response question partially correct, tell them how many choices + // they got correct alongside the feedback. + if ($oldversion < 2011011200) { + + // Define field shownumcorrect to be added to question_multichoiceset. + $table = new xmldb_table('question_multichoiceset'); + $field = new xmldb_field('shownumcorrect', XMLDB_TYPE_INTEGER, '2', null, + XMLDB_NOTNULL, null, '0', 'answernumbering'); + + // Launch add field shownumcorrect. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Record that qtype_multichoiceset savepoint reached. + upgrade_plugin_savepoint(true, 2011011200, 'qtype', 'multichoiceset'); + } + + // Moodle v2.1.0 release upgrade line + // Put any upgrade step following this. + + // Moodle v2.2.0 release upgrade line + // Put any upgrade step following this. + + // Moodle v2.3.0 release upgrade line + // Put any upgrade step following this. + + // Moodle v2.4.0 release upgrade line + // Put any upgrade step following this. + + // Moodle v2.5.0 release upgrade line. + // Put any upgrade step following this. + + // Moodle v2.6.0 release upgrade line. + // Put any upgrade step following this. + + if ($oldversion < 2013110500) { + // Find duplicate rows before they break the2013110504 step below. + $problemids = $DB->get_recordset_sql(" + SELECT question, MIN(id) AS recordidtokeep + FROM {question_multichoiceset} + GROUP BY question + HAVING COUNT(1) > 1 + "); + foreach ($problemids as $problem) { + $DB->delete_records_select('question_multichoiceset', + 'question = ? AND id > ?', + array($problem->question, $problem->recordidtokeep)); + } + $problemids->close(); + + // Record that qtype_multichoiceset savepoint reached. + upgrade_plugin_savepoint(true, 2013110500, 'qtype', 'multichoiceset'); + } + + if ($oldversion < 2013110501) { + + // Define table question_multichoiceset to be renamed to qtype_multichoiceset_options. + $table = new xmldb_table('question_multichoiceset'); + + // Launch rename table for question_multichoiceset. + if ($dbman->table_exists($table)) { + $dbman->rename_table($table, 'qtype_multichoiceset_options'); + } + + // Record that qtype_multichoiceset savepoint was reached. + upgrade_plugin_savepoint(true, 2013110501, 'qtype', 'multichoiceset'); + } + + if ($oldversion < 2013110502) { + + // Define key question (foreign) to be dropped form qtype_multichoiceset_options. + $table = new xmldb_table('qtype_multichoiceset_options'); + $key = new xmldb_key('question', XMLDB_KEY_FOREIGN, array('question'), 'question', array('id')); + + // Launch drop key question. + $dbman->drop_key($table, $key); + + // Record that qtype_multichoiceset savepoint was reached. + upgrade_plugin_savepoint(true, 2013110502, 'qtype', 'multichoiceset'); + } + + if ($oldversion < 2013110503) { + + // Rename field question on table qtype_multichoiceset_options to questionid. + $table = new xmldb_table('qtype_multichoiceset_options'); + $field = new xmldb_field('question', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'id'); + + // Launch rename field question. + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'questionid'); + } + + // Record that qtype_multichoiceset savepoint was reached. + upgrade_plugin_savepoint(true, 2013110503, 'qtype', 'multichoiceset'); + } + + if ($oldversion < 2013110504) { + + // Define key questionid (foreign-unique) to be added to qtype_multichoiceset_options. + $table = new xmldb_table('qtype_multichoiceset_options'); + $key = new xmldb_key('questionid', XMLDB_KEY_FOREIGN_UNIQUE, array('questionid'), 'question', array('id')); + + // Launch add key questionid. + $dbman->add_key($table, $key); + + // Record that qtype_multichoiceset savepoint was reached. + upgrade_plugin_savepoint(true, 2013110504, 'qtype', 'multichoiceset'); + } + + if ($oldversion < 2013110505) { + + // Define field answers to be dropped from qtype_multichoiceset_options. + $table = new xmldb_table('qtype_multichoiceset_options'); + $field = new xmldb_field('answers'); + + // Conditionally launch drop field answers. + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + // Record that qtype_multichoiceset savepoint was reached. + upgrade_plugin_savepoint(true, 2013110505, 'qtype', 'multichoiceset'); + } + + if ($oldversion < 2015040100) { + + // Fix wrong component for combined feedback files. + $params = array('component' => 'qtype_multichoiceset' + , 'filearea1' => 'correctfeedback', 'filearea2' => 'incorrectfeedback'); + $sql = "component = :component AND (filearea = :filearea1 OR filearea = :filearea2)"; + $DB->set_field_select('files', 'component', 'question', $sql, $params); + + // Record that qtype_multichoiceset savepoint was reached. + upgrade_plugin_savepoint(true, 2015040100, 'qtype', 'multichoiceset'); + } + + // Moodle v2.8.0 release upgrade line. + // Put any upgrade step following this. + + // Moodle v2.9.0 release upgrade line. + // Put any upgrade step following this. + + // Moodle v3.0.0 release upgrade line. + // Put any upgrade step following this. + + // Moodle v3.1.0 release upgrade line. + // Put any upgrade step following this. + + // Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + // Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this. + + // Moodle v3.4.0 release upgrade line. + // Put any upgrade step following this. + + // Moodle v3.5.0 release upgrade line. + // Put any upgrade step following this. + + // Moodle v3.6.0 release upgrade line. + // Put any upgrade step following this. + + // Moodle v3.7.0 release upgrade line. + // Put any upgrade step following this. + + // Moodle v3.8.0 release upgrade line. + // Put any upgrade step following this. + + // Add a new checkbox for the question author to decide + // Whether standard instruction ('Select one or more:') is displayed. + $dbman = $DB->get_manager(); + $newversion = 2020111700; + if ($oldversion < $newversion) { + + // Define field showstandardinstruction to be added to qtype_multichoiceset_options. + $table = new xmldb_table('qtype_multichoiceset_options'); + $field = new xmldb_field('showstandardinstruction', XMLDB_TYPE_INTEGER, '2', + null, XMLDB_NOTNULL, null, '1', 'shownumcorrect'); + + // Conditionally launch add field showstandardinstruction. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Multichoice savepoint reached. + upgrade_plugin_savepoint(true, $newversion, 'qtype', 'multichoiceset'); + } + // Automatically generated Moodle v3.9.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} + + diff --git a/question/type/multichoiceset/db/upgradelib.php b/question/type/multichoiceset/db/upgradelib.php new file mode 100644 index 00000000..fac6fcea --- /dev/null +++ b/question/type/multichoiceset/db/upgradelib.php @@ -0,0 +1,164 @@ +. + +/** + * Upgrade library code for the multichoiceset question type. + * + * @package qtype_multichoiceset + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class to convert multichoiceset question attempt data when upgrading to the new question engine. + * + * This class is used by the code in question/engine/upgrade/upgradelib.php. + * + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoiceset_qe2_attempt_updater extends question_qtype_attempt_updater { + /** @var array variable name => value */ + protected $order; + + /** + * Return if the answer is blank (no responses selected) + * + * @param stdObject $state the question state + * @return boolean + */ + public function is_blank_answer($state) { + // Blank multichoiceset answers are not empty strings, they rather end in a colon. + return empty($state->answer) || substr($state->answer, -1) == ':'; + } + + /** + * Return the right answer + * + * @return string + */ + public function right_answer() { + $rightbits = array(); + foreach ($this->question->options->answers as $ans) { + if ($ans->fraction >= 0.000001) { + $rightbits[] = $this->to_text($ans->answer); + } + } + return implode('; ', $rightbits); + } + + /** + * Expand the answer response + * + * @param string $answer + * @return string + */ + protected function explode_answer($answer) { + if (strpos($answer, ':') !== false) { + list($order, $responses) = explode(':', $answer); + return $responses; + } else { + // Sometimes, a bug means that a state is missing the : bit, + // We need to deal with that. + $this->logger->log_assumption("Dealing with missing order information + in attempt at multiple choice question {$this->question->id}"); + return $answer; + } + } + + /** + * Set the data elements for the question step + * + * @param stdObject $state the question state + * @return array question response summary + */ + public function response_summary($state) { + $responses = $this->explode_answer($state->answer); + if (!empty($responses)) { + $responses = explode(',', $responses); + $bits = array(); + foreach ($responses as $response) { + if (array_key_exists($response, $this->question->options->answers)) { + $bits[] = $this->to_text( + $this->question->options->answers[$response]->answer); + } else { + $this->logger->log_assumption("Dealing with a place where the + student selected a choice that was later deleted for + multiple choice question {$this->question->id}"); + $bits[] = '[CHOICE THAT WAS LATER DELETED]'; + } + } + return implode('; ', $bits); + } else { + return null; + } + } + + /** + * Test if the question response was selected + * + * @param stdObject $state the question state + * @return boolean + */ + public function was_answered($state) { + $responses = $this->explode_answer($state->answer); + return !empty($responses); + } + + /** + * Set the data elements for the first question step + * + * @param stdObject $state the question state + * @param array $data the question data + */ + public function set_first_step_data_elements($state, &$data) { + if (!$state->answer) { + return; + } + list($order, $responses) = explode(':', $state->answer); + $data['_order'] = $order; + $this->order = explode(',', $order); + } + + /** + * Supply any missing first step data + * + * @param array $data the question data + */ + public function supply_missing_first_step_data(&$data) { + $data['_order'] = implode(',', array_keys($this->question->options->answers)); + } + + /** + * Set the data elements for the question step + * + * @param stdObject $state the question state + * @param array $data the question data + */ + public function set_data_elements_for_step($state, &$data) { + $responses = $this->explode_answer($state->answer); + $responses = explode(',', $responses); + foreach ($this->order as $key => $ansid) { + if (in_array($ansid, $responses)) { + $data['choice' . $key] = 1; + } else { + $data['choice' . $key] = 0; + } + } + } +} diff --git a/question/type/multichoiceset/edit_multichoiceset_form.php b/question/type/multichoiceset/edit_multichoiceset_form.php new file mode 100644 index 00000000..ba5f4419 --- /dev/null +++ b/question/type/multichoiceset/edit_multichoiceset_form.php @@ -0,0 +1,223 @@ +. + +/** + * Defines the editing form for the multichoiceset question type. + * + * @package qtype_multichoiceset + * @copyright 2007 Jamie Pratt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Multiple choice all or nothing editing form definition. + * + * @copyright 2007 Jamie Pratt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoiceset_edit_form extends question_edit_form { + /** + * Add question-type specific form fields. + * + * @param object $mform the form being built. + */ + protected function definition_inner($mform) { + $mform->addElement('advcheckbox', 'shuffleanswers', + get_string('shuffleanswers', 'qtype_multichoice'), null, null, array(0, 1)); + $mform->addHelpButton('shuffleanswers', 'shuffleanswers', 'qtype_multichoice'); + $mform->setDefault('shuffleanswers', 1); + + $mform->addElement('select', 'answernumbering', + get_string('answernumbering', 'qtype_multichoice'), + qtype_multichoice::get_numbering_styles()); + $mform->setDefault('answernumbering', get_config('qtype_multichoiceset', 'answernumbering')); + + $mform->addElement('selectyesno', 'showstandardinstruction', + get_string('showstandardinstruction', 'qtype_multichoiceset'), null, null, [0, 1]); + $mform->addHelpButton('showstandardinstruction', 'showstandardinstruction', 'qtype_multichoiceset'); + $mform->setDefault('showstandardinstruction', 0); + + $this->add_per_answer_fields($mform, get_string('choiceno', 'qtype_multichoice', '{no}'), + null, max(5, QUESTION_NUMANS_START)); + + $mform->addElement('header', 'overallfeedbackhdr', get_string('combinedfeedback', 'question')); + + foreach (array('correctfeedback', 'incorrectfeedback') as $feedbackname) { + $element = $mform->addElement('editor', $feedbackname, get_string($feedbackname, 'question'), + array('rows' => 5), $this->editoroptions); + $mform->setType($feedbackname, PARAM_RAW); + $element->setValue(array('text' => get_string($feedbackname.'default', 'question'))); + + if ($feedbackname == 'incorrectfeedback') { + $mform->addElement('advcheckbox', 'shownumcorrect', + get_string('options', 'question'), + get_string('shownumpartscorrect', 'question')); + } + } + + $this->add_interactive_settings(true, true); + } + + /** + * Get the list of form elements to repeat, one for each answer. + * + * @param object $mform the form being built. + * @param string $label the label to use for each option. + * @param array $gradeoptions the possible grades for each answer. + * @param array $repeatedoptions reference to array of repeated options to fill + * @param string $answersoption reference to return the name of $question->options + * field holding an array of answers + * @return array of form fields. + */ + protected function get_per_answer_fields($mform, $label, $gradeoptions, + &$repeatedoptions, &$answersoption) { + $repeated = array(); + $repeated[] = $mform->createElement('editor', 'answer', + $label, array('rows' => 1), $this->editoroptions); + $repeated[] = $mform->createElement('checkbox', 'correctanswer', + get_string('correctanswer', 'qtype_multichoiceset')); + $repeated[] = $mform->createElement('editor', 'feedback', + get_string('feedback', 'question'), array('rows' => 1), $this->editoroptions); + + // These are returned by arguments passed by reference. + $repeatedoptions['answer']['type'] = PARAM_RAW; + $answersoption = 'answers'; + + return $repeated; + } + + /** + * Create the form elements required by one hint. + * @param bool $withclearwrong whether this question type uses the 'Clear wrong' option on hints. + * @param bool $withshownumpartscorrect whether this quesiton type uses the 'Show num parts correct' option on hints. + * @return array form field elements for one hint. + */ + protected function get_hint_fields($withclearwrong = false, $withshownumpartscorrect = false) { + list($repeated, $repeatedoptions) = parent::get_hint_fields( + $withclearwrong, $withshownumpartscorrect); + $repeated[] = $this->_form->createElement('advcheckbox', 'hintshowchoicefeedback', '', + get_string('showeachanswerfeedback', 'qtype_multichoiceset')); + return array($repeated, $repeatedoptions); + } + + /** + * Perform any preprocessing needed on the data passed to {@link set_data()} + * before it is used to initialise the form. + * @param object $question the data being passed to the form. + * @return object $question the modified data. + */ + protected function data_preprocessing($question) { + $question = parent::data_preprocessing($question); + $question = $this->data_preprocessing_answers($question, true); + $question = $this->data_preprocessing_hints($question, true, true); + + if (!empty($question->options->answers)) { + $key = 0; + foreach ($question->options->answers as $answer) { + $question->correctanswer[$key] = $answer->fraction > 0; + $key++; + } + } + + if (!empty($question->hints)) { + $key = 0; + foreach ($question->hints as $hint) { + $question->hintshowchoicefeedback[$key] = !empty($hint->options); + $key += 1; + } + } + + if (!empty($question->options)) { + $question->shuffleanswers = $question->options->shuffleanswers; + $question->answernumbering = $question->options->answernumbering; + $question->shownumcorrect = $question->options->shownumcorrect; + $question->showstandardinstruction = $question->options->showstandardinstruction; + // Prepare feedback editor to display files in draft area. + foreach (array('correctfeedback', 'incorrectfeedback') as $feedbackname) { + $draftid = file_get_submitted_draft_itemid($feedbackname); + $text = $question->options->$feedbackname; + $feedbackformat = $feedbackname . 'format'; + $format = $question->options->$feedbackformat; + $defaultvalues[$feedbackname] = array(); + $defaultvalues[$feedbackname]['text'] = file_prepare_draft_area( + $draftid, + $this->context->id, + 'question', + $feedbackname, + !empty($question->id) ? (int)$question->id : null, + $this->fileoptions, + $text + ); + $defaultvalues[$feedbackname]['format'] = $format; + $defaultvalues[$feedbackname]['itemid'] = $draftid; + } + // Prepare files code block ends. + + $question = (object)((array)$question + $defaultvalues); + } + return $question; + } + + /** + * Perform any validation needed + * @param object $data the data being returned by the form. + * @param array $files any files being returned by the form. + * @return array any errors in the form + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + + $answers = $data['answer']; + $answercount = 0; + $numberofcorrectanswers = 0; + foreach ($answers as $key => $answer) { + $trimmedanswer = trim($answer['text']); + if (empty($trimmedanswer)) { + continue; + } + + $answercount++; + if (!empty($data['correctanswer'][$key])) { + $numberofcorrectanswers++; + } + } + + // Perform sanity checks on number of correct answers. + if ($numberofcorrectanswers == 0) { + $errors['answer[0]'] = get_string('errnocorrect', 'qtype_multichoiceset'); + } + + // Perform sanity checks on number of answers. + if ($answercount == 0) { + $errors['answer[0]'] = get_string('notenoughanswers', 'qtype_multichoice', 2); + $errors['answer[1]'] = get_string('notenoughanswers', 'qtype_multichoice', 2); + } else if ($answercount == 1) { + $errors['answer[1]'] = get_string('notenoughanswers', 'qtype_multichoice', 2); + } + + return $errors; + } + + /** + * Return the question type name. + * @return string the question type name + */ + public function qtype() { + return 'multichoiceset'; + } +} diff --git a/question/type/multichoiceset/lang/en/qtype_multichoiceset.php b/question/type/multichoiceset/lang/en/qtype_multichoiceset.php new file mode 100644 index 00000000..c90c907c --- /dev/null +++ b/question/type/multichoiceset/lang/en/qtype_multichoiceset.php @@ -0,0 +1,37 @@ +. + +/** + * Strings for component 'qtype_multichoiceset', language 'en', branch 'MOODLE_20_STABLE' + * + * @package qtype_multichoiceset + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['correctanswer'] = 'Correct'; +$string['deletedchoice'] = 'This choice was deleted after the attempt was started.'; +$string['errnocorrect'] = 'At least one of the choices should be correct so that it is possible to get a full grade for this question.'; +$string['pluginname'] = 'All-or-Nothing Multiple Choice'; +$string['pluginname_help'] = 'After an optional introduction, the respondent can choose one or more answers. If the chosen answers correspond exactly to the "Correct" choices defined in the question, the respondent gets 100%. If he/she chooses any "Incorrect" choices or does not select all of the "Correct" choices, the grade is 0%. At least one choice must be marked correct for each question. Add a "None of the above" option to handle a question where none of the other choices are correct. Unlike the multiple choice question with fractional grades, the only possible grades for an all-or-nothing question are 100% or 0%'; +$string['pluginname_link'] = 'question/type/multichoiceset'; +$string['pluginnameadding'] = 'Adding an All-or-Nothing Multiple Choice Question'; +$string['pluginnameediting'] = 'Editing an All-or-Nothing Multiple Choice Question'; +$string['pluginnamesummary'] = 'Allows the selection of multiple responses from a pre-defined list and uses all-or-nothing grading (100% or 0%).'; +$string['privacy:metadata'] = 'The All-or-Nothing Multiple Choice plugin does not store any personal data.'; +$string['showeachanswerfeedback'] = 'Show the feedback for the selected responses.'; +$string['showstandardinstruction'] = 'Show standard instructions'; +$string['showstandardinstruction_help'] = 'Whether to show the instructions "Select one or more:" before All-or-Nothing multiple choice answers.'; diff --git a/question/type/multichoiceset/lang/es/qtype_multichoiceset.php b/question/type/multichoiceset/lang/es/qtype_multichoiceset.php new file mode 100644 index 00000000..5018b069 --- /dev/null +++ b/question/type/multichoiceset/lang/es/qtype_multichoiceset.php @@ -0,0 +1,36 @@ +. + +/** + * Strings for component 'qtype_multichoiceset', language 'es', branch 'MOODLE_20_STABLE' + * + * @package qtype_multichoiceset + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['correctanswer'] = 'Correcto'; +$string['deletedchoice'] = 'Esta opción se eliminó después de que se inició el intento.'; +$string['errnocorrect'] = 'Al menos una de las opciones debe ser correcta para que sea posible obtener una calificación completa para esta pregunta.'; +$string['pluginname'] = 'Opción múltiple de todo o nada'; +$string['pluginname_help'] = 'Después de una introducción opcional, el encuestado puede elegir una o más respuestas. Si las respuestas elegidas corresponden exactamente a las opciones "correctas" definidas en la pregunta, el encuestado obtiene el 100%. Si elige alguna opción "incorrecta" o no selecciona todas las opciones "correctas", la calificación es 0%. Al menos una opción debe estar marcada como correcta para cada pregunta. Agregue una opción "Ninguna de las anteriores" para manejar una pregunta donde ninguna de las otras opciones sea correcta. A diferencia de la pregunta de opción múltiple con calificaciones fraccionarias, las únicas calificaciones posibles para una pregunta de todo o nada son 100% o 0% '; +$string['pluginname_link'] = 'question/type/multichoiceset'; +$string['pluginnameadding'] = 'Agregar una pregunta de opción múltiple de todo o nada'; +$string['pluginnameediting'] = 'Editando una pregunta de opción múltiple de todo o nada'; +$string['pluginnamesummary'] = 'Permite la selección de respuestas múltiples de una lista predefinida y usa la calificación de todo o nada (100% o 0%).'; +$string['privacy:metadata'] = 'The All-or-Nothing Multiple Choice plugin does not store any personal data.'; +$string['showeachanswerfeedback'] = 'Mostrar los comentarios de las respuestas seleccionadas.'; +$string['showstandardinstruction_help'] = 'Si es que se muestran o no las instrucciones "Seleccione una o más:" antes de las respuestas de opción múltiple de todo o nada.'; diff --git a/question/type/multichoiceset/lib.php b/question/type/multichoiceset/lib.php new file mode 100644 index 00000000..174b91ab --- /dev/null +++ b/question/type/multichoiceset/lib.php @@ -0,0 +1,49 @@ +. + +/** + * Serve question type files + * + * @since 2.0 + * @package qtype_multichoiceset + * @copyright Dongsheng Cai + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Checks file access for All or Nothing multiple choice questions. + * + * Extract the WordProcessingML XML files from the .docx file, and use a sequence of XSLT + * steps to convert it into Moodle Question XML + * + * @param stdClass $course course settings object + * @param object $cm the course module object representing the activity + * @param stdClass $context context object + * @param string $filearea the name of the file area. + * @param array $args the remaining bits of the file path. + * @param bool $forcedownload whether the user must be forced to download the file. + * @param array $options additional options affecting the file serving + * @return bool Success + */ +function qtype_multichoiceset_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { + global $CFG; + require_once($CFG->libdir . '/questionlib.php'); + question_pluginfile($course, $context, 'qtype_multichoiceset', $filearea, $args, $forcedownload, $options); +} diff --git a/question/type/multichoiceset/mobile/multichoiceset.html b/question/type/multichoiceset/mobile/multichoiceset.html new file mode 100644 index 00000000..a90fdf24 --- /dev/null +++ b/question/type/multichoiceset/mobile/multichoiceset.html @@ -0,0 +1,24 @@ +
+ +

+ +

+

+ +

+
+ + + + +

+ +

+
+ + + +
+
+
diff --git a/question/type/multichoiceset/mobile/multichoiceset.js b/question/type/multichoiceset/mobile/multichoiceset.js new file mode 100644 index 00000000..a3a57ef1 --- /dev/null +++ b/question/type/multichoiceset/mobile/multichoiceset.js @@ -0,0 +1,61 @@ +var that = this; +var result = { + + componentInit: function() { + + // This.question should be provided to us here. + // This.question.html (string) is the main source of data, presumably prepared by the renderer. + // There are also other useful objects with question like infoHtml which is used by the + // page to display the question state, but with which we need do nothing. + // This code just prepares bits of this.question.html storing it in the question object ready for + // passing to the template (multichoiceset.html). + // Note this is written in 'standard' javascript rather than ES6. Both work. + + if (!this.question) { + return that.CoreQuestionHelperProvider.showComponentError(that.onAbort); + } + + // Create a temporary div to ease extraction of parts of the provided html. + var div = document.createElement('div'); + div.innerHTML = this.question.html; + + // Replace Moodle's correct/incorrect classes, feedback and icons with mobile versions. + that.CoreQuestionHelperProvider.replaceCorrectnessClasses(div); + that.CoreQuestionHelperProvider.replaceFeedbackClasses(div); + that.CoreQuestionHelperProvider.treatCorrectnessIcons(div); + + // Get useful parts of the provided question html data. + var questiontext = div.querySelector('.qtext'); + var prompt = div.querySelector('.prompt'); + var answeroptions = div.querySelector('.answer'); + + // Add the useful parts back into the question object ready for rendering in the template. + this.question.text = questiontext.innerHTML; + // Without the question text there is no point in proceeding. + if (typeof this.question.text === 'undefined') { + return that.CoreQuestionHelperProvider.showComponentError(that.onAbort); + } + if (prompt !== null) { + this.question.prompt = prompt.innerHTML; + } + + var options = []; + var divs = answeroptions.querySelectorAll('div[class^=r]'); // Only get the answer options divs (class="r0..."). + divs.forEach(function(d, i) { + // Each answer option contains all the data for presentation, it just needs extracting. + var label = d.querySelector('label').innerHTML; + var name = d.querySelector('label').getAttribute('for'); + var checked = (d.querySelector('input[type=checkbox]').getAttribute('checked') ? true : false); + var disabled = (d.querySelector('input').getAttribute('disabled') === 'disabled' ? true : false); + var feedback = (d.querySelector('div') ? d.querySelector('div').innerHTML : ''); + var qclass = d.getAttribute('class'); + options.push({text: label, name: name, checked: checked, disabled: disabled, feedback: feedback, qclass: qclass}); + }); + this.question.options = options; + + return true; + } +}; + +// This next line is required as is (because of an eval step that puts this result object into the global scope). +result; \ No newline at end of file diff --git a/question/type/multichoiceset/phpunit.xml b/question/type/multichoiceset/phpunit.xml new file mode 100644 index 00000000..5f850099 --- /dev/null +++ b/question/type/multichoiceset/phpunit.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + . + + + + diff --git a/question/type/multichoiceset/pix/icon.gif b/question/type/multichoiceset/pix/icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..9b96e216ddfdd2c5e2408c78213186cfd0319ccb GIT binary patch literal 204 zcmZ?wbhEHb6krfwXpv>m(9rns;R6E$gSxu<$B!SatgI$ap8W9PLveBOg$oxdDJlK^ z`&URv$k5Pm_3G7TW@aTNCAzx0pFVx!+|z#z|{1GEBYqW}W~tLFptz7$KV^9eK36gfDe&EC80`!Pd8-A)YF E0Hg + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/question/type/multichoiceset/question.php b/question/type/multichoiceset/question.php new file mode 100644 index 00000000..9de4d28a --- /dev/null +++ b/question/type/multichoiceset/question.php @@ -0,0 +1,73 @@ +. + +/** + * Multiple choice question definition classes. + * + * @package qtype_multichoiceset + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/question/type/multichoice/question.php'); + +/** + * Represents an all or nothing multiple response question. + * + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoiceset_question extends qtype_multichoice_multi_question { + + /** + * Get the grade + * + * Calculate the grade based on the users response + * + * @param array $response responses, as returned by + * {@link question_attempt_step::get_qt_data()}. + * @return array fraction and state + */ + public function grade_response(array $response) { + $fraction = 0; + list($numright, $total) = $this->get_num_parts_right($response); + $numwrong = $this->get_num_selected_choices($response) - $numright; + $numcorrect = $this->get_num_correct_choices(); + if ($numwrong == 0 && $numcorrect == $numright) { + $fraction = 1; + } + + $state = question_state::graded_state_for_fraction($fraction); + + return array($fraction, $state); + } + + /** + * Disable hint settings if too many choices selected + * + * Disable those hint settings that we don't want when the student has selected + * more choices than the number of right choices. This avoids giving the game away. + * @param question_hint_with_parts $hint a hint. + */ + protected function disable_hint_settings_when_too_many_selected( + question_hint_with_parts $hint) { + parent::disable_hint_settings_when_too_many_selected($hint); + $hint->showchoicefeedback = false; + } +} diff --git a/question/type/multichoiceset/questiontype.php b/question/type/multichoiceset/questiontype.php new file mode 100644 index 00000000..f919c407 --- /dev/null +++ b/question/type/multichoiceset/questiontype.php @@ -0,0 +1,689 @@ +. + +/** + * The questiontype class for the multiple choice question type. + * + * @package qtype_multichoiceset + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/question/engine/lib.php'); +require_once($CFG->dirroot . '/question/type/multichoice/questiontype.php'); +require_once($CFG->dirroot . '/question/format/xml/format.php'); + +/** + * The multiple choice all or nothing question type. + * + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoiceset extends question_type { + /** + * Can the answer fields contain HTML? + * + * @return bool return true if restore_decode_content_links_worker should be called. + */ + public function has_html_answers() { + return true; + } + + /** + * Get the question options. + * + * @param stdObject $question the question + * @return void + */ + public function get_question_options($question) { + global $DB, $OUTPUT; + $question->options = $DB->get_record('qtype_multichoiceset_options', + array('questionid' => $question->id), '*', MUST_EXIST); + if ($question->options === false) { + // If this has happened, then we have a problem. + // For the user to be able to edit or delete this question, we need options. + debugging("Question ID {$question->id} was missing an options record. Using default.", DEBUG_DEVELOPER); + + $question->options = $this->create_default_options($question); + } + + parent::get_question_options($question); + } + + /** + * Create a default options object for the provided question. + * + * @param object $question The queston we are working with. + * @return object The options object. + */ + protected function create_default_options($question) { + // Create a default question options record. + $options = new stdClass(); + $options->questionid = $question->id; + + // Get the default strings and just set the format. + $options->correctfeedback = get_string('correctfeedbackdefault', 'question'); + $options->correctfeedbackformat = FORMAT_HTML; + $options->partiallycorrectfeedback = get_string('partiallycorrectfeedbackdefault', 'question');; + $options->partiallycorrectfeedbackformat = FORMAT_HTML; + $options->incorrectfeedback = get_string('incorrectfeedbackdefault', 'question'); + $options->incorrectfeedbackformat = FORMAT_HTML; + + $config = get_config('qtype_multichoiceset'); + $options->single = $config->answerhowmany; + if (isset($question->layout)) { + $options->layout = $question->layout; + } + $options->answernumbering = $config->answernumbering; + $options->shuffleanswers = $config->shuffleanswers; + $options->showstandardinstruction = 0; + $options->shownumcorrect = 1; + + return $options; + } + + /** + * Save the question options. + * + * @param stdObject $question the question + * @return stdObject error if there aren't at least 2 answers + */ + public function save_question_options($question) { + global $DB; + $context = $question->context; + $result = new stdClass(); + + $oldanswers = $DB->get_records('question_answers', + array('question' => $question->id), 'id ASC'); + + // Following hack to check at least two answers exist. + $answercount = 0; + foreach ($question->answer as $key => $answer) { + if ($answer != '') { + $answercount++; + } + } + if ($answercount < 2) { // Check there are at lest 2 answers for multiple choice. + $result->notice = get_string('notenoughanswers', 'qtype_multichoice', '2'); + return $result; + } + + // Insert all the new answers. + $totalfraction = 0; + $maxfraction = -1; + foreach ($question->answer as $key => $answerdata) { + if (trim($answerdata['text']) == '') { + continue; + } + + // Update an existing answer if possible. + $answer = array_shift($oldanswers); + if (!$answer) { + $answer = new stdClass(); + $answer->question = $question->id; + $answer->answer = ''; + $answer->feedback = ''; + $answer->id = $DB->insert_record('question_answers', $answer); + } + + if (is_array($answerdata)) { + // Doing an import. + $answer->answer = $this->import_or_save_files($answerdata, + $context, 'question', 'answer', $answer->id); + $answer->answerformat = $answerdata['format']; + } else { + // Saving the form. + $answer->answer = $answerdata; + $answer->answerformat = FORMAT_HTML; + } + $answer->fraction = !empty($question->correctanswer[$key]); + $answer->feedback = $this->import_or_save_files($question->feedback[$key], + $context, 'question', 'answerfeedback', $answer->id); + $answer->feedbackformat = $question->feedback[$key]['format']; + + $DB->update_record('question_answers', $answer); + } + + // Delete any left over old answer records. + $fs = get_file_storage(); + foreach ($oldanswers as $oldanswer) { + $fs->delete_area_files($context->id, 'question', 'answerfeedback', $oldanswer->id); + $DB->delete_records('question_answers', array('id' => $oldanswer->id)); + } + + $options = $DB->get_record('qtype_multichoiceset_options', array('questionid' => $question->id)); + if (!$options) { + $options = new stdClass(); + $options->questionid = $question->id; + $options->correctfeedback = ''; + $options->incorrectfeedback = ''; + $options->showstandardinstruction = 0; + $options->id = $DB->insert_record('qtype_multichoiceset_options', $options); + } + + if (isset($question->layout)) { + $options->layout = $question->layout; + } + $options->answernumbering = $question->answernumbering; + $options->shuffleanswers = $question->shuffleanswers; + $options->showstandardinstruction = !empty($question->showstandardinstruction); + $options->correctfeedback = $this->import_or_save_files($question->correctfeedback, + $context, 'question', 'correctfeedback', $question->id); + $options->correctfeedbackformat = $question->correctfeedback['format']; + $options->incorrectfeedback = $this->import_or_save_files($question->incorrectfeedback, + $context, 'question', 'incorrectfeedback', $question->id); + $options->incorrectfeedbackformat = $question->incorrectfeedback['format']; + $options->shownumcorrect = !empty($question->shownumcorrect); + + $DB->update_record('qtype_multichoiceset_options', $options); + $this->save_hints($question, true); + } + + /** + * Save all hints. + * + * @param stdObject $formdata form data of question + * @param bool $withparts whether the question has parts + * @return stdObject + */ + public function save_hints($formdata, $withparts = false) { + global $DB; + $context = $formdata->context; + + $oldhints = $DB->get_records('question_hints', + array('questionid' => $formdata->id), 'id ASC'); + + if (!empty($formdata->hint)) { + $numhints = max(array_keys($formdata->hint)) + 1; + } else { + $numhints = 0; + } + + if ($withparts) { + if (!empty($formdata->hintclearwrong)) { + $numclears = max(array_keys($formdata->hintclearwrong)) + 1; + } else { + $numclears = 0; + } + if (!empty($formdata->hintshownumcorrect)) { + $numshows = max(array_keys($formdata->hintshownumcorrect)) + 1; + } else { + $numshows = 0; + } + $numhints = max($numhints, $numclears, $numshows); + } + + if (!empty($formdata->hintshowchoicefeedback)) { + $numshowfeedbacks = max(array_keys($formdata->hintshowchoicefeedback)) + 1; + } else { + $numshowfeedbacks = 0; + } + $numhints = max($numhints, $numshowfeedbacks); + + for ($i = 0; $i < $numhints; $i += 1) { + if (html_is_blank($formdata->hint[$i]['text'])) { + $formdata->hint[$i]['text'] = ''; + } + + if ($withparts) { + $clearwrong = !empty($formdata->hintclearwrong[$i]); + $shownumcorrect = !empty($formdata->hintshownumcorrect[$i]); + } + + $showchoicefeedback = !empty($formdata->hintshowchoicefeedback[$i]); + + if (empty($formdata->hint[$i]['text']) && empty($clearwrong) && + empty($shownumcorrect) && empty($showchoicefeedback)) { + continue; + } + + // Update an existing hint if possible. + $hint = array_shift($oldhints); + if (!$hint) { + $hint = new stdClass(); + $hint->questionid = $formdata->id; + $hint->hint = ''; + $hint->id = $DB->insert_record('question_hints', $hint); + } + + $hint->hint = $this->import_or_save_files($formdata->hint[$i], + $context, 'question', 'hint', $hint->id); + $hint->hintformat = $formdata->hint[$i]['format']; + if ($withparts) { + $hint->clearwrong = $clearwrong; + $hint->shownumcorrect = $shownumcorrect; + } + $hint->options = $showchoicefeedback; + $DB->update_record('question_hints', $hint); + } + + // Delete any remaining old hints. + $fs = get_file_storage(); + foreach ($oldhints as $oldhint) { + $fs->delete_area_files($context->id, 'question', 'hint', $oldhint->id); + $DB->delete_records('question_hints', array('id' => $oldhint->id)); + } + } + + /** + * Make a hint object. + * + * @param stdObject $hint a hint + * @return stdObject + */ + protected function make_hint($hint) { + return qtype_multichoiceset_hint::load_from_record($hint); + } + + /** + * Make a question instance. + * + * @param stdObject $questiondata the question data + * @return stdObject + */ + protected function make_question_instance($questiondata) { + question_bank::load_question_definition_classes($this->name()); + $class = 'qtype_multichoiceset_question'; + return new $class(); + } + + /** + * Initialise the question instance. + * + * @param question_definition $question the question_definition we are creating + * @param stdObject $questiondata the question data + * @return void + */ + protected function initialise_question_instance(question_definition $question, $questiondata) { + parent::initialise_question_instance($question, $questiondata); + $question->shuffleanswers = $questiondata->options->shuffleanswers; + $question->answernumbering = $questiondata->options->answernumbering; + $question->showstandardinstruction = $questiondata->options->showstandardinstruction; + if (!empty($questiondata->options->layout)) { + $question->layout = $questiondata->options->layout; + } else { + $question->layout = qtype_multichoice_single_question::LAYOUT_VERTICAL; + } + $question->correctfeedback = $questiondata->options->correctfeedback; + $question->correctfeedbackformat = $questiondata->options->correctfeedbackformat; + $question->incorrectfeedback = $questiondata->options->incorrectfeedback; + $question->incorrectfeedbackformat = $questiondata->options->incorrectfeedbackformat; + $question->shownumcorrect = $questiondata->options->shownumcorrect; + + $this->initialise_question_answers($question, $questiondata, false); + } + + /** + * Make an answer. + * + * @param stdObject $answer the answer + * @return stdObject + */ + public function make_answer($answer) { + // Overridden just so we can make it public for use by question.php. + return parent::make_answer($answer); + } + + /** + * Delete the question. + * + * @param int $questionid the question ID + * @param stdObject $contextid the context ID + * @return stdObject + */ + public function delete_question($questionid, $contextid) { + global $DB; + $DB->delete_records('qtype_multichoiceset_options', array('questionid' => $questionid)); + return parent::delete_question($questionid, $contextid); + } + + /** + * Get the number of correct response choices. + * + * @param stdObject $questiondata the question data + * @return int + */ + protected function get_num_correct_choices($questiondata) { + $numright = 0; + foreach ($questiondata->options->answers as $answer) { + if (!question_state::graded_state_for_fraction($answer->fraction)->is_incorrect()) { + $numright += 1; + } + } + return $numright; + } + + /** + * Get the score if random response chosen - but not computed for this question type. + * + * @param stdObject $questiondata the question data + * @return stdObject + */ + public function get_random_guess_score($questiondata) { + // Pretty much impossible to compute for _multi questions. Don't try. + return null; + } + + /** + * Get the possible responses to the question. + * + * @param stdObject $questiondata the question data + * @return array array of question parts + */ + public function get_possible_responses($questiondata) { + $parts = array(); + + foreach ($questiondata->options->answers as $aid => $answer) { + $parts[$aid] = array($aid => new question_possible_response( + html_to_text(format_text( + $answer->answer, $answer->answerformat, array('noclean' => true)), + 0, false), $answer->fraction)); + } + + return $parts; + } + + /** + * Get the available question numbering styles. + * + * @return array of the numbering styles supported. For each one, there + * should be a lang string answernumberingxxx in the qtype_multichoice + * language file, and a case in the switch statement in number_in_style, + * and it should be listed in the definition of this column in install.xml. + */ + public static function get_numbering_styles() { + $styles = array(); + foreach (array('abc', 'ABCD', '123', 'iii', 'IIII', 'none') as $numberingoption) { + $styles[$numberingoption] + = get_string('answernumbering' . $numberingoption, 'qtype_multichoice'); + } + return $styles; + } + + /** + * Move files from old to new context. + * + * @param int $questionid the question ID + * @param stdObject $oldcontextid the source context ID + * @param stdObject $newcontextid the destination context ID + * @return void + */ + public function move_files($questionid, $oldcontextid, $newcontextid) { + $fs = get_file_storage(); + + parent::move_files($questionid, $oldcontextid, $newcontextid); + $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid, true); + + $fs->move_area_files_to_new_context($oldcontextid, + $newcontextid, 'question', 'correctfeedback', $questionid); + $fs->move_area_files_to_new_context($oldcontextid, + $newcontextid, 'question', 'incorrectfeedback', $questionid); + } + + /** + * Delete any files in the context. + * + * @param int $questionid the question ID + * @param stdObject $contextid the context ID + * @return void + */ + protected function delete_files($questionid, $contextid) { + $fs = get_file_storage(); + + parent::delete_files($questionid, $contextid); + $this->delete_files_in_answers($questionid, $contextid, true); + $fs->delete_area_files($contextid, 'question', 'correctfeedback', $questionid); + $fs->delete_area_files($contextid, 'question', 'incorrectfeedback', $questionid); + } + + + // IMPORT EXPORT FUNCTIONS. + + /** + * Provide export functionality for xml format. + * + * @param stdObject $question the question object + * @param qformat_xml $format the format object so that helper methods can be used + * @param mixed $extra any additional format specific data that may be passed by the format (see format code for info) + * @return string the data to append to the output buffer or false if error + */ + public function export_to_xml($question, qformat_xml $format, $extra=null) { + $expout = ''; + $fs = get_file_storage(); + $contextid = $question->contextid; + + $expout .= " ".$format->get_single($question->options->shuffleanswers)."\n"; + + $textformat = $format->get_format($question->options->correctfeedbackformat); + $files = $fs->get_area_files($contextid, 'question', 'correctfeedback', $question->id); + $expout .= " \n" + . ' ' . $format->writetext($question->options->correctfeedback); + $expout .= $format->write_files($files); + $expout .= " \n"; + + $textformat = $format->get_format($question->options->incorrectfeedbackformat); + $files = $fs->get_area_files($contextid, 'question', 'incorrectfeedback', $question->id); + $expout .= " \n" + . ' ' . $format->writetext($question->options->incorrectfeedback); + $expout .= $format->write_files($files); + $expout .= " \n"; + if (!empty($question->options->shownumcorrect)) { + $expout .= " \n"; + } + $expout .= " {$question->options->answernumbering}\n"; + $expout .= " {$question->options->showstandardinstruction}\n"; + $expout .= $format->write_answers($question->options->answers); + + return $expout; + } + + /** + * Provide import functionality for xml format + * + * @param mixed $data the segment of data containing the question + * @param stdObject $question question object processed (so far) by standard import code + * @param qformat_xml $format the format object so that helper methods can be used (in particular error()) + * @param mixed $extra any additional format specific data that may be passed by the format (see format code for info) + * @return stdObject question object suitable for save_options() call or false if cannot handle + */ + public function import_from_xml($data, $question, qformat_xml $format, $extra=null) { + // Check question is for us. + if (!isset($data['@']['type']) || $data['@']['type'] != 'multichoiceset') { + return false; + } + + $question = $format->import_headers($data); + $question->qtype = 'multichoiceset'; + + $question->shuffleanswers = $format->trans_single( + $format->getpath($data, array('#', 'shuffleanswers', 0, '#'), 1)); + + $question->answernumbering = $format->getpath($data, + array('#', 'answernumbering', 0, '#'), 'abc'); + + $question->showstandardinstruction = $format->trans_single( + $format->getpath($data, array('#', 'showstandardinstruction', 0, '#'), 1)); + + $question->correctfeedback = array(); + $question->correctfeedback['text'] = $format->getpath($data, array('#', 'correctfeedback', 0, '#', 'text', 0, '#'), + '', true); + $question->correctfeedback['format'] = $format->trans_format( + $format->getpath($data, array('#', 'correctfeedback', 0, '@', 'format'), + $format->get_format($question->questiontextformat))); + $question->correctfeedback['files'] = array(); + // Restore files in correctfeedback. + $files = $format->getpath($data, array('#', 'correctfeedback', 0, '#', 'file'), array(), false); + foreach ($files as $file) { + $filesdata = new stdclass; + $filesdata->content = $file['#']; + $filesdata->encoding = $file['@']['encoding']; + $filesdata->name = $file['@']['name']; + $question->correctfeedback['files'][] = $filesdata; + } + + $question->incorrectfeedback = array(); + $question->incorrectfeedback['text'] = $format->getpath($data, array('#', 'incorrectfeedback', 0, '#', 'text', 0, '#'), + '', true ); + $question->incorrectfeedback['format'] = $format->trans_format( + $format->getpath($data, array('#', 'incorrectfeedback', 0, '@', 'format'), + $format->get_format($question->questiontextformat))); + $question->incorrectfeedback['files'] = array(); + // Restore files in incorrectfeedback. + $files = $format->getpath($data, array('#', 'incorrectfeedback', 0, '#', 'file'), array(), false); + foreach ($files as $file) { + $filesdata = new stdclass; + $filesdata->content = $file['#']; + $filesdata->encoding = $file['@']['encoding']; + $filesdata->name = $file['@']['name']; + $question->incorrectfeedback['files'][] = $filesdata; + } + + $question->shownumcorrect = array_key_exists('shownumcorrect', $data['#']); + + // Run through the answers. + $answers = $data['#']['answer']; + foreach ($answers as $answer) { + $ans = $format->import_answer($answer, true, + $format->get_format($question->questiontextformat)); + $question->answer[] = $ans->answer; + + // FIX: Some tools set fraction to `0.0` + // which leeds to false `true` answers. + $question->correctanswer[] = !empty($ans->fraction) && + (float)$ans->fraction > 0; + $question->feedback[] = $ans->feedback; + + // Backwards compatibility. + if (array_key_exists('correctanswer', $answer['#'])) { + $key = end(array_keys($question->correctanswer)); + $question->correctanswer[$key] = $format->getpath($answer, + array('#', 'correctanswer', 0, '#'), 0); + } + } + + $format->import_hints($question, $data, true, true, + $format->get_format($question->questiontextformat)); + + // Get extra choicefeedback setting from each hint. + if (!empty($question->hintoptions)) { + foreach ($question->hintoptions as $key => $options) { + $question->hintshowchoicefeedback[$key] = !empty($options); + } + } + return $question; + } + + /** + * Support export to wordtable and htmltable format plugins + * + * Just call the corresponding XML functions + * cf. https://moodle.org/plugins/pluginversions.php?plugin=qformat_wordtable + * + * @param stdObject $question the question object + * @param qformat_xml $format the format object so that helper methods can be used + * @param mixed $extra any additional format specific data that may be passed by the format (see format code for info) + * @return string the data to append to the output buffer or false if error + */ + public function export_to_wordtable($question, qformat_xml $format, $extra=null) { + return $this->export_to_xml($question, $format, $extra); + } + + /** + * Support import from wordtable format plugin + * + * Just call the corresponding XML function + * cf. https://moodle.org/plugins/pluginversions.php?plugin=qformat_wordtable + * + * @param mixed $data the segment of data containing the question + * @param stdObject $question question object processed (so far) by standard import code + * @param qformat_xml $format the format object so that helper methods can be used (in particular error()) + * @param mixed $extra any additional format specific data that may be passed by the format (see format code for info) + * @return stdObject question object suitable for save_options() call or false if cannot handle + */ + public function import_from_wordtable($data, $question, qformat_xml $format, $extra=null) { + return $this->import_from_xml($data, $question, $format, $extra); + } + + /** + * Support export to htmltable format plugin + * + * Just call the corresponding XML functions + * cf. https://moodle.org/plugins/pluginversions.php?plugin=qformat_htmltable + * + * @param stdObject $question the question object + * @param qformat_xml $format the format object so that helper methods can be used + * @param mixed $extra any additional format specific data that may be passed by the format (see format code for info) + * @return string the data to append to the output buffer or false if error + */ + public function export_to_htmltable($question, qformat_xml $format, $extra=null) { + return $this->export_to_xml($question, $format, $extra); + } + +} + +/** + * An extension of {@link question_hint_with_parts} for multichoiceset questions. + * + * This has an extra option for whether to show the feedback for each choice. + * + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoiceset_hint extends question_hint_with_parts { + /** @var bool whether to show the feedback for each choice. */ + public $showchoicefeedback; + + /** + * Constructor. + * + * @param int $id Question ID + * @param string $hint The hint text + * @param int $hintformat + * @param bool $shownumcorrect whether the number of right parts should be shown + * @param bool $clearwrong whether the wrong parts should be reset. + * @param bool $showchoicefeedback whether to show the feedback for each choice. + */ + public function __construct($id, $hint, $hintformat, $shownumcorrect, + $clearwrong, $showchoicefeedback) { + parent::__construct($id, $hint, $hintformat, $shownumcorrect, $clearwrong); + $this->showchoicefeedback = $showchoicefeedback; + } + + /** + * Create a basic hint from a row loaded from the question_hints table in the database. + * + * @param object $row with $row->hint, ->shownumcorrect and ->clearwrong set. + * @return question_hint_with_parts + */ + public static function load_from_record($row) { + return new qtype_multichoiceset_hint($row->id, $row->hint, $row->hintformat, + $row->shownumcorrect, $row->clearwrong, !empty($row->options)); + } + + /** + * Adjust the display options + * + * @param question_display_options $options display options + * @return void + */ + public function adjust_display_options(question_display_options $options) { + parent::adjust_display_options($options); + $options->suppresschoicefeedback = !$this->showchoicefeedback; + } +} diff --git a/question/type/multichoiceset/styles.css b/question/type/multichoiceset/styles.css new file mode 100644 index 00000000..7b093b56 --- /dev/null +++ b/question/type/multichoiceset/styles.css @@ -0,0 +1,77 @@ +.que.multichoiceset .answer .specificfeedback { + display: inline; + padding: 0 0.7em; + background: #fff3bf; +} + +.que.multichoiceset .answer div.r0, +.que.multichoiceset .answer div.r1 { + display: flex; + margin: 0.25rem 0; + align-items: flex-start; +} + +.que.multichoiceset .answer div.r0 label, +.que.multichoiceset .answer div.r1 label, +.que.multichoiceset .answer div.r0 div.specificfeedback, +.que.multichoiceset .answer div.r1 div.specificfeedback { + /* In Chrome and IE, the text-indent above is applied to any embedded table + cells or
  • s, which screws up the intended layout. This fixes it again. */ + text-indent: 0; +} + +.que.multichoiceset .answer div.r0 .icon.fa-check, +.que.multichoiceset .answer div.r1 .icon.fa-check, +.que.multichoiceset .answer div.r0 .icon.fa-remove, +.que.multichoiceset .answer div.r1 .icon.fa-remove { + text-indent: 0; +} + +.que.multichoiceset .answer div.r0 input, +.que.multichoiceset .answer div.r1 input { + margin: 0.3rem 0.5rem; + width: 14px; +} + +/* Editing form. */ +body#page-question-type-multichoiceset div[id^=fitem_id_][id*=answer_] { + background: #eee; + margin-top: 0; + margin-bottom: 0; + padding-bottom: 5px; + padding-top: 5px; + border: 1px solid #bbb; + border-bottom: 0; +} + +body#page-question-type-multichoiceset div[id^=fitem_id_][id*=answer_] .fitemtitle { + font-weight: bold; +} + +body#page-question-type-multichoiceset div[id^=fitem_id_] .fitemtitle { + margin-left: 0; + margin-right: 0; + padding-left: 6px; + padding-right: 0; +} + +body#page-question-type-multichoiceset div[id^=fitem_id_][id*=fraction_] { + background: #eee; + margin-bottom: 0; + margin-top: 0; + padding-bottom: 5px; + padding-top: 5px; + border: 1px solid #bbb; + border-top: 0; + border-bottom: 0; +} + +body#page-question-type-multichoiceset div[id^=fitem_id_][id*=feedback_] { + background: #eee; + margin-bottom: 2em; + margin-top: 0; + padding-bottom: 5px; + padding-top: 5px; + border: 1px solid #bbb; + border-top: 0; +} diff --git a/question/type/multichoiceset/tests/behat/add.feature b/question/type/multichoiceset/tests/behat/add.feature new file mode 100644 index 00000000..53352851 --- /dev/null +++ b/question/type/multichoiceset/tests/behat/add.feature @@ -0,0 +1,38 @@ +@qtype @qtype_multichoiceset +Feature: Test creating a All-or-Nothing Multiple Choice question + As a teacher + In order to test my students + I need to be able to create a All-or-Nothing Multiple Choice question + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | T1 | Teacher1 | teacher1@moodle.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Question bank" in current page administration + + Scenario: Create a All-or-Nothing Multiple Choice question + When I add a "All-or-Nothing Multiple Choice" question filling the form with: + | Question name | All-or-nothing-001 | + | Question text | Find the capital cities in Europe. | + | General feedback | Paris and London | + | Choice 1 | Tokyo | + | Choice 2 | Spain | + | Choice 3 | London | + | Choice 4 | Barcelona | + | Choice 5 | Paris | + | id_correctanswer_0 | 0 | + | id_correctanswer_1 | 0 | + | id_correctanswer_2 | 1 | + | id_correctanswer_3 | 0 | + | id_correctanswer_4 | 1 | + | Hint 1 | First hint | + | Hint 2 | Second hint | + Then I should see "All-or-nothing-001" diff --git a/question/type/multichoiceset/tests/behat/backup_and_restore.feature b/question/type/multichoiceset/tests/behat/backup_and_restore.feature new file mode 100644 index 00000000..aec520eb --- /dev/null +++ b/question/type/multichoiceset/tests/behat/backup_and_restore.feature @@ -0,0 +1,48 @@ +@qtype @qtype_multichoiceset +Feature: Test duplicating a quiz containing a All-or-Nothing Multiple Choice question + As a teacher + In order re-use my courses containing All-or-Nothing Multiple Choice questions + I need to be able to backup and restore them + + Background: + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | Test questions | + And the following "questions" exist: + | questioncategory | qtype | name | template | + | Test questions | multichoiceset | All-or-nothing-001 | two_of_four | + And the following "activities" exist: + | activity | name | course | idnumber | + | quiz | Test quiz | C1 | quiz1 | + And quiz "Test quiz" contains the following questions: + | All-or-nothing-001 | 1 | + And I log in as "admin" + And I am on "Course 1" course homepage + + @javascript + Scenario: Backup and restore a course containing a All-or-Nothing Multiple Choic question + When I backup "Course 1" course using this options: + | Confirmation | Filename | test_backup.mbz | + And I restore "test_backup.mbz" backup into a new course using this options: + | Schema | Course name | Course 2 | + And I navigate to "Question bank" in current page administration + And I click on "Edit" "link" in the "All-or-nothing-001" "table_row" + Then the following fields match these values: + | Question name | All-or-nothing-001 | + | Question text | Which are the odd numbers? | + | General feedback | The odd numbers are One and Three. | + | Default mark | 1 | + | Shuffle the choices? | 1 | + | Choice 1 | One | + | Choice 2 | Two | + | Choice 3 | Three | + | Choice 4 | Four | + | id_correctanswer_0 | 1 | + | id_correctanswer_1 | 0 | + | id_correctanswer_2 | 1 | + | id_correctanswer_3 | 0 | + | For any correct response | Well done! | + | For any incorrect response | That is not right at all. | diff --git a/question/type/multichoiceset/tests/behat/edit.feature b/question/type/multichoiceset/tests/behat/edit.feature new file mode 100644 index 00000000..056eb11f --- /dev/null +++ b/question/type/multichoiceset/tests/behat/edit.feature @@ -0,0 +1,36 @@ +@qtype @qtype_multichoiceset +Feature: Test editing a All-or-Nothing Multiple Choice question + As a teacher + In order to be able to update my All-or-Nothing Multiple Choice question + I need to edit them + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | T1 | Teacher1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | Test questions | + And the following "questions" exist: + | questioncategory | qtype | name | template | + | Test questions | multichoiceset | All-or-nothing for editing | two_of_four | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Question bank" in current page administration + + Scenario: Edit a All-or-Nothing Multiple Choice question + When I click on "Edit" "link" in the "All-or-nothing for editing" "table_row" + And I set the following fields to these values: + | Question name | | + And I press "id_submitbutton" + Then I should see "You must supply a value here." + When I set the following fields to these values: + | Question name | Edited All-or-nothing name | + And I press "id_submitbutton" + Then I should see "Edited All-or-nothing name" diff --git a/question/type/multichoiceset/tests/behat/export.feature b/question/type/multichoiceset/tests/behat/export.feature new file mode 100644 index 00000000..7c3298d4 --- /dev/null +++ b/question/type/multichoiceset/tests/behat/export.feature @@ -0,0 +1,35 @@ +@qtype @qtype_multichoiceset +Feature: Test exporting All-or-Nothing Multiple Choice questions + As a teacher + In order to be able to reuse my All-or-Nothing Multiple Choice questions + I need to export them + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | T1 | Teacher1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | Test questions | + And the following "questions" exist: + | questioncategory | qtype | name | template | + | Test questions | multichoiceset | Multi-choice-001 | two_of_four | + And I log in as "teacher1" + And I am on "Course 1" course homepage + + Scenario: Export a All-or-Nothing Multiple Choice question + When I navigate to "Question bank > Export" in current page administration + And I set the field "id_format_xml" to "1" + And I press "Export questions to file" + Then following "click here" should download between "1800" and "1900" bytes + # If the download step is the last in the scenario then we can sometimes run + # into the situation where the download page causes a http redirect but behat + # has already conducted its reset (generating an error). By putting a logout + # step we avoid behat doing the reset until we are off that page. + And I log out diff --git a/question/type/multichoiceset/tests/behat/import.feature b/question/type/multichoiceset/tests/behat/import.feature new file mode 100644 index 00000000..39b6c87e --- /dev/null +++ b/question/type/multichoiceset/tests/behat/import.feature @@ -0,0 +1,30 @@ +@qtype @qtype_multichoiceset +Feature: Test importing All-or-Nothing Multiple Choice questions + As a teacher + In order to reuse All-or-Nothing Multiple Choice questions + I need to import them + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | T1 | Teacher1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I log in as "teacher1" + And I am on "Course 1" course homepage + + @javascript @_file_upload + Scenario: import All-or-Nothing Multiple Choice question. + When I navigate to "Question bank > Import" in current page administration + And I set the field "id_format_xml" to "1" + And I upload "question/type/multichoiceset/tests/fixtures/qtype_sample_multichoiceset.xml" file to "Import" filemanager + And I press "id_submitbutton" + Then I should see "Parsing questions from import file." + And I should see "Importing 1 questions from file" + And I should see "1. Find the capital cities in Europe." + And I press "Continue" + And I should see "All-or-nothing-001" diff --git a/question/type/multichoiceset/tests/behat/preview.feature b/question/type/multichoiceset/tests/behat/preview.feature new file mode 100644 index 00000000..3bcca7d4 --- /dev/null +++ b/question/type/multichoiceset/tests/behat/preview.feature @@ -0,0 +1,56 @@ +@qtype @qtype_multichoiceset +Feature: Preview a All-or-Nothing Multiple Choice question + As a teacher + In order to check my All-or-Nothing Multiple Choice questions will work for students + I need to preview them + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | T1 | Teacher1 | teacher1@moodle.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | Test questions | + And the following "questions" exist: + | questioncategory | qtype | name | template | + | Test questions | multichoiceset | All-or-nothing-001 | two_of_four | + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to "Question bank" in current page administration + + @javascript @_switch_window + Scenario: Preview a Multiple choice question and submit a wrong response. + When I click on "Preview" "link" in the "All-or-nothing-001" "table_row" + And I switch to "questionpreview" window + And I set the field "How questions behave" to "Immediate feedback" + And I press "Start again with these options" + And I click on "One" "checkbox" + And I click on "Two" "checkbox" + And I press "Check" + Then I should see "One is odd" + And I should see "Two is even" + And I should see "Mark 0.00 out of 1.00" + And I switch to the main window + + @javascript @_switch_window + Scenario: Preview a Multiple choice question and submit a correct response. + When I click on "Preview" "link" in the "All-or-nothing-001" "table_row" + And I switch to "questionpreview" window + And I set the field "How questions behave" to "Immediate feedback" + And I press "Start again with these options" + And I click on "One" "checkbox" + And I click on "Three" "checkbox" + And I press "Check" + Then I should see "One is odd" + And I should see "Three is odd" + And I should see "Mark 1.00 out of 1.00" + And I should see "Well done!" + And I should see "The odd numbers are One and Three." + And I should see "The correct answers are: One, Three" + And I switch to the main window diff --git a/question/type/multichoiceset/tests/fixtures/qtype_sample_multichoiceset.xml b/question/type/multichoiceset/tests/fixtures/qtype_sample_multichoiceset.xml new file mode 100644 index 00000000..fccbe1e7 --- /dev/null +++ b/question/type/multichoiceset/tests/fixtures/qtype_sample_multichoiceset.xml @@ -0,0 +1,72 @@ + + + + + + $course$/Default for C1 + + + + + + + + All-or-nothing-001 + + + Find the capital cities in Europe. + + + Berlin, Paris and London + + 1.0000000 + 0.3333333 + 0 + true + abc + + Your answer is correct. + + + Your answer is incorrect. + + + + Tokyo + + + + + + Spain + + + + + + London + + + + + + Barcelona + + + + + + Paris + + + + + + First hint + + + Second hint + + + + diff --git a/question/type/multichoiceset/tests/helper.php b/question/type/multichoiceset/tests/helper.php new file mode 100644 index 00000000..21873100 --- /dev/null +++ b/question/type/multichoiceset/tests/helper.php @@ -0,0 +1,245 @@ +. + +/** + * Contains the helper class for the select missing words question type tests. + * + * @package qtype_multichoiceset + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Test helper class for the all or nothing multichoice question type. + * + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoiceset_test_helper { + /** + * Get dummy test questions. + * + * @return array + */ + public function get_test_questions() { + return array('two_of_four'); + } + + /** + * Makes a multichoiceset question with 2 correct answers. + * + * @return qtype_miltichoiceset_question + */ + public function make_multichoiceset_question_two_of_four() { + question_bank::load_question_definition_classes('multichoiceset'); + $mc = new qtype_multichoiceset_question(); + test_question_maker::initialise_a_question($mc); + $mc->name = 'All or nothing multiple choice choice question'; + $mc->questiontext = 'Which are the odd numbers?'; + $mc->generalfeedback = 'The odd numbers are One and Three.'; + $mc->qtype = question_bank::get_qtype('multichoiceset'); + $mc->shuffleanswers = 1; + $mc->answernumbering = 'abc'; + test_question_maker::set_standard_combined_feedback_fields($mc); + $mc->answers = array( + 13 => new question_answer(13, 'One', 1.0, 'One is odd.', FORMAT_HTML), + 14 => new question_answer(14, 'Two', 0, 'Two is even.', FORMAT_HTML), + 15 => new question_answer(15, 'Three', 1.0, 'Three is odd.', FORMAT_HTML), + 16 => new question_answer(16, 'Four', 0, 'Four is even.', FORMAT_HTML), + ); + return $mc; + } + + /** + * Get the question data, as it would be loaded by get_question_options. + * + * @return object + */ + public static function get_multichoiceset_question_data_two_of_four() { + global $USER; + + $qdata = new stdClass(); + + $qdata->createdby = $USER->id; + $qdata->modifiedby = $USER->id; + $qdata->qtype = 'multichoiceset'; + $qdata->name = 'All or nothing multiple choice choice question'; + $qdata->questiontext = 'Which are the odd numbers?'; + $qdata->questiontextformat = FORMAT_HTML; + $qdata->generalfeedback = 'The odd numbers are One and Three.'; + $qdata->generalfeedbackformat = FORMAT_HTML; + $qdata->defaultmark = 1; + $qdata->length = 1; + $qdata->penalty = 0.3333333; + $qdata->hidden = 0; + + $qdata->options = new stdClass(); + $qdata->options->shuffleanswers = 1; + $qdata->options->answernumbering = '123'; + $qdata->options->layout = 0; + $qdata->options->correctfeedback + = test_question_maker::STANDARD_OVERALL_CORRECT_FEEDBACK; + $qdata->options->correctfeedbackformat = FORMAT_HTML; + $qdata->options->shownumcorrect = 1; + $qdata->options->incorrectfeedback + = test_question_maker::STANDARD_OVERALL_INCORRECT_FEEDBACK; + $qdata->options->incorrectfeedbackformat = FORMAT_HTML; + + $qdata->options->answers = array( + 13 => (object) array( + 'id' => 13, + 'answer' => 'One', + 'answerformat' => FORMAT_PLAIN, + 'fraction' => '1.0', + 'feedback' => 'One is odd.', + 'feedbackformat' => FORMAT_HTML, + ), + 14 => (object) array( + 'id' => 14, + 'answer' => 'Two', + 'answerformat' => FORMAT_PLAIN, + 'fraction' => '0.0', + 'feedback' => 'Two is even.', + 'feedbackformat' => FORMAT_HTML, + ), + 15 => (object) array( + 'id' => 15, + 'answer' => 'Three', + 'answerformat' => FORMAT_PLAIN, + 'fraction' => '1.0', + 'feedback' => 'Three is odd.', + 'feedbackformat' => FORMAT_HTML, + ), + 16 => (object) array( + 'id' => 16, + 'answer' => 'Four', + 'answerformat' => FORMAT_PLAIN, + 'fraction' => '0.0', + 'feedback' => 'Four is even.', + 'feedbackformat' => FORMAT_HTML, + ), + ); + + $qdata->hints = array( + 1 => (object) array( + 'hint' => 'Hint 1.', + 'hintformat' => FORMAT_HTML, + 'shownumcorrect' => 1, + 'clearwrong' => 0, + 'options' => 0, + ), + 2 => (object) array( + 'hint' => 'Hint 2.', + 'hintformat' => FORMAT_HTML, + 'shownumcorrect' => 1, + 'clearwrong' => 1, + 'options' => 1, + ), + ); + + return $qdata; + } + /** + * Get the question data, as it would be loaded by get_question_options. + * + * @return object + */ + public static function get_multichoiceset_question_form_data_two_of_four() { + $qdata = new stdClass(); + + $qdata->name = 'All or nothing multiple choice choice question'; + $qdata->questiontext = array('text' => 'Which are the odd numbers?', 'format' => FORMAT_HTML); + $qdata->generalfeedback = array('text' => 'The odd numbers are One and Three.', 'format' => FORMAT_HTML); + $qdata->defaultmark = 1; + $qdata->noanswers = 5; + $qdata->numhints = 2; + $qdata->penalty = 0.3333333; + + $qdata->shuffleanswers = 1; + $qdata->answernumbering = '123'; + $qdata->single = '0'; + $qdata->correctfeedback = array('text' => test_question_maker::STANDARD_OVERALL_CORRECT_FEEDBACK, + 'format' => FORMAT_HTML); + $qdata->shownumcorrect = 1; + $qdata->incorrectfeedback = array('text' => test_question_maker::STANDARD_OVERALL_INCORRECT_FEEDBACK, + 'format' => FORMAT_HTML); + $qdata->correctanswer = array('1', '0', '1', '0', '0'); + $qdata->answer = array( + 0 => array( + 'text' => 'One', + 'format' => FORMAT_PLAIN + ), + 1 => array( + 'text' => 'Two', + 'format' => FORMAT_PLAIN + ), + 2 => array( + 'text' => 'Three', + 'format' => FORMAT_PLAIN + ), + 3 => array( + 'text' => 'Four', + 'format' => FORMAT_PLAIN + ), + 4 => array( + 'text' => '', + 'format' => FORMAT_PLAIN + ) + ); + + $qdata->feedback = array( + 0 => array( + 'text' => 'One is odd.', + 'format' => FORMAT_HTML + ), + 1 => array( + 'text' => 'Two is even.', + 'format' => FORMAT_HTML + ), + 2 => array( + 'text' => 'Three is odd.', + 'format' => FORMAT_HTML + ), + 3 => array( + 'text' => 'Four is even.', + 'format' => FORMAT_HTML + ), + 4 => array( + 'text' => '', + 'format' => FORMAT_HTML + ) + ); + + $qdata->hint = array( + 0 => array( + 'text' => 'Hint 1.', + 'format' => FORMAT_HTML + ), + 1 => array( + 'text' => 'Hint 2.', + 'format' => FORMAT_HTML + ) + ); + $qdata->hintclearwrong = array(0, 1); + $qdata->hintshownumcorrect = array(1, 1); + + return $qdata; + } +} diff --git a/question/type/multichoiceset/tests/question_test.php b/question/type/multichoiceset/tests/question_test.php new file mode 100644 index 00000000..9dcf4d77 --- /dev/null +++ b/question/type/multichoiceset/tests/question_test.php @@ -0,0 +1,142 @@ +. + +/** + * Unit tests for the multiple choice question definition classes. + * + * @package qtype_multichoiceset + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); +require_once($CFG->dirroot . '/question/type/multichoiceset/tests/helper.php'); + + +/** + * Unit tests for the multiple choice all or nothing question definition class. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoiceset_question_test extends advanced_testcase { + /** + * Get a test question. + * + * @param stdObject $which + * @return qtype_multichoiceset_question the requested question object. + */ + protected function get_test_multichoiceset_question($which = null) { + return test_question_maker::make_question('multichoiceset', $which); + } + + public function test_get_expected_data() { + $question = $this->get_test_multichoiceset_question('two_of_four'); + $question->start_attempt(new question_attempt_step(), 1); + + $this->assertEquals(array('choice0' => PARAM_BOOL, 'choice1' => PARAM_BOOL, + 'choice2' => PARAM_BOOL, 'choice3' => PARAM_BOOL), $question->get_expected_data()); + } + + public function test_is_complete_response() { + $question = $this->get_test_multichoiceset_question('two_of_four'); + $question->start_attempt(new question_attempt_step(), 1); + + $this->assertFalse($question->is_complete_response(array())); + $this->assertFalse($question->is_complete_response( + array('choice0' => '0', 'choice1' => '0', 'choice2' => '0', 'choice3' => '0'))); + $this->assertTrue($question->is_complete_response(array('choice1' => '1'))); + $this->assertTrue($question->is_complete_response( + array('choice0' => '1', 'choice1' => '1', 'choice2' => '1', 'choice3' => '1'))); + } + + public function test_is_gradable_response() { + $question = $this->get_test_multichoiceset_question('two_of_four'); + $question->start_attempt(new question_attempt_step(), 1); + + $this->assertFalse($question->is_gradable_response(array())); + $this->assertFalse($question->is_gradable_response( + array('choice0' => '0', 'choice1' => '0', 'choice2' => '0', 'choice3' => '0'))); + $this->assertTrue($question->is_gradable_response(array('choice1' => '1'))); + $this->assertTrue($question->is_gradable_response( + array('choice0' => '1', 'choice1' => '1', 'choice2' => '1', 'choice3' => '1'))); + } + + public function test_grading() { + $question = $this->get_test_multichoiceset_question('two_of_four'); + $question->shuffleanswers = false; + $question->start_attempt(new question_attempt_step(), 1); + + $this->assertEquals(array(1, question_state::$gradedright), + $question->grade_response(array('choice0' => '1', 'choice2' => '1'))); + $this->assertEquals(array(0, question_state::$gradedwrong), + $question->grade_response(array('choice0' => '1'))); + $this->assertEquals(array(0, question_state::$gradedwrong), + $question->grade_response( + array('choice0' => '1', 'choice1' => '1', 'choice2' => '1'))); + $this->assertEquals(array(0, question_state::$gradedwrong), + $question->grade_response(array('choice1' => '1'))); + } + + public function test_get_correct_response() { + $question = $this->get_test_multichoiceset_question('two_of_four'); + $question->shuffleanswers = false; + $question->start_attempt(new question_attempt_step(), 1); + + $this->assertEquals(array('choice0' => '1', 'choice2' => '1'), + $question->get_correct_response()); + } + + public function test_get_question_summary() { + $mc = $this->get_test_multichoiceset_question('two_of_four'); + $mc->start_attempt(new question_attempt_step(), 1); + + $qsummary = $mc->get_question_summary(); + + $this->assertRegExp('/' . preg_quote($mc->questiontext) . '/', $qsummary); + foreach ($mc->answers as $answer) { + $this->assertRegExp('/' . preg_quote($answer->answer) . '/', $qsummary); + } + } + + public function test_summarise_response() { + $mc = $this->get_test_multichoiceset_question('two_of_four'); + $mc->shuffleanswers = false; + $mc->start_attempt(new question_attempt_step(), 1); + + $summary = $mc->summarise_response(array('choice1' => 1, 'choice2' => 1), + test_question_maker::get_a_qa($mc)); + + $this->assertEquals('Two; Three', $summary); + } + + public function test_classify_response() { + $mc = $this->get_test_multichoiceset_question('two_of_four'); + $mc->shuffleanswers = false; + $mc->start_attempt(new question_attempt_step(), 1); + + $this->assertEquals(array( + 13 => new question_classified_response(13, 'One', 1.0), + 14 => new question_classified_response(14, 'Two', 0.0), + ), $mc->classify_response(array('choice0' => 1, 'choice1' => 1))); + + $this->assertEquals(array(), $mc->classify_response(array())); + } +} diff --git a/question/type/multichoiceset/tests/questiontype_test.php b/question/type/multichoiceset/tests/questiontype_test.php new file mode 100644 index 00000000..571a30ad --- /dev/null +++ b/question/type/multichoiceset/tests/questiontype_test.php @@ -0,0 +1,88 @@ +. + +/** + * Unit tests for the mulitple choice question definition class. + * + * @package qtype_multichoiceset + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/question/type/multichoiceset/questiontype.php'); + + +/** + * Unit tests for the multiple choice all or nothing question definition class. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoiceset_test extends advanced_testcase { + /** @var $qtype the question type. */ + protected $qtype; + + protected function setUp() { + $this->qtype = new qtype_multichoiceset(); + } + + protected function tearDown() { + $this->qtype = null; + } + + public function test_name() { + $this->assertEquals($this->qtype->name(), 'multichoiceset'); + } + + /** + * Get test question data. + * + * @return stdObject + */ + protected function get_test_question_data() { + $q = new stdClass(); + $q->id = 1; + $q->options = new stdClass(); + $q->options->answers[1] = (object) array('answer' => 'frog', + 'answerformat' => FORMAT_HTML, 'fraction' => 1); + $q->options->answers[2] = (object) array('answer' => 'toad', + 'answerformat' => FORMAT_HTML, 'fraction' => 0); + + return $q; + } + + public function test_can_analyse_responses() { + $this->assertTrue($this->qtype->can_analyse_responses()); + } + + public function test_get_random_guess_score() { + $q = $this->get_test_question_data(); + $this->assertNull($this->qtype->get_random_guess_score($q)); + } + + public function test_get_possible_responses() { + $q = $this->get_test_question_data(); + + $this->assertEquals(array( + 1 => array(1 => new question_possible_response('frog', 1)), + 2 => array(2 => new question_possible_response('toad', 0)), + ), $this->qtype->get_possible_responses($q)); + } +} diff --git a/question/type/multichoiceset/tests/walkthrough_test.php b/question/type/multichoiceset/tests/walkthrough_test.php new file mode 100644 index 00000000..5bd330e8 --- /dev/null +++ b/question/type/multichoiceset/tests/walkthrough_test.php @@ -0,0 +1,123 @@ +. + +/** + * This file contains tests that walk mutichoice questions through various behaviours. + * + * Note, there are already lots of tests of the multichoice type in the behaviour + * tests. (Search for test_question_maker::make_a_multichoice.) This file only + * contains a few additional tests for problems that were found during testing. + * + * @package qtype_multichoiceset + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/question/engine/lib.php'); +require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); +require_once($CFG->dirroot . '/question/type/multichoiceset/tests/helper.php'); + +/** + * Unit tests for the mutiple choice all or nothingquestion type. + * + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoiceset_walkthrough_test extends qbehaviour_walkthrough_test_base { + + public function test_deferredfeedback_feedback_multichoiceset() { + // Create a multichoiceset question. + $mc = $dd = test_question_maker::make_question('multichoiceset'); + $mc->shuffleanswers = false; + + $this->start_attempt_at_question($mc, 'deferredfeedback', 2); + $this->process_submission($mc->get_correct_response()); + $this->quba->finish_all_questions(); + + // Verify. + $this->check_current_state(question_state::$gradedright); + $this->check_current_mark(2); + $this->check_current_output( + $this->get_contains_mc_checkbox_expectation('choice0', false, true), + $this->get_contains_mc_checkbox_expectation('choice1', false, false), + $this->get_contains_mc_checkbox_expectation('choice2', false, true), + $this->get_contains_mc_checkbox_expectation('choice3', false, false), + $this->get_contains_correct_expectation(), + new question_pattern_expectation('/class="r0 correct"/'), + new question_pattern_expectation('/class="r1"/')); + } + + public function test_deferredfeedback_resume_multichoiceset_right_right() { + + // Create a multichoiceset question. + $mc = $dd = test_question_maker::make_question('multichoiceset'); + $mc->shuffleanswers = false; + + $this->start_attempt_at_question($mc, 'deferredfeedback', 2); + $this->process_submission($mc->get_correct_response()); + $this->quba->finish_all_questions(); + + // Verify. + $this->check_current_state(question_state::$gradedright); + $this->check_current_mark(2); + $this->check_current_output( + $this->get_contains_mc_checkbox_expectation('choice0', false, true), + $this->get_contains_mc_checkbox_expectation('choice1', false, false), + $this->get_contains_mc_checkbox_expectation('choice2', false, true), + $this->get_contains_mc_checkbox_expectation('choice3', false, false), + $this->get_contains_correct_expectation(), + new question_pattern_expectation('/class="r0 correct"/'), + new question_pattern_expectation('/class="r1"/')); + + // Save the old attempt. + $oldqa = $this->quba->get_question_attempt($this->slot); + + // Reinitialise. + $this->setUp(); + $this->quba->set_preferred_behaviour('deferredfeedback'); + $this->slot = $this->quba->add_question($mc, 2); + $this->quba->start_question_based_on($this->slot, $oldqa); + + // Verify. + $this->check_current_state(question_state::$complete); + $this->check_output_contains_lang_string('notchanged', 'question'); + $this->check_current_mark(null); + $this->check_current_output( + $this->get_contains_mc_checkbox_expectation('choice0', true, true), + $this->get_contains_mc_checkbox_expectation('choice1', true, false), + $this->get_contains_mc_checkbox_expectation('choice2', true, true), + $this->get_contains_mc_checkbox_expectation('choice3', true, false)); + + // Now resubmit. + $this->quba->finish_all_questions(); + + // Verify. + $this->check_current_state(question_state::$gradedright); + $this->check_current_mark(2); + $this->check_current_output( + $this->get_contains_mc_checkbox_expectation('choice0', false, true), + $this->get_contains_mc_checkbox_expectation('choice1', false, false), + $this->get_contains_mc_checkbox_expectation('choice2', false, true), + $this->get_contains_mc_checkbox_expectation('choice3', false, false), + $this->get_contains_correct_expectation(), + new question_pattern_expectation('/class="r0 correct"/'), + new question_pattern_expectation('/class="r1"/')); + } +} diff --git a/question/type/multichoiceset/version.php b/question/type/multichoiceset/version.php new file mode 100644 index 00000000..a6fe4f7c --- /dev/null +++ b/question/type/multichoiceset/version.php @@ -0,0 +1,38 @@ +. + +/** + * Version information for the multiple choice All-or-Nothing question type. + * + * @package qtype_multichoiceset + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'qtype_multichoiceset'; +$plugin->version = 2020111701; + +$plugin->requires = 2020060900; + +$plugin->cron = 0; +$plugin->maturity = MATURITY_STABLE; +$plugin->release = '1.7.0 for Moodle 3.9'; + +$plugin->dependencies = array( + 'qtype_multichoice' => ANY_VERSION, +); From 2030e5aa8c6a86c6c13e609753d6344e74d8beb2 Mon Sep 17 00:00:00 2001 From: Sanmugam Kathirvel Date: Tue, 16 Mar 2021 20:39:51 +0530 Subject: [PATCH 3/5] Re attempt and inprogress relatedD changes --- mod/quiz/view.php | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/mod/quiz/view.php b/mod/quiz/view.php index e423fb32..52972306 100644 --- a/mod/quiz/view.php +++ b/mod/quiz/view.php @@ -221,22 +221,29 @@ * When the user try to attempt the quiz very first time */ -if(empty($viewobj->attempts)){ - - $sql = "select id from training_eventteststatus where mdlemail = '".$USER->email."' and mdlcourse_id = ".$quiz->course." and mdlquiz_id = ".$quiz->id." and part_status = 0"; - $result = $mysqli->query($sql); - $count = $result->num_rows; +$sql = "select id from training_eventteststatus where mdlemail = '".$USER->email."' and mdlcourse_id = ".$quiz->course." and mdlquiz_id = ".$quiz->id." and part_status = 0"; +$result = $mysqli->query($sql); +$count = $result->num_rows; /** * When the user try to do re-attempt */ -}else{ +if(!empty($viewobj->attempts)){ foreach($viewobj->attempts as $a){ - $sql = "select id from training_eventteststatus where mdlemail = '".$USER->email."' and mdlcourse_id = ".$quiz->course." and mdlquiz_id=".$a->quiz." and mdlattempt_id=".$a->id." and part_status <= 2"; + // When re attempt with Continue the last attempt + if ('inprogress' === $a->state) { + $count = 1; + break; + } + // When re attempt begins + if ('finished' !== $a->state) { + $sql = "select id from training_eventteststatus where mdlemail = '".$USER->email."' and mdlcourse_id = ".$quiz->course." and mdlquiz_id=".$a->quiz." and mdlattempt_id=".$a->id." and part_status <= 2"; - $result = $mysqli->query($sql); - $count = $result->num_rows; + $result = $mysqli->query($sql); + $count = $result->num_rows; + break; + } } } From d359f79df69d816de3671737f7c00cc49de1fdce Mon Sep 17 00:00:00 2001 From: Sanmugam Kathirvel Date: Thu, 18 Mar 2021 00:16:36 +0530 Subject: [PATCH 4/5] Spoken course enrollment plugin --- enrol/spoken/bulkchangeforms.php | 0 .../classes/deleteselectedusers_form.php | 37 + .../classes/deleteselectedusers_operation.php | 98 ++ .../spoken/classes/editselectedusers_form.php | 37 + .../classes/editselectedusers_operation.php | 161 +++ enrol/spoken/classes/empty_form.php | 41 + enrol/spoken/classes/privacy/provider.php | 41 + .../task/send_expiry_notifications.php | 58 + enrol/spoken/classes/task/sync_enrolments.php | 58 + enrol/spoken/cli/sync.php | 76 ++ enrol/spoken/db/access.php | 85 ++ enrol/spoken/db/install.php | 29 + enrol/spoken/db/messages.php | 29 + enrol/spoken/db/services.php | 44 + enrol/spoken/db/tasks.php | 48 + enrol/spoken/db/upgrade.php | 49 + enrol/spoken/externallib.php | 257 ++++ enrol/spoken/lang/en/enrol_spoken.php | 127 ++ enrol/spoken/lib.php | 1085 +++++++++++++++++ enrol/spoken/locallib.php | 153 +++ enrol/spoken/pix/withkey.gif | Bin 0 -> 608 bytes enrol/spoken/pix/withkey.png | Bin 0 -> 230 bytes enrol/spoken/pix/withkey.svg | 3 + enrol/spoken/pix/withoutkey.gif | Bin 0 -> 625 bytes enrol/spoken/pix/withoutkey.png | Bin 0 -> 182 bytes enrol/spoken/pix/withoutkey.svg | 3 + enrol/spoken/settings.php | 124 ++ enrol/spoken/tests/behat/key_holder.feature | 51 + .../spoken/tests/behat/self_enrolment.feature | 127 ++ enrol/spoken/tests/externallib_test.php | 254 ++++ enrol/spoken/tests/spoken_test.php | 790 ++++++++++++ enrol/spoken/unenrolself.php | 63 + enrol/spoken/version.php | 30 + 33 files changed, 3958 insertions(+) create mode 100644 enrol/spoken/bulkchangeforms.php create mode 100644 enrol/spoken/classes/deleteselectedusers_form.php create mode 100644 enrol/spoken/classes/deleteselectedusers_operation.php create mode 100644 enrol/spoken/classes/editselectedusers_form.php create mode 100644 enrol/spoken/classes/editselectedusers_operation.php create mode 100644 enrol/spoken/classes/empty_form.php create mode 100644 enrol/spoken/classes/privacy/provider.php create mode 100644 enrol/spoken/classes/task/send_expiry_notifications.php create mode 100644 enrol/spoken/classes/task/sync_enrolments.php create mode 100644 enrol/spoken/cli/sync.php create mode 100644 enrol/spoken/db/access.php create mode 100644 enrol/spoken/db/install.php create mode 100644 enrol/spoken/db/messages.php create mode 100644 enrol/spoken/db/services.php create mode 100644 enrol/spoken/db/tasks.php create mode 100644 enrol/spoken/db/upgrade.php create mode 100644 enrol/spoken/externallib.php create mode 100644 enrol/spoken/lang/en/enrol_spoken.php create mode 100644 enrol/spoken/lib.php create mode 100644 enrol/spoken/locallib.php create mode 100644 enrol/spoken/pix/withkey.gif create mode 100644 enrol/spoken/pix/withkey.png create mode 100644 enrol/spoken/pix/withkey.svg create mode 100644 enrol/spoken/pix/withoutkey.gif create mode 100644 enrol/spoken/pix/withoutkey.png create mode 100644 enrol/spoken/pix/withoutkey.svg create mode 100644 enrol/spoken/settings.php create mode 100644 enrol/spoken/tests/behat/key_holder.feature create mode 100644 enrol/spoken/tests/behat/self_enrolment.feature create mode 100644 enrol/spoken/tests/externallib_test.php create mode 100644 enrol/spoken/tests/spoken_test.php create mode 100644 enrol/spoken/unenrolself.php create mode 100644 enrol/spoken/version.php diff --git a/enrol/spoken/bulkchangeforms.php b/enrol/spoken/bulkchangeforms.php new file mode 100644 index 00000000..e69de29b diff --git a/enrol/spoken/classes/deleteselectedusers_form.php b/enrol/spoken/classes/deleteselectedusers_form.php new file mode 100644 index 00000000..3e617e31 --- /dev/null +++ b/enrol/spoken/classes/deleteselectedusers_form.php @@ -0,0 +1,37 @@ +. + +/** + * The form to confirm the intention to bulk delete users enrolments. + * + * @package enrol_spoken + * @copyright 2018 Farhan Karmali + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once("$CFG->dirroot/enrol/bulkchange_forms.php"); + +/** + * The form to confirm the intention to bulk delete users enrolments. + * + * @package enrol_spoken + * @copyright 2018 Farhan Karmali + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class enrol_spoken_deleteselectedusers_form extends enrol_bulk_enrolment_confirm_form { +} diff --git a/enrol/spoken/classes/deleteselectedusers_operation.php b/enrol/spoken/classes/deleteselectedusers_operation.php new file mode 100644 index 00000000..f4a0f64a --- /dev/null +++ b/enrol/spoken/classes/deleteselectedusers_operation.php @@ -0,0 +1,98 @@ +. + +/** + * A bulk operation for the spoken enrolment plugin to delete selected users enrolments. + * + * @package enrol_spoken + * @copyright 2018 Farhan Karmali + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * A bulk operation for the spoken enrolment plugin to delete selected users enrolments. + * + * @package enrol_spoken + * @copyright 2018 Farhan Karmali + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class enrol_spoken_deleteselectedusers_operation extends enrol_bulk_enrolment_operation { + + /** + * Returns the title to display for this bulk operation. + * + * @return string + */ + public function get_identifier() { + return 'deleteselectedusers'; + } + + /** + * Returns the identifier for this bulk operation. This is the key used when the plugin + * returns an array containing all of the bulk operations it supports. + * + * @return string + */ + public function get_title() { + return get_string('deleteselectedusers', 'enrol_spoken'); + } + + /** + * Returns a enrol_bulk_enrolment_operation extension form to be used + * in collecting required information for this operation to be processed. + * + * @param string|moodle_url|null $defaultaction + * @param mixed $defaultcustomdata + * @return enrol_spoken_deleteselectedusers_form + */ + public function get_form($defaultaction = null, $defaultcustomdata = null) { + if (!array($defaultcustomdata)) { + $defaultcustomdata = array(); + } + $defaultcustomdata['title'] = $this->get_title(); + $defaultcustomdata['message'] = get_string('confirmbulkdeleteenrolment', 'enrol_spoken'); + $defaultcustomdata['button'] = get_string('unenrolusers', 'enrol_spoken'); + + return new enrol_spoken_deleteselectedusers_form($defaultaction, $defaultcustomdata); + } + + /** + * Processes the bulk operation request for the given userids with the provided properties. + * + * @param course_enrolment_manager $manager + * @param array $users + * @param stdClass $properties The data returned by the form. + */ + public function process(course_enrolment_manager $manager, array $users, stdClass $properties) { + if (!has_capability("enrol/spoken:unenrol", $manager->get_context())) { + return false; + } + + foreach ($users as $user) { + foreach ($user->enrolments as $enrolment) { + $plugin = $enrolment->enrolmentplugin; + $instance = $enrolment->enrolmentinstance; + if ($plugin->allow_unenrol_user($instance, $enrolment)) { + $plugin->unenrol_user($instance, $user->id); + } + } + } + + return true; + } +} diff --git a/enrol/spoken/classes/editselectedusers_form.php b/enrol/spoken/classes/editselectedusers_form.php new file mode 100644 index 00000000..123c7289 --- /dev/null +++ b/enrol/spoken/classes/editselectedusers_form.php @@ -0,0 +1,37 @@ +. + +/** + * The form to collect required information when bulk editing users enrolments. + * + * @package enrol_spoken + * @copyright 2018 Farhan Karmali + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once("$CFG->dirroot/enrol/bulkchange_forms.php"); + +/** + * The form to collect required information when bulk editing users enrolments. + * + * @package enrol_spoken + * @copyright 2018 Farhan Karmali + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class enrol_spoken_editselectedusers_form extends enrol_bulk_enrolment_change_form { +} diff --git a/enrol/spoken/classes/editselectedusers_operation.php b/enrol/spoken/classes/editselectedusers_operation.php new file mode 100644 index 00000000..031800f0 --- /dev/null +++ b/enrol/spoken/classes/editselectedusers_operation.php @@ -0,0 +1,161 @@ +. + +/** + * A bulk operation for the manual enrolment plugin to edit selected users. + * + * @package enrol_spoken + * @copyright 2018 Farhan Karmali + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * A bulk operation for the manual enrolment plugin to edit selected users. + * + * @package enrol_spoken + * @copyright 2018 Farhan Karmali + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class enrol_spoken_editselectedusers_operation extends enrol_bulk_enrolment_operation { + + /** + * Returns the title to display for this bulk operation. + * + * @return string + */ + public function get_title() { + return get_string('editselectedusers', 'enrol_spoken'); + } + + /** + * Returns the identifier for this bulk operation. This is the key used when the plugin + * returns an array containing all of the bulk operations it supports. + */ + public function get_identifier() { + return 'editselectedusers'; + } + + /** + * Processes the bulk operation request for the given userids with the provided properties. + * + * @param course_enrolment_manager $manager + * @param array $users + * @param stdClass $properties The data returned by the form. + */ + public function process(course_enrolment_manager $manager, array $users, stdClass $properties) { + global $DB, $USER; + + if (!has_capability("enrol/spoken:manage", $manager->get_context())) { + return false; + } + + // Get all of the user enrolment id's. + $ueids = array(); + $instances = array(); + foreach ($users as $user) { + foreach ($user->enrolments as $enrolment) { + $ueids[] = $enrolment->id; + if (!array_key_exists($enrolment->id, $instances)) { + $instances[$enrolment->id] = $enrolment; + } + } + } + + // Check that each instance is manageable by the current user. + foreach ($instances as $instance) { + if (!$this->plugin->allow_manage($instance)) { + return false; + } + } + + // Collect the known properties. + $status = $properties->status; + $timestart = $properties->timestart; + $timeend = $properties->timeend; + + list($ueidsql, $params) = $DB->get_in_or_equal($ueids, SQL_PARAMS_NAMED); + + $updatesql = array(); + if ($status == ENROL_USER_ACTIVE || $status == ENROL_USER_SUSPENDED) { + $updatesql[] = 'status = :status'; + $params['status'] = (int)$status; + } + if (!empty($timestart)) { + $updatesql[] = 'timestart = :timestart'; + $params['timestart'] = (int)$timestart; + } + if (!empty($timeend)) { + $updatesql[] = 'timeend = :timeend'; + $params['timeend'] = (int)$timeend; + } + if (empty($updatesql)) { + return true; + } + + // Update the modifierid. + $updatesql[] = 'modifierid = :modifierid'; + $params['modifierid'] = (int)$USER->id; + + // Update the time modified. + $updatesql[] = 'timemodified = :timemodified'; + $params['timemodified'] = time(); + + // Build the SQL statement. + $updatesql = join(', ', $updatesql); + $sql = "UPDATE {user_enrolments} + SET $updatesql + WHERE id $ueidsql"; + + if ($DB->execute($sql, $params)) { + foreach ($users as $user) { + foreach ($user->enrolments as $enrolment) { + $enrolment->courseid = $enrolment->enrolmentinstance->courseid; + $enrolment->enrol = 'spoken'; + // Trigger event. + $event = \core\event\user_enrolment_updated::create( + array( + 'objectid' => $enrolment->id, + 'courseid' => $enrolment->courseid, + 'context' => context_course::instance($enrolment->courseid), + 'relateduserid' => $user->id, + 'other' => array('enrol' => 'spoken') + ) + ); + $event->trigger(); + } + } + // Delete cached course contacts for this course because they may be affected. + cache::make('core', 'coursecontacts')->delete($manager->get_context()->instanceid); + return true; + } + + return false; + } + + /** + * Returns a enrol_bulk_enrolment_operation extension form to be used + * in collecting required information for this operation to be processed. + * + * @param string|moodle_url|null $defaultaction + * @param mixed $defaultcustomdata + * @return enrol_spoken_editselectedusers_form + */ + public function get_form($defaultaction = null, $defaultcustomdata = null) { + return new enrol_spoken_editselectedusers_form($defaultaction, $defaultcustomdata); + } +} diff --git a/enrol/spoken/classes/empty_form.php b/enrol/spoken/classes/empty_form.php new file mode 100644 index 00000000..3835270f --- /dev/null +++ b/enrol/spoken/classes/empty_form.php @@ -0,0 +1,41 @@ +. + +/** + * Empty enrol_spoken form. + * + * Useful to mimic valid enrol instances UI when the enrolment instance is not available. + * + * @package enrol_spoken + * @copyright 2015 David Monllaó + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/formslib.php'); + +class enrol_spoken_empty_form extends moodleform { + + /** + * Form definition. + * @return void + */ + public function definition() { + $this->_form->addElement('header', 'spokenheader', $this->_customdata->header); + $this->_form->addElement('static', 'info', '', $this->_customdata->info); + } +} diff --git a/enrol/spoken/classes/privacy/provider.php b/enrol/spoken/classes/privacy/provider.php new file mode 100644 index 00000000..dda50f69 --- /dev/null +++ b/enrol/spoken/classes/privacy/provider.php @@ -0,0 +1,41 @@ +. +/** + * Privacy Subsystem implementation for enrol_spoken. + * + * @package enrol_spoken + * @copyright 2018 Carlos Escobedo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace enrol_spoken\privacy; +defined('MOODLE_INTERNAL') || die(); +/** + * Privacy Subsystem for enrol_spoken implementing null_provider. + * + * @copyright 2018 Carlos Escobedo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/enrol/spoken/classes/task/send_expiry_notifications.php b/enrol/spoken/classes/task/send_expiry_notifications.php new file mode 100644 index 00000000..58da01a6 --- /dev/null +++ b/enrol/spoken/classes/task/send_expiry_notifications.php @@ -0,0 +1,58 @@ +. + +/** + * Send expiry notifications task. + * + * @package enrol_spoken + * @author Farhan Karmali + * @copyright Farhan Karmali + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace enrol_spoken\task; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Send expiry notifications task. + * + * @package enrol_spoken + * @author Farhan Karmali + * @copyright Farhan Karmali + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class send_expiry_notifications extends \core\task\scheduled_task { + + /** + * Name for this task. + * + * @return string + */ + public function get_name() { + return get_string('sendexpirynotificationstask', 'enrol_spoken'); + } + + /** + * Run task for sending expiry notifications. + */ + public function execute() { + $enrol = enrol_get_plugin('spoken'); + $trace = new \text_progress_trace(); + $enrol->send_expiry_notifications($trace); + } + +} diff --git a/enrol/spoken/classes/task/sync_enrolments.php b/enrol/spoken/classes/task/sync_enrolments.php new file mode 100644 index 00000000..dd31fb98 --- /dev/null +++ b/enrol/spoken/classes/task/sync_enrolments.php @@ -0,0 +1,58 @@ +. + +/** + * Sync enrolments task. + * + * @package enrol_spoken + * @author Farhan Karmali + * @copyright Farhan Karmali + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace enrol_spoken\task; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Sync enrolments task. + * + * @package enrol_spoken + * @author Farhan Karmali + * @copyright Farhan Karmali + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sync_enrolments extends \core\task\scheduled_task { + + /** + * Name for this task. + * + * @return string + */ + public function get_name() { + return get_string('syncenrolmentstask', 'enrol_spoken'); + } + + /** + * Run task for syncing enrolments. + */ + public function execute() { + $enrol = enrol_get_plugin('spoken'); + $trace = new \text_progress_trace(); + $enrol->sync($trace); + } + +} diff --git a/enrol/spoken/cli/sync.php b/enrol/spoken/cli/sync.php new file mode 100644 index 00000000..2fb322bc --- /dev/null +++ b/enrol/spoken/cli/sync.php @@ -0,0 +1,76 @@ +. + +/** + * CLI update for spoken enrolments, use for debugging or immediate update + * of all courses. + * + * Notes: + * - it is required to use the web server account when executing PHP CLI scripts + * - you need to change the "www-data" to match the apache user account + * - use "su" if "sudo" not available + * + * @package enrol_spoken + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('CLI_SCRIPT', true); + +require(__DIR__.'/../../../config.php'); +require_once("$CFG->libdir/clilib.php"); + +// Now get cli options. +list($options, $unrecognized) = cli_get_params(array('verbose'=>false, 'help'=>false), array('v'=>'verbose', 'h'=>'help')); + +if ($unrecognized) { + $unrecognized = implode("\n ", $unrecognized); + cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); +} + +if ($options['help']) { + $help = + "Execute spoken course enrol updates. + +Options: +-v, --verbose Print verbose progress information +-h, --help Print out this help + +Example: +\$ sudo -u www-data /usr/bin/php enrol/spoken/cli/sync.php +"; + + echo $help; + die; +} + +if (!enrol_is_enabled('spoken')) { + cli_error('enrol_spoken plugin is disabled, synchronisation stopped', 2); +} + +if (empty($options['verbose'])) { + $trace = new null_progress_trace(); +} else { + $trace = new text_progress_trace(); +} + +/** @var $plugin enrol_spoken_plugin */ +$plugin = enrol_get_plugin('spoken'); + +$result = $plugin->sync($trace, null); +$plugin->send_expiry_notifications($trace); + +exit($result); diff --git a/enrol/spoken/db/access.php b/enrol/spoken/db/access.php new file mode 100644 index 00000000..8659116b --- /dev/null +++ b/enrol/spoken/db/access.php @@ -0,0 +1,85 @@ +. + +/** + * Capabilities for spoken enrolment plugin. + * + * @package enrol_spoken + * @copyright 2010 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + /* Add or edit enrol-spoken instance in course. */ + 'enrol/spoken:config' => array( + + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW, + ) + ), + + /* Manage user spoken-enrolments. */ + 'enrol/spoken:manage' => array( + + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW, + ) + ), + + 'enrol/spoken:holdkey' => array( + + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + ), + + /* Voluntarily unenrol spoken from course - watch out for data loss. */ + 'enrol/spoken:unenrolspoken' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'student' => CAP_ALLOW, + ) + ), + + /* Unenrol anybody from course (including spoken) - watch out for data loss. */ + 'enrol/spoken:unenrol' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW, + ) + ), + + /* Ability to enrol spoken in courses. */ + 'enrol/spoken:enrolspoken' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'user' => CAP_ALLOW, + ) + ), + +); diff --git a/enrol/spoken/db/install.php b/enrol/spoken/db/install.php new file mode 100644 index 00000000..d9a952ab --- /dev/null +++ b/enrol/spoken/db/install.php @@ -0,0 +1,29 @@ +. + +/** + * spoken enrol plugin installation script + * + * @package enrol_spoken + * @copyright 2010 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +function xmldb_enrol_spoken_install() { + global $CFG, $DB; + +} diff --git a/enrol/spoken/db/messages.php b/enrol/spoken/db/messages.php new file mode 100644 index 00000000..7eebdd00 --- /dev/null +++ b/enrol/spoken/db/messages.php @@ -0,0 +1,29 @@ +. + +/** + * Defines message providers for spoken enrolments. + * + * @package enrol_spoken + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$messageproviders = array ( + + 'expiry_notification' => array(), + +); diff --git a/enrol/spoken/db/services.php b/enrol/spoken/db/services.php new file mode 100644 index 00000000..f3b49b6e --- /dev/null +++ b/enrol/spoken/db/services.php @@ -0,0 +1,44 @@ +. + +/** + * spoken enrol plugin external functions and service definitions. + * + * @package enrol_spoken + * @copyright 2013 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.6 + */ + +$functions = array( + 'enrol_spoken_get_instance_info' => array( + 'classname' => 'enrol_spoken_external', + 'methodname' => 'get_instance_info', + 'classpath' => 'enrol/spoken/externallib.php', + 'description' => 'spoken enrolment instance information.', + 'type' => 'read', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ), + + 'enrol_spoken_enrol_user' => array( + 'classname' => 'enrol_spoken_external', + 'methodname' => 'enrol_user', + 'classpath' => 'enrol/spoken/externallib.php', + 'description' => 'spoken enrol the current user in the given course.', + 'type' => 'write', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ) +); diff --git a/enrol/spoken/db/tasks.php b/enrol/spoken/db/tasks.php new file mode 100644 index 00000000..32bc8a5a --- /dev/null +++ b/enrol/spoken/db/tasks.php @@ -0,0 +1,48 @@ +. + +/** + * Task definition for enrol_spoken. + * @author Farhan Karmali + * @copyright Farhan Karmali + * @package enrol_spoken + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$tasks = array( + array( + 'classname' => '\enrol_spoken\task\sync_enrolments', + 'blocking' => 0, + 'minute' => '*/10', + 'hour' => '*', + 'day' => '*', + 'month' => '*', + 'dayofweek' => '*', + 'disabled' => 0 + ), + array( + 'classname' => '\enrol_spoken\task\send_expiry_notifications', + 'blocking' => 0, + 'minute' => '*/10', + 'hour' => '*', + 'day' => '*', + 'month' => '*', + 'dayofweek' => '*', + 'disabled' => 0 + ) +); diff --git a/enrol/spoken/db/upgrade.php b/enrol/spoken/db/upgrade.php new file mode 100644 index 00000000..1895763f --- /dev/null +++ b/enrol/spoken/db/upgrade.php @@ -0,0 +1,49 @@ +. + +/** + * This file keeps track of upgrades to the spoken enrolment plugin + * + * @package enrol_spoken + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +function xmldb_enrol_spoken_upgrade($oldversion) { + global $CFG; + + // Automatically generated Moodle v3.5.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.6.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.7.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.8.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.9.0 release upgrade line. + // Put any upgrade step following this. + + // Automatically generated Moodle v3.10.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/enrol/spoken/externallib.php b/enrol/spoken/externallib.php new file mode 100644 index 00000000..f046d1ee --- /dev/null +++ b/enrol/spoken/externallib.php @@ -0,0 +1,257 @@ +. + +/** + * spoken enrol plugin external functions + * + * @package enrol_spoken + * @copyright 2013 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once("$CFG->libdir/externallib.php"); + +/** + * spoken enrolment external functions. + * + * @package enrol_spoken + * @copyright 2012 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.6 + */ +class enrol_spoken_external extends external_api { + + /** + * Returns description of get_instance_info() parameters. + * + * @return external_function_parameters + */ + public static function get_instance_info_parameters() { + return new external_function_parameters( + array('instanceid' => new external_value(PARAM_INT, 'instance id of spoken enrolment plugin.')) + ); + } + + /** + * Return spoken-enrolment instance information. + * + * @param int $instanceid instance id of spoken enrolment plugin. + * @return array instance information. + * @throws moodle_exception + */ + public static function get_instance_info($instanceid) { + global $DB, $CFG; + + require_once($CFG->libdir . '/enrollib.php'); + + $params = self::validate_parameters(self::get_instance_info_parameters(), array('instanceid' => $instanceid)); + + // Retrieve spoken enrolment plugin. + $enrolplugin = enrol_get_plugin('spoken'); + if (empty($enrolplugin)) { + throw new moodle_exception('invaliddata', 'error'); + } + + self::validate_context(context_system::instance()); + + $enrolinstance = $DB->get_record('enrol', array('id' => $params['instanceid']), '*', MUST_EXIST); + $course = $DB->get_record('course', array('id' => $enrolinstance->courseid), '*', MUST_EXIST); + if (!core_course_category::can_view_course_info($course) && !can_access_course($course)) { + throw new moodle_exception('coursehidden'); + } + + $instanceinfo = (array) $enrolplugin->get_enrol_info($enrolinstance); + if (isset($instanceinfo['requiredparam']->enrolpassword)) { + $instanceinfo['enrolpassword'] = $instanceinfo['requiredparam']->enrolpassword; + } + unset($instanceinfo->requiredparam); + + return $instanceinfo; + } + + /** + * Returns description of get_instance_info() result value. + * + * @return external_description + */ + public static function get_instance_info_returns() { + return new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'id of course enrolment instance'), + 'courseid' => new external_value(PARAM_INT, 'id of course'), + 'type' => new external_value(PARAM_PLUGIN, 'type of enrolment plugin'), + 'name' => new external_value(PARAM_RAW, 'name of enrolment plugin'), + 'status' => new external_value(PARAM_RAW, 'status of enrolment plugin'), + 'enrolpassword' => new external_value(PARAM_RAW, 'password required for enrolment', VALUE_OPTIONAL), + ) + ); + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 3.0 + */ + public static function enrol_user_parameters() { + return new external_function_parameters( + array( + 'courseid' => new external_value(PARAM_INT, 'Id of the course'), + 'password' => new external_value(PARAM_RAW, 'Enrolment key', VALUE_DEFAULT, ''), + 'instanceid' => new external_value(PARAM_INT, 'Instance id of spoken enrolment plugin.', VALUE_DEFAULT, 0) + ) + ); + } + + /** + * spoken enrol the current user in the given course. + * + * @param int $courseid id of course + * @param string $password enrolment key + * @param int $instanceid instance id of spoken enrolment plugin + * @return array of warnings and status result + * @since Moodle 3.0 + * @throws moodle_exception + */ + public static function enrol_user($courseid, $password = '', $instanceid = 0) { + global $CFG; + + require_once($CFG->libdir . '/enrollib.php'); + + $params = self::validate_parameters(self::enrol_user_parameters(), + array( + 'courseid' => $courseid, + 'password' => $password, + 'instanceid' => $instanceid + )); + + $warnings = array(); + + $course = get_course($params['courseid']); + $context = context_course::instance($course->id); + self::validate_context(context_system::instance()); + + if (!core_course_category::can_view_course_info($course)) { + throw new moodle_exception('coursehidden'); + } + + // Retrieve the spoken enrolment plugin. + $enrol = enrol_get_plugin('spoken'); + if (empty($enrol)) { + throw new moodle_exception('canntenrol', 'enrol_spoken'); + } + + // We can expect multiple spoken-enrolment instances. + $instances = array(); + $enrolinstances = enrol_get_instances($course->id, true); + foreach ($enrolinstances as $courseenrolinstance) { + if ($courseenrolinstance->enrol == "spoken") { + // Instance specified. + if (!empty($params['instanceid'])) { + if ($courseenrolinstance->id == $params['instanceid']) { + $instances[] = $courseenrolinstance; + break; + } + } else { + $instances[] = $courseenrolinstance; + } + + } + } + if (empty($instances)) { + throw new moodle_exception('canntenrol', 'enrol_spoken'); + } + + // Try to enrol the user in the instance/s. + $enrolled = false; + foreach ($instances as $instance) { + $enrolstatus = $enrol->can_spoken_enrol($instance); + if ($enrolstatus === true) { + if ($instance->password and $params['password'] !== $instance->password) { + + // Check if we are using group enrolment keys. + if ($instance->customint1) { + require_once($CFG->dirroot . "/enrol/spoken/locallib.php"); + + if (!enrol_spoken_check_group_enrolment_key($course->id, $params['password'])) { + $warnings[] = array( + 'item' => 'instance', + 'itemid' => $instance->id, + 'warningcode' => '2', + 'message' => get_string('passwordinvalid', 'enrol_spoken') + ); + continue; + } + } else { + if ($enrol->get_config('showhint')) { + $hint = core_text::substr($instance->password, 0, 1); + $warnings[] = array( + 'item' => 'instance', + 'itemid' => $instance->id, + 'warningcode' => '3', + 'message' => s(get_string('passwordinvalidhint', 'enrol_spoken', $hint)) // message is PARAM_TEXT. + ); + continue; + } else { + $warnings[] = array( + 'item' => 'instance', + 'itemid' => $instance->id, + 'warningcode' => '4', + 'message' => get_string('passwordinvalid', 'enrol_spoken') + ); + continue; + } + } + } + + // Do the enrolment. + $data = array('enrolpassword' => $params['password']); + $enrol->enrol_spoken($instance, (object) $data); + $enrolled = true; + break; + } else { + $warnings[] = array( + 'item' => 'instance', + 'itemid' => $instance->id, + 'warningcode' => '1', + 'message' => $enrolstatus + ); + } + } + + $result = array(); + $result['status'] = $enrolled; + $result['warnings'] = $warnings; + return $result; + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 3.0 + */ + public static function enrol_user_returns() { + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'status: true if the user is enrolled, false otherwise'), + 'warnings' => new external_warnings() + ) + ); + } +} diff --git a/enrol/spoken/lang/en/enrol_spoken.php b/enrol/spoken/lang/en/enrol_spoken.php new file mode 100644 index 00000000..de6edfd8 --- /dev/null +++ b/enrol/spoken/lang/en/enrol_spoken.php @@ -0,0 +1,127 @@ +. + +/** + * Strings for component 'enrol_spoken', language 'en'. + * + * @package enrol_spoken + * @copyright 2010 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['canntenrol'] = 'Enrolment is disabled or inactive'; +$string['canntenrolearly'] = 'You cannot enrol yet; enrolment starts on {$a}.'; +$string['canntenrollate'] = 'You cannot enrol any more, since enrolment ended on {$a}.'; +$string['cohortnonmemberinfo'] = 'Only members of cohort \'{$a}\' can spoken-enrol.'; +$string['cohortonly'] = 'Only cohort members'; +$string['cohortonly_help'] = 'spoken enrolment may be restricted to members of a specified cohort only. Note that changing this setting has no effect on existing enrolments.'; +$string['confirmbulkdeleteenrolment'] = 'Are you sure you want to delete these user enrolments?'; +$string['customwelcomemessage'] = 'Custom welcome message'; +$string['customwelcomemessage_help'] = 'A custom welcome message may be added as plain text or Moodle-auto format, including HTML tags and multi-lang tags. + +The following placeholders may be included in the message: + +* Course name {$a->coursename} +* Link to user\'s profile page {$a->profileurl} +* User email {$a->email} +* User fullname {$a->fullname}'; +$string['defaultrole'] = 'Default role assignment'; +$string['defaultrole_desc'] = 'Select role which should be assigned to users during spoken enrolment'; +$string['deleteselectedusers'] = 'Delete selected user enrolments'; +$string['editselectedusers'] = 'Edit selected user enrolments'; +$string['enrolenddate'] = 'End date'; +$string['enrolenddate_help'] = 'If enabled, users can enrol themselves until this date only.'; +$string['enrolenddaterror'] = 'Enrolment end date cannot be earlier than start date'; +$string['enrolme'] = 'Enrol me'; +$string['enrolperiod'] = 'Enrolment duration'; +$string['enrolperiod_desc'] = 'Default length of time that the enrolment is valid. If set to zero, the enrolment duration will be unlimited by default.'; +$string['enrolperiod_help'] = 'Length of time that the enrolment is valid, starting with the moment the user enrols themselves. If disabled, the enrolment duration will be unlimited.'; +$string['enrolstartdate'] = 'Start date'; +$string['enrolstartdate_help'] = 'If enabled, users can enrol themselves from this date onward only.'; +$string['expiredaction'] = 'Enrolment expiry action'; +$string['expiredaction_help'] = 'Select action to carry out when user enrolment expires. Please note that some user data and settings are purged from course during course unenrolment.'; +$string['expirymessageenrollersubject'] = 'spoken enrolment expiry notification'; +$string['expirymessageenrollerbody'] = 'spoken enrolment in the course \'{$a->course}\' will expire within the next {$a->threshold} for the following users: + +{$a->users} + +To extend their enrolment, go to {$a->extendurl}'; +$string['expirymessageenrolledsubject'] = 'spoken enrolment expiry notification'; +$string['expirymessageenrolledbody'] = 'Dear {$a->user}, + +This is a notification that your enrolment in the course \'{$a->course}\' is due to expire on {$a->timeend}. + +If you need help, please contact {$a->enroller}.'; +$string['expirynotifyall'] = 'Teacher and enrolled user'; +$string['expirynotifyenroller'] = 'Teacher only'; +$string['groupkey'] = 'Use group enrolment keys'; +$string['groupkey_desc'] = 'Use group enrolment keys by default.'; +$string['groupkey_help'] = 'In addition to restricting access to the course to only those who know the key, use of group enrolment keys means users are automatically added to groups when they enrol in the course. + +Note: An enrolment key for the course must be specified in the spoken enrolment settings as well as group enrolment keys in the group settings.'; +$string['keyholder'] = 'You should have received this enrolment key from:'; +$string['longtimenosee'] = 'Unenrol inactive after'; +$string['longtimenosee_help'] = 'If users haven\'t accessed a course for a long time, then they are automatically unenrolled. This parameter specifies that time limit.'; +$string['maxenrolled'] = 'Max enrolled users'; +$string['maxenrolled_help'] = 'Specifies the maximum number of users that can spoken enrol. 0 means no limit.'; +$string['maxenrolledreached'] = 'Maximum number of users allowed to spoken-enrol was already reached.'; +$string['messageprovider:expiry_notification'] = 'spoken enrolment expiry notifications'; +$string['newenrols'] = 'Allow new enrolments'; +$string['newenrols_desc'] = 'Allow users to spoken enrol into new courses by default.'; +$string['newenrols_help'] = 'This setting determines whether a user can enrol into this course.'; +$string['nopassword'] = 'No enrolment key required.'; +$string['password'] = 'Enrolment key'; +$string['password_help'] = 'An enrolment key enables access to the course to be restricted to only those who know the key. + +If the field is left blank, any user may enrol in the course. + +If an enrolment key is specified, any user attempting to enrol in the course will be required to supply the key. Note that a user only needs to supply the enrolment key ONCE, when they enrol in the course.'; +$string['passwordinvalid'] = 'Incorrect enrolment key, please try again'; +$string['passwordinvalidhint'] = 'That enrolment key was incorrect, please try again
    +(Here\'s a hint - it starts with \'{$a}\')'; +$string['pluginname'] = 'spoken enrolment'; +$string['pluginname_desc'] = 'The spoken enrolment plugin allows users to choose which courses they want to participate in. The courses may be protected by an enrolment key. Internally the enrolment is done via the manual enrolment plugin which has to be enabled in the same course.'; +$string['requirepassword'] = 'Require enrolment key'; +$string['requirepassword_desc'] = 'Require enrolment key in new courses and prevent removing of enrolment key from existing courses.'; +$string['role'] = 'Default assigned role'; +$string['spoken:config'] = 'Configure spoken enrol instances'; +$string['spoken:enrolspoken'] = 'spoken enrol in course'; +$string['spoken:holdkey'] = 'Appear as the spoken enrolment key holder'; +$string['spoken:manage'] = 'Manage enrolled users'; +$string['spoken:unenrol'] = 'Unenrol users from course'; +$string['spoken:unenrolspoken'] = 'Unenrol spoken from the course'; +$string['sendcoursewelcomemessage'] = 'Send course welcome message'; +$string['sendcoursewelcomemessage_help'] = 'When a user spoken enrols in the course, they may be sent a welcome message email. If sent from the course contact (by default the teacher), and more than one user has this role, the email is sent from the first user to be assigned the role.'; +$string['sendexpirynotificationstask'] = "spoken enrolment send expiry notifications task"; +$string['showhint'] = 'Show hint'; +$string['showhint_desc'] = 'Show first letter of the guest access key.'; +$string['status'] = 'Allow existing enrolments'; +$string['status_desc'] = 'Enable spoken enrolment method in new courses.'; +$string['status_help'] = 'If enabled together with \'Allow new enrolments\' disabled, only users who spoken enrolled previously can access the course. If disabled, this spoken enrolment method is effectively disabled, since all existing spoken enrolments are suspended and new users cannot spoken enrol.'; +$string['syncenrolmentstask'] = 'Synchronise spoken enrolments task'; +$string['unenrol'] = 'Unenrol user'; +$string['unenrolspokenconfirm'] = 'Do you really want to unenrol yourspoken from course "{$a}"?'; +$string['unenroluser'] = 'Do you really want to unenrol "{$a->user}" from course "{$a->course}"?'; +$string['unenrolusers'] = 'Unenrol users'; +$string['usepasswordpolicy'] = 'Use password policy'; +$string['usepasswordpolicy_desc'] = 'Use standard password policy for enrolment keys.'; +$string['welcometocourse'] = 'Welcome to {$a}'; +$string['welcometocoursetext'] = 'Welcome to {$a->coursename}! + +If you have not done so already, you should edit your profile page so that we can learn more about you: + + {$a->profileurl}'; +$string['privacy:metadata'] = 'The spoken enrolment plugin does not store any personal data.'; diff --git a/enrol/spoken/lib.php b/enrol/spoken/lib.php new file mode 100644 index 00000000..5c4c46ba --- /dev/null +++ b/enrol/spoken/lib.php @@ -0,0 +1,1085 @@ +. + +/** + * spoken enrolment plugin. + * + * @package enrol_spoken + * @copyright 2010 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * spoken enrolment plugin implementation. + * @author Petr Skoda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class enrol_spoken_plugin extends enrol_plugin { + + protected $lasternoller = null; + protected $lasternollerinstanceid = 0; + + /** + * Returns optional enrolment information icons. + * + * This is used in course list for quick overview of enrolment options. + * + * We are not using single instance parameter because sometimes + * we might want to prevent icon repetition when multiple instances + * of one type exist. One instance may also produce several icons. + * + * @param array $instances all enrol instances of this type in one course + * @return array of pix_icon + */ + public function get_info_icons(array $instances) { + $key = false; + $nokey = false; + foreach ($instances as $instance) { + if ($this->can_spoken_enrol($instance, false) !== true) { + // User can not enrol himspoken. + // Note that we do not check here if user is already enrolled for performance reasons - + // such check would execute extra queries for each course in the list of courses and + // would hide spoken-enrolment icons from guests. + continue; + } + if ($instance->password or $instance->customint1) { + $key = true; + } else { + $nokey = true; + } + } + $icons = array(); + if ($nokey) { + $icons[] = new pix_icon('withoutkey', get_string('pluginname', 'enrol_spoken'), 'enrol_spoken'); + } + if ($key) { + $icons[] = new pix_icon('withkey', get_string('pluginname', 'enrol_spoken'), 'enrol_spoken'); + } + return $icons; + } + + /** + * Returns localised name of enrol instance + * + * @param stdClass $instance (null is accepted too) + * @return string + */ + public function get_instance_name($instance) { + global $DB; + + if (empty($instance->name)) { + if (!empty($instance->roleid) and $role = $DB->get_record('role', array('id'=>$instance->roleid))) { + $role = ' (' . role_get_name($role, context_course::instance($instance->courseid, IGNORE_MISSING)) . ')'; + } else { + $role = ''; + } + $enrol = $this->get_name(); + return get_string('pluginname', 'enrol_'.$enrol) . $role; + } else { + return format_string($instance->name); + } + } + + public function roles_protected() { + // Users may tweak the roles later. + return false; + } + + public function allow_unenrol(stdClass $instance) { + // Users with unenrol cap may unenrol other users manually manually. + return true; + } + + public function allow_manage(stdClass $instance) { + // Users with manage cap may tweak period and status. + return true; + } + + public function show_enrolme_link(stdClass $instance) { + + if (true !== $this->can_spoken_enrol($instance, false)) { + return false; + } + + return true; + } + + /** + * Return true if we can add a new instance to this course. + * + * @param int $courseid + * @return boolean + */ + public function can_add_instance($courseid) { + $context = context_course::instance($courseid, MUST_EXIST); + + if (!has_capability('moodle/course:enrolconfig', $context) or !has_capability('enrol/spoken:config', $context)) { + return false; + } + + return true; + } + + /** + * spoken enrol user to course + * + * @param stdClass $instance enrolment instance + * @param stdClass $data data needed for enrolment. + * @return bool|array true if enroled else eddor code and messege + */ + public function enrol_spoken(stdClass $instance, $data = null) { + global $DB, $USER, $CFG; + + // Don't enrol user if password is not passed when required. + if ($instance->password && !isset($data->enrolpassword)) { + return; + } + + $timestart = time(); + if ($instance->enrolperiod) { + $timeend = $timestart + $instance->enrolperiod; + } else { + $timeend = 0; + } + + $this->enrol_user($instance, $USER->id, $instance->roleid, $timestart, $timeend); + + \core\notification::success(get_string('youenrolledincourse', 'enrol')); + + if ($instance->password and $instance->customint1 and $data->enrolpassword !== $instance->password) { + // It must be a group enrolment, let's assign group too. + $groups = $DB->get_records('groups', array('courseid'=>$instance->courseid), 'id', 'id, enrolmentkey'); + foreach ($groups as $group) { + if (empty($group->enrolmentkey)) { + continue; + } + if ($group->enrolmentkey === $data->enrolpassword) { + // Add user to group. + require_once($CFG->dirroot.'/group/lib.php'); + groups_add_member($group->id, $USER->id); + break; + } + } + } + // Send welcome message. + if ($instance->customint4 != ENROL_DO_NOT_SEND_EMAIL) { + $this->email_welcome_message($instance, $USER); + } + } + + /** + * Creates course enrol form, checks if form submitted + * and enrols user if necessary. It can also redirect. + * + * @param stdClass $instance + * @return string html text, usually a form in a text box + */ + public function enrol_page_hook(stdClass $instance) { + + global $DB, $CFG, $OUTPUT, $USER; + + require_once("$CFG->dirroot/enrol/spoken/locallib.php"); + + $enrolstatus = $this->can_spoken_enrol($instance); + + if (true === $enrolstatus) { + // This user can spoken enrol using this instance. + $form = new enrol_spoken_enrol_form(null, $instance); + $instanceid = optional_param('instance', 0, PARAM_INT); + if ($instance->id == $instanceid) { + if ($data = $form->get_data()) { + $this->enrol_spoken($instance, $data); + } + } + } else { + // This user can not spoken enrol using this instance. Using an empty form to keep + // the UI consistent with other enrolment plugins that returns a form. + $data = new stdClass(); + $data->header = $this->get_instance_name($instance); + $data->info = $enrolstatus; + + // The can_spoken_enrol call returns a button to the login page if the user is a + // guest, setting the login url to the form if that is the case. + $url = isguestuser() ? get_login_url() : null; + $form = new enrol_spoken_empty_form($url, $data); + } + + // @custom create enrollment and redirect to course from here + // + if (!$enrol = enrol_get_plugin('spoken')) { + return false; + } + + + // write spoken queries + require_once($CFG->dirroot.'/mod/quiz/spoken-config.php'); + $sql = "select id from training_eventteststatus where mdlemail = '".$USER->email."' and mdlcourse_id = ".$instance->courseid." and part_status >= 0"; + $result = $mysqli->query($sql); + $count = $result->num_rows; + if ($count) { + $enrol->enrol_user($instance, $USER->id, 5, 0, 0); // 5 => role student role id + } else { + return get_string('canntenrol', 'enrol_spoken'); + } + // @custom + + + ob_start(); + $form->display(); + $output = ob_get_clean(); + return $OUTPUT->box($output); + } + + /** + * Checks if user can spoken enrol. + * + * @param stdClass $instance enrolment instance + * @param bool $checkuserenrolment if true will check if user enrolment is inactive. + * used by navigation to improve performance. + * @return bool|string true if successful, else error message or false. + */ + public function can_spoken_enrol(stdClass $instance, $checkuserenrolment = true) { + global $CFG, $DB, $OUTPUT, $USER; + + if ($checkuserenrolment) { + if (isguestuser()) { + // Can not enrol guest. + return get_string('noguestaccess', 'enrol') . $OUTPUT->continue_button(get_login_url()); + } + // Check if user is already enroled. + if ($DB->get_record('user_enrolments', array('userid' => $USER->id, 'enrolid' => $instance->id))) { + return get_string('canntenrol', 'enrol_spoken'); + } + } + + if ($instance->status != ENROL_INSTANCE_ENABLED) { + return get_string('canntenrol', 'enrol_spoken'); + } + + // Check if user has the capability to enrol in this context. + if (!has_capability('enrol/spoken:enrolspoken', context_course::instance($instance->courseid))) { + return get_string('canntenrol', 'enrol_spoken'); + } + + if ($instance->enrolstartdate != 0 and $instance->enrolstartdate > time()) { + return get_string('canntenrolearly', 'enrol_spoken', userdate($instance->enrolstartdate)); + } + + if ($instance->enrolenddate != 0 and $instance->enrolenddate < time()) { + return get_string('canntenrollate', 'enrol_spoken', userdate($instance->enrolenddate)); + } + + if (!$instance->customint6) { + // New enrols not allowed. + return get_string('canntenrol', 'enrol_spoken'); + } + + if ($DB->record_exists('user_enrolments', array('userid' => $USER->id, 'enrolid' => $instance->id))) { + return get_string('canntenrol', 'enrol_spoken'); + } + + if ($instance->customint3 > 0) { + // Max enrol limit specified. + $count = $DB->count_records('user_enrolments', array('enrolid' => $instance->id)); + if ($count >= $instance->customint3) { + // Bad luck, no more spoken enrolments here. + return get_string('maxenrolledreached', 'enrol_spoken'); + } + } + + if ($instance->customint5) { + require_once("$CFG->dirroot/cohort/lib.php"); + if (!cohort_is_member($instance->customint5, $USER->id)) { + $cohort = $DB->get_record('cohort', array('id' => $instance->customint5)); + if (!$cohort) { + return null; + } + $a = format_string($cohort->name, true, array('context' => context::instance_by_id($cohort->contextid))); + return markdown_to_html(get_string('cohortnonmemberinfo', 'enrol_spoken', $a)); + } + } + + // @sanmugam: We can write the spoken login here + // print_r($instance); + // return get_string('canntenrol', 'enrol_spoken'); + // die; + return true; + } + + /** + * Return information for enrolment instance containing list of parameters required + * for enrolment, name of enrolment plugin etc. + * + * @param stdClass $instance enrolment instance + * @return stdClass instance info. + */ + public function get_enrol_info(stdClass $instance) { + + $instanceinfo = new stdClass(); + $instanceinfo->id = $instance->id; + $instanceinfo->courseid = $instance->courseid; + $instanceinfo->type = $this->get_name(); + $instanceinfo->name = $this->get_instance_name($instance); + $instanceinfo->status = $this->can_spoken_enrol($instance); + + if ($instance->password) { + $instanceinfo->requiredparam = new stdClass(); + $instanceinfo->requiredparam->enrolpassword = get_string('password', 'enrol_spoken'); + } + + // If enrolment is possible and password is required then return ws function name to get more information. + if ((true === $instanceinfo->status) && $instance->password) { + $instanceinfo->wsfunction = 'enrol_spoken_get_instance_info'; + } + return $instanceinfo; + } + + /** + * Add new instance of enrol plugin with default settings. + * @param stdClass $course + * @return int id of new instance + */ + public function add_default_instance($course) { + $fields = $this->get_instance_defaults(); + + if ($this->get_config('requirepassword')) { + $fields['password'] = generate_password(20); + } + + return $this->add_instance($course, $fields); + } + + /** + * Returns defaults for new instances. + * @return array + */ + public function get_instance_defaults() { + $expirynotify = $this->get_config('expirynotify'); + if ($expirynotify == 2) { + $expirynotify = 1; + $notifyall = 1; + } else { + $notifyall = 0; + } + + $fields = array(); + $fields['status'] = $this->get_config('status'); + $fields['roleid'] = $this->get_config('roleid'); + $fields['enrolperiod'] = $this->get_config('enrolperiod'); + $fields['expirynotify'] = $expirynotify; + $fields['notifyall'] = $notifyall; + $fields['expirythreshold'] = $this->get_config('expirythreshold'); + $fields['customint1'] = $this->get_config('groupkey'); + $fields['customint2'] = $this->get_config('longtimenosee'); + $fields['customint3'] = $this->get_config('maxenrolled'); + $fields['customint4'] = $this->get_config('sendcoursewelcomemessage'); + $fields['customint5'] = 0; + $fields['customint6'] = $this->get_config('newenrols'); + + return $fields; + } + + /** + * Send welcome email to specified user. + * + * @param stdClass $instance + * @param stdClass $user user record + * @return void + */ + protected function email_welcome_message($instance, $user) { + global $CFG, $DB; + + $course = $DB->get_record('course', array('id'=>$instance->courseid), '*', MUST_EXIST); + $context = context_course::instance($course->id); + + $a = new stdClass(); + $a->coursename = format_string($course->fullname, true, array('context'=>$context)); + $a->profileurl = "$CFG->wwwroot/user/view.php?id=$user->id&course=$course->id"; + + if (trim($instance->customtext1) !== '') { + $message = $instance->customtext1; + $key = array('{$a->coursename}', '{$a->profileurl}', '{$a->fullname}', '{$a->email}'); + $value = array($a->coursename, $a->profileurl, fullname($user), $user->email); + $message = str_replace($key, $value, $message); + if (strpos($message, '<') === false) { + // Plain text only. + $messagetext = $message; + $messagehtml = text_to_html($messagetext, null, false, true); + } else { + // This is most probably the tag/newline soup known as FORMAT_MOODLE. + $messagehtml = format_text($message, FORMAT_MOODLE, array('context'=>$context, 'para'=>false, 'newlines'=>true, 'filter'=>true)); + $messagetext = html_to_text($messagehtml); + } + } else { + $messagetext = get_string('welcometocoursetext', 'enrol_spoken', $a); + $messagehtml = text_to_html($messagetext, null, false, true); + } + + $subject = get_string('welcometocourse', 'enrol_spoken', format_string($course->fullname, true, array('context'=>$context))); + + $sendoption = $instance->customint4; + $contact = $this->get_welcome_email_contact($sendoption, $context); + + // Directly emailing welcome message rather than using messaging. + email_to_user($user, $contact, $subject, $messagetext, $messagehtml); + } + + /** + * Sync all meta course links. + * + * @param progress_trace $trace + * @param int $courseid one course, empty mean all + * @return int 0 means ok, 1 means error, 2 means plugin disabled + */ + public function sync(progress_trace $trace, $courseid = null) { + global $DB; + + if (!enrol_is_enabled('spoken')) { + $trace->finished(); + return 2; + } + + // Unfortunately this may take a long time, execution can be interrupted safely here. + core_php_time_limit::raise(); + raise_memory_limit(MEMORY_HUGE); + + $trace->output('Verifying spoken-enrolments...'); + + $params = array('now'=>time(), 'useractive'=>ENROL_USER_ACTIVE, 'courselevel'=>CONTEXT_COURSE); + $coursesql = ""; + if ($courseid) { + $coursesql = "AND e.courseid = :courseid"; + $params['courseid'] = $courseid; + } + + // Note: the logic of spoken enrolment guarantees that user logged in at least once (=== u.lastaccess set) + // and that user accessed course at least once too (=== user_lastaccess record exists). + + // First deal with users that did not log in for a really long time - they do not have user_lastaccess records. + $sql = "SELECT e.*, ue.userid + FROM {user_enrolments} ue + JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'spoken' AND e.customint2 > 0) + JOIN {user} u ON u.id = ue.userid + WHERE :now - u.lastaccess > e.customint2 + $coursesql"; + $rs = $DB->get_recordset_sql($sql, $params); + foreach ($rs as $instance) { + $userid = $instance->userid; + unset($instance->userid); + $this->unenrol_user($instance, $userid); + $days = $instance->customint2 / DAYSECS; + $trace->output("unenrolling user $userid from course $instance->courseid " . + "as they did not log in for at least $days days", 1); + } + $rs->close(); + + // Now unenrol from course user did not visit for a long time. + $sql = "SELECT e.*, ue.userid + FROM {user_enrolments} ue + JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'spoken' AND e.customint2 > 0) + JOIN {user_lastaccess} ul ON (ul.userid = ue.userid AND ul.courseid = e.courseid) + WHERE :now - ul.timeaccess > e.customint2 + $coursesql"; + $rs = $DB->get_recordset_sql($sql, $params); + foreach ($rs as $instance) { + $userid = $instance->userid; + unset($instance->userid); + $this->unenrol_user($instance, $userid); + $days = $instance->customint2 / DAYSECS; + $trace->output("unenrolling user $userid from course $instance->courseid " . + "as they did not access the course for at least $days days", 1); + } + $rs->close(); + + $trace->output('...user spoken-enrolment updates finished.'); + $trace->finished(); + + $this->process_expirations($trace, $courseid); + + return 0; + } + + /** + * Returns the user who is responsible for spoken enrolments in given instance. + * + * Usually it is the first editing teacher - the person with "highest authority" + * as defined by sort_by_roleassignment_authority() having 'enrol/spoken:manage' + * capability. + * + * @param int $instanceid enrolment instance id + * @return stdClass user record + */ + protected function get_enroller($instanceid) { + global $DB; + + if ($this->lasternollerinstanceid == $instanceid and $this->lasternoller) { + return $this->lasternoller; + } + + $instance = $DB->get_record('enrol', array('id'=>$instanceid, 'enrol'=>$this->get_name()), '*', MUST_EXIST); + $context = context_course::instance($instance->courseid); + + if ($users = get_enrolled_users($context, 'enrol/spoken:manage')) { + $users = sort_by_roleassignment_authority($users, $context); + $this->lasternoller = reset($users); + unset($users); + } else { + $this->lasternoller = parent::get_enroller($instanceid); + } + + $this->lasternollerinstanceid = $instanceid; + + return $this->lasternoller; + } + + /** + * Restore instance and map settings. + * + * @param restore_enrolments_structure_step $step + * @param stdClass $data + * @param stdClass $course + * @param int $oldid + */ + public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) { + global $DB; + if ($step->get_task()->get_target() == backup::TARGET_NEW_COURSE) { + $merge = false; + } else { + $merge = array( + 'courseid' => $data->courseid, + 'enrol' => $this->get_name(), + 'status' => $data->status, + 'roleid' => $data->roleid, + ); + } + if ($merge and $instances = $DB->get_records('enrol', $merge, 'id')) { + $instance = reset($instances); + $instanceid = $instance->id; + } else { + if (!empty($data->customint5)) { + if ($step->get_task()->is_samesite()) { + // Keep cohort restriction unchanged - we are on the same site. + } else { + // Use some id that can not exist in order to prevent spoken enrolment, + // because we do not know what cohort it is in this site. + $data->customint5 = -1; + } + } + $instanceid = $this->add_instance($course, (array)$data); + } + $step->set_mapping('enrol', $oldid, $instanceid); + } + + /** + * Restore user enrolment. + * + * @param restore_enrolments_structure_step $step + * @param stdClass $data + * @param stdClass $instance + * @param int $oldinstancestatus + * @param int $userid + */ + public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) { + $this->enrol_user($instance, $userid, null, $data->timestart, $data->timeend, $data->status); + } + + /** + * Restore role assignment. + * + * @param stdClass $instance + * @param int $roleid + * @param int $userid + * @param int $contextid + */ + public function restore_role_assignment($instance, $roleid, $userid, $contextid) { + // This is necessary only because we may migrate other types to this instance, + // we do not use component in manual or spoken enrol. + role_assign($roleid, $userid, $contextid, '', 0); + } + + /** + * Is it possible to delete enrol instance via standard UI? + * + * @param stdClass $instance + * @return bool + */ + public function can_delete_instance($instance) { + $context = context_course::instance($instance->courseid); + return has_capability('enrol/spoken:config', $context); + } + + /** + * Is it possible to hide/show enrol instance via standard UI? + * + * @param stdClass $instance + * @return bool + */ + public function can_hide_show_instance($instance) { + $context = context_course::instance($instance->courseid); + + if (!has_capability('enrol/spoken:config', $context)) { + return false; + } + + // If the instance is currently disabled, before it can be enabled, + // we must check whether the password meets the password policies. + if ($instance->status == ENROL_INSTANCE_DISABLED) { + if ($this->get_config('requirepassword')) { + if (empty($instance->password)) { + return false; + } + } + // Only check the password if it is set. + if (!empty($instance->password) && $this->get_config('usepasswordpolicy')) { + if (!check_password_policy($instance->password, $errmsg)) { + return false; + } + } + } + + return true; + } + + /** + * Return an array of valid options for the status. + * + * @return array + */ + protected function get_status_options() { + $options = array(ENROL_INSTANCE_ENABLED => get_string('yes'), + ENROL_INSTANCE_DISABLED => get_string('no')); + return $options; + } + + /** + * Return an array of valid options for the newenrols property. + * + * @return array + */ + protected function get_newenrols_options() { + $options = array(1 => get_string('yes'), 0 => get_string('no')); + return $options; + } + + /** + * Return an array of valid options for the groupkey property. + * + * @return array + */ + protected function get_groupkey_options() { + $options = array(1 => get_string('yes'), 0 => get_string('no')); + return $options; + } + + /** + * Return an array of valid options for the expirynotify property. + * + * @return array + */ + protected function get_expirynotify_options() { + $options = array(0 => get_string('no'), + 1 => get_string('expirynotifyenroller', 'enrol_spoken'), + 2 => get_string('expirynotifyall', 'enrol_spoken')); + return $options; + } + + /** + * Return an array of valid options for the longtimenosee property. + * + * @return array + */ + protected function get_longtimenosee_options() { + $options = array(0 => get_string('never'), + 1800 * 3600 * 24 => get_string('numdays', '', 1800), + 1000 * 3600 * 24 => get_string('numdays', '', 1000), + 365 * 3600 * 24 => get_string('numdays', '', 365), + 180 * 3600 * 24 => get_string('numdays', '', 180), + 150 * 3600 * 24 => get_string('numdays', '', 150), + 120 * 3600 * 24 => get_string('numdays', '', 120), + 90 * 3600 * 24 => get_string('numdays', '', 90), + 60 * 3600 * 24 => get_string('numdays', '', 60), + 30 * 3600 * 24 => get_string('numdays', '', 30), + 21 * 3600 * 24 => get_string('numdays', '', 21), + 14 * 3600 * 24 => get_string('numdays', '', 14), + 7 * 3600 * 24 => get_string('numdays', '', 7)); + return $options; + } + + /** + * The spoken enrollment plugin has several bulk operations that can be performed. + * @param course_enrolment_manager $manager + * @return array + */ + public function get_bulk_operations(course_enrolment_manager $manager) { + global $CFG; + require_once($CFG->dirroot.'/enrol/spoken/locallib.php'); + $context = $manager->get_context(); + $bulkoperations = array(); + if (has_capability("enrol/spoken:manage", $context)) { + $bulkoperations['editselectedusers'] = new enrol_spoken_editselectedusers_operation($manager, $this); + } + if (has_capability("enrol/spoken:unenrol", $context)) { + $bulkoperations['deleteselectedusers'] = new enrol_spoken_deleteselectedusers_operation($manager, $this); + } + return $bulkoperations; + } + + /** + * Add elements to the edit instance form. + * + * @param stdClass $instance + * @param MoodleQuickForm $mform + * @param context $context + * @return bool + */ + public function edit_instance_form($instance, MoodleQuickForm $mform, $context) { + global $CFG, $DB; + + // Merge these two settings to one value for the single selection element. + if ($instance->notifyall and $instance->expirynotify) { + $instance->expirynotify = 2; + } + unset($instance->notifyall); + + $nameattribs = array('size' => '20', 'maxlength' => '255'); + $mform->addElement('text', 'name', get_string('custominstancename', 'enrol'), $nameattribs); + $mform->setType('name', PARAM_TEXT); + $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'server'); + + $options = $this->get_status_options(); + $mform->addElement('select', 'status', get_string('status', 'enrol_spoken'), $options); + $mform->addHelpButton('status', 'status', 'enrol_spoken'); + + $options = $this->get_newenrols_options(); + $mform->addElement('select', 'customint6', get_string('newenrols', 'enrol_spoken'), $options); + $mform->addHelpButton('customint6', 'newenrols', 'enrol_spoken'); + $mform->disabledIf('customint6', 'status', 'eq', ENROL_INSTANCE_DISABLED); + + $passattribs = array('size' => '20', 'maxlength' => '50'); + $mform->addElement('passwordunmask', 'password', get_string('password', 'enrol_spoken'), $passattribs); + $mform->addHelpButton('password', 'password', 'enrol_spoken'); + if (empty($instance->id) and $this->get_config('requirepassword')) { + $mform->addRule('password', get_string('required'), 'required', null, 'client'); + } + $mform->addRule('password', get_string('maximumchars', '', 50), 'maxlength', 50, 'server'); + + $options = $this->get_groupkey_options(); + $mform->addElement('select', 'customint1', get_string('groupkey', 'enrol_spoken'), $options); + $mform->addHelpButton('customint1', 'groupkey', 'enrol_spoken'); + + $roles = $this->extend_assignable_roles($context, $instance->roleid); + $mform->addElement('select', 'roleid', get_string('role', 'enrol_spoken'), $roles); + + $options = array('optional' => true, 'defaultunit' => 86400); + $mform->addElement('duration', 'enrolperiod', get_string('enrolperiod', 'enrol_spoken'), $options); + $mform->addHelpButton('enrolperiod', 'enrolperiod', 'enrol_spoken'); + + $options = $this->get_expirynotify_options(); + $mform->addElement('select', 'expirynotify', get_string('expirynotify', 'core_enrol'), $options); + $mform->addHelpButton('expirynotify', 'expirynotify', 'core_enrol'); + + $options = array('optional' => false, 'defaultunit' => 86400); + $mform->addElement('duration', 'expirythreshold', get_string('expirythreshold', 'core_enrol'), $options); + $mform->addHelpButton('expirythreshold', 'expirythreshold', 'core_enrol'); + $mform->disabledIf('expirythreshold', 'expirynotify', 'eq', 0); + + $options = array('optional' => true); + $mform->addElement('date_time_selector', 'enrolstartdate', get_string('enrolstartdate', 'enrol_spoken'), $options); + $mform->setDefault('enrolstartdate', 0); + $mform->addHelpButton('enrolstartdate', 'enrolstartdate', 'enrol_spoken'); + + $options = array('optional' => true); + $mform->addElement('date_time_selector', 'enrolenddate', get_string('enrolenddate', 'enrol_spoken'), $options); + $mform->setDefault('enrolenddate', 0); + $mform->addHelpButton('enrolenddate', 'enrolenddate', 'enrol_spoken'); + + $options = $this->get_longtimenosee_options(); + $mform->addElement('select', 'customint2', get_string('longtimenosee', 'enrol_spoken'), $options); + $mform->addHelpButton('customint2', 'longtimenosee', 'enrol_spoken'); + + $mform->addElement('text', 'customint3', get_string('maxenrolled', 'enrol_spoken')); + $mform->addHelpButton('customint3', 'maxenrolled', 'enrol_spoken'); + $mform->setType('customint3', PARAM_INT); + + require_once($CFG->dirroot.'/cohort/lib.php'); + + $cohorts = array(0 => get_string('no')); + $allcohorts = cohort_get_available_cohorts($context, 0, 0, 0); + if ($instance->customint5 && !isset($allcohorts[$instance->customint5])) { + $c = $DB->get_record('cohort', + array('id' => $instance->customint5), + 'id, name, idnumber, contextid, visible', + IGNORE_MISSING); + if ($c) { + // Current cohort was not found because current user can not see it. Still keep it. + $allcohorts[$instance->customint5] = $c; + } + } + foreach ($allcohorts as $c) { + $cohorts[$c->id] = format_string($c->name, true, array('context' => context::instance_by_id($c->contextid))); + if ($c->idnumber) { + $cohorts[$c->id] .= ' ['.s($c->idnumber).']'; + } + } + if ($instance->customint5 && !isset($allcohorts[$instance->customint5])) { + // Somebody deleted a cohort, better keep the wrong value so that random ppl can not enrol. + $cohorts[$instance->customint5] = get_string('unknowncohort', 'cohort', $instance->customint5); + } + if (count($cohorts) > 1) { + $mform->addElement('select', 'customint5', get_string('cohortonly', 'enrol_spoken'), $cohorts); + $mform->addHelpButton('customint5', 'cohortonly', 'enrol_spoken'); + } else { + $mform->addElement('hidden', 'customint5'); + $mform->setType('customint5', PARAM_INT); + $mform->setConstant('customint5', 0); + } + + $mform->addElement('select', 'customint4', get_string('sendcoursewelcomemessage', 'enrol_spoken'), + enrol_send_welcome_email_options()); + $mform->addHelpButton('customint4', 'sendcoursewelcomemessage', 'enrol_spoken'); + + $options = array('cols' => '60', 'rows' => '8'); + $mform->addElement('textarea', 'customtext1', get_string('customwelcomemessage', 'enrol_spoken'), $options); + $mform->addHelpButton('customtext1', 'customwelcomemessage', 'enrol_spoken'); + + if (enrol_accessing_via_instance($instance)) { + $warntext = get_string('instanceeditspokenwarningtext', 'core_enrol'); + $mform->addElement('static', 'spokenwarn', get_string('instanceeditspokenwarning', 'core_enrol'), $warntext); + } + } + + /** + * We are a good plugin and don't invent our own UI/validation code path. + * + * @return boolean + */ + public function use_standard_editing_ui() { + return true; + } + + /** + * Perform custom validation of the data used to edit the instance. + * + * @param array $data array of ("fieldname"=>value) of submitted data + * @param array $files array of uploaded files "element_name"=>tmp_file_path + * @param object $instance The instance loaded from the DB + * @param context $context The context of the instance we are editing + * @return array of "element_name"=>"error_description" if there are errors, + * or an empty array if everything is OK. + * @return void + */ + public function edit_instance_validation($data, $files, $instance, $context) { + $errors = array(); + + $checkpassword = false; + + if ($instance->id) { + // Check the password if we are enabling the plugin again. + if (($instance->status == ENROL_INSTANCE_DISABLED) && ($data['status'] == ENROL_INSTANCE_ENABLED)) { + $checkpassword = true; + } + + // Check the password if the instance is enabled and the password has changed. + if (($data['status'] == ENROL_INSTANCE_ENABLED) && ($instance->password !== $data['password'])) { + $checkpassword = true; + } + } else { + $checkpassword = true; + } + + if ($checkpassword) { + $require = $this->get_config('requirepassword'); + $policy = $this->get_config('usepasswordpolicy'); + if ($require and trim($data['password']) === '') { + $errors['password'] = get_string('required'); + } else if (!empty($data['password']) && $policy) { + $errmsg = ''; + if (!check_password_policy($data['password'], $errmsg)) { + $errors['password'] = $errmsg; + } + } + } + + if ($data['status'] == ENROL_INSTANCE_ENABLED) { + if (!empty($data['enrolenddate']) and $data['enrolenddate'] < $data['enrolstartdate']) { + $errors['enrolenddate'] = get_string('enrolenddaterror', 'enrol_spoken'); + } + } + + if ($data['expirynotify'] > 0 and $data['expirythreshold'] < 86400) { + $errors['expirythreshold'] = get_string('errorthresholdlow', 'core_enrol'); + } + + // Now these ones are checked by quickforms, but we may be called by the upload enrolments tool, or a webservive. + if (core_text::strlen($data['name']) > 255) { + $errors['name'] = get_string('err_maxlength', 'form', 255); + } + $validstatus = array_keys($this->get_status_options()); + $validnewenrols = array_keys($this->get_newenrols_options()); + if (core_text::strlen($data['password']) > 50) { + $errors['name'] = get_string('err_maxlength', 'form', 50); + } + $validgroupkey = array_keys($this->get_groupkey_options()); + $context = context_course::instance($instance->courseid); + $validroles = array_keys($this->extend_assignable_roles($context, $instance->roleid)); + $validexpirynotify = array_keys($this->get_expirynotify_options()); + $validlongtimenosee = array_keys($this->get_longtimenosee_options()); + $tovalidate = array( + 'enrolstartdate' => PARAM_INT, + 'enrolenddate' => PARAM_INT, + 'name' => PARAM_TEXT, + 'customint1' => $validgroupkey, + 'customint2' => $validlongtimenosee, + 'customint3' => PARAM_INT, + 'customint4' => PARAM_INT, + 'customint5' => PARAM_INT, + 'customint6' => $validnewenrols, + 'status' => $validstatus, + 'enrolperiod' => PARAM_INT, + 'expirynotify' => $validexpirynotify, + 'roleid' => $validroles + ); + if ($data['expirynotify'] != 0) { + $tovalidate['expirythreshold'] = PARAM_INT; + } + $typeerrors = $this->validate_param_types($data, $tovalidate); + $errors = array_merge($errors, $typeerrors); + + return $errors; + } + + /** + * Add new instance of enrol plugin. + * @param object $course + * @param array $fields instance fields + * @return int id of new instance, null if can not be created + */ + public function add_instance($course, array $fields = null) { + // In the form we are representing 2 db columns with one field. + if (!empty($fields) && !empty($fields['expirynotify'])) { + if ($fields['expirynotify'] == 2) { + $fields['expirynotify'] = 1; + $fields['notifyall'] = 1; + } else { + $fields['notifyall'] = 0; + } + } + + return parent::add_instance($course, $fields); + } + + /** + * Update instance of enrol plugin. + * @param stdClass $instance + * @param stdClass $data modified instance fields + * @return boolean + */ + public function update_instance($instance, $data) { + // In the form we are representing 2 db columns with one field. + if ($data->expirynotify == 2) { + $data->expirynotify = 1; + $data->notifyall = 1; + } else { + $data->notifyall = 0; + } + // Keep previous/default value of disabled expirythreshold option. + if (!$data->expirynotify) { + $data->expirythreshold = $instance->expirythreshold; + } + // Add previous value of newenrols if disabled. + if (!isset($data->customint6)) { + $data->customint6 = $instance->customint6; + } + + return parent::update_instance($instance, $data); + } + + /** + * Gets a list of roles that this user can assign for the course as the default for spoken-enrolment. + * + * @param context $context the context. + * @param integer $defaultrole the id of the role that is set as the default for spoken-enrolment + * @return array index is the role id, value is the role name + */ + public function extend_assignable_roles($context, $defaultrole) { + global $DB; + + $roles = get_assignable_roles($context, ROLENAME_BOTH); + if (!isset($roles[$defaultrole])) { + if ($role = $DB->get_record('role', array('id' => $defaultrole))) { + $roles[$defaultrole] = role_get_name($role, $context, ROLENAME_BOTH); + } + } + return $roles; + } + + /** + * Get the "from" contact which the email will be sent from. + * + * @param int $sendoption send email from constant ENROL_SEND_EMAIL_FROM_* + * @param $context context where the user will be fetched + * @return mixed|stdClass the contact user object. + */ + public function get_welcome_email_contact($sendoption, $context) { + global $CFG; + + $contact = null; + // Send as the first user assigned as the course contact. + if ($sendoption == ENROL_SEND_EMAIL_FROM_COURSE_CONTACT) { + $rusers = array(); + if (!empty($CFG->coursecontact)) { + $croles = explode(',', $CFG->coursecontact); + list($sort, $sortparams) = users_order_by_sql('u'); + // We only use the first user. + $i = 0; + do { + $allnames = get_all_user_name_fields(true, 'u'); + $rusers = get_role_users($croles[$i], $context, true, 'u.id, u.confirmed, u.username, '. $allnames . ', + u.email, r.sortorder, ra.id', 'r.sortorder, ra.id ASC, ' . $sort, null, '', '', '', '', $sortparams); + $i++; + } while (empty($rusers) && !empty($croles[$i])); + } + if ($rusers) { + $contact = array_values($rusers)[0]; + } + } else if ($sendoption == ENROL_SEND_EMAIL_FROM_KEY_HOLDER) { + // Send as the first user with enrol/spoken:holdkey capability assigned in the course. + list($sort) = users_order_by_sql('u'); + $keyholders = get_users_by_capability($context, 'enrol/spoken:holdkey', 'u.*', $sort); + if (!empty($keyholders)) { + $contact = array_values($keyholders)[0]; + } + } + + // If send welcome email option is set to no reply or if none of the previous options have + // returned a contact send welcome message as noreplyuser. + if ($sendoption == ENROL_SEND_EMAIL_FROM_NOREPLY || empty($contact)) { + $contact = core_user::get_noreply_user(); + } + + return $contact; + } +} + +/** + * Get icon mapping for font-awesome. + */ +function enrol_spoken_get_fontawesome_icon_map() { + return [ + 'enrol_spoken:withkey' => 'fa-key', + 'enrol_spoken:withoutkey' => 'fa-sign-in', + ]; +} diff --git a/enrol/spoken/locallib.php b/enrol/spoken/locallib.php new file mode 100644 index 00000000..bd1011c6 --- /dev/null +++ b/enrol/spoken/locallib.php @@ -0,0 +1,153 @@ +. + +/** + * spoken enrol plugin implementation. + * + * @package enrol_spoken + * @copyright 2010 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once("$CFG->libdir/formslib.php"); +require_once($CFG->dirroot . '/enrol/locallib.php'); + +/** + * Check if the given password match a group enrolment key in the specified course. + * + * @param int $courseid course id + * @param string $enrolpassword enrolment password + * @return bool True if match + * @since Moodle 3.0 + */ +function enrol_spoken_check_group_enrolment_key($courseid, $enrolpassword) { + global $DB; + + $found = false; + $groups = $DB->get_records('groups', array('courseid' => $courseid), 'id ASC', 'id, enrolmentkey'); + + foreach ($groups as $group) { + if (empty($group->enrolmentkey)) { + continue; + } + if ($group->enrolmentkey === $enrolpassword) { + $found = true; + break; + } + } + return $found; +} + +class enrol_spoken_enrol_form extends moodleform { + protected $instance; + protected $toomany = false; + + /** + * Overriding this function to get unique form id for multiple spoken enrolments. + * + * @return string form identifier + */ + protected function get_form_identifier() { + $formid = $this->_customdata->id.'_'.get_class($this); + return $formid; + } + + public function definition() { + global $USER, $OUTPUT, $CFG; + $mform = $this->_form; + $instance = $this->_customdata; + $this->instance = $instance; + $plugin = enrol_get_plugin('spoken'); + + $heading = $plugin->get_instance_name($instance); + $mform->addElement('header', 'spokenheader', $heading); + + if ($instance->password) { + // Change the id of spoken enrolment key input as there can be multiple spoken enrolment methods. + $mform->addElement('password', 'enrolpassword', get_string('password', 'enrol_spoken'), + array('id' => 'enrolpassword_'.$instance->id)); + $context = context_course::instance($this->instance->courseid); + $keyholders = get_users_by_capability($context, 'enrol/spoken:holdkey', user_picture::fields('u')); + $keyholdercount = 0; + foreach ($keyholders as $keyholder) { + $keyholdercount++; + if ($keyholdercount === 1) { + $mform->addElement('static', 'keyholder', '', get_string('keyholder', 'enrol_spoken')); + } + $keyholdercontext = context_user::instance($keyholder->id); + if ($USER->id == $keyholder->id || has_capability('moodle/user:viewdetails', context_system::instance()) || + has_coursecontact_role($keyholder->id)) { + $profilelink = '' . fullname($keyholder) . ''; + } else { + $profilelink = fullname($keyholder); + } + $profilepic = $OUTPUT->user_picture($keyholder, array('size' => 35, 'courseid' => $this->instance->courseid)); + $mform->addElement('static', 'keyholder'.$keyholdercount, '', $profilepic . $profilelink); + } + + } else { + $mform->addElement('static', 'nokey', '', get_string('nopassword', 'enrol_spoken')); + } + + $this->add_action_buttons(false, get_string('enrolme', 'enrol_spoken')); + + $mform->addElement('hidden', 'id'); + $mform->setType('id', PARAM_INT); + $mform->setDefault('id', $instance->courseid); + + $mform->addElement('hidden', 'instance'); + $mform->setType('instance', PARAM_INT); + $mform->setDefault('instance', $instance->id); + } + + public function validation($data, $files) { + global $DB, $CFG; + + $errors = parent::validation($data, $files); + $instance = $this->instance; + + if ($this->toomany) { + $errors['notice'] = get_string('error'); + return $errors; + } + + if ($instance->password) { + if ($data['enrolpassword'] !== $instance->password) { + if ($instance->customint1) { + // Check group enrolment key. + if (!enrol_spoken_check_group_enrolment_key($instance->courseid, $data['enrolpassword'])) { + // We can not hint because there are probably multiple passwords. + $errors['enrolpassword'] = get_string('passwordinvalid', 'enrol_spoken'); + } + + } else { + $plugin = enrol_get_plugin('spoken'); + if ($plugin->get_config('showhint')) { + $hint = core_text::substr($instance->password, 0, 1); + $errors['enrolpassword'] = get_string('passwordinvalidhint', 'enrol_spoken', $hint); + } else { + $errors['enrolpassword'] = get_string('passwordinvalid', 'enrol_spoken'); + } + } + } + } + + return $errors; + } +} diff --git a/enrol/spoken/pix/withkey.gif b/enrol/spoken/pix/withkey.gif new file mode 100644 index 0000000000000000000000000000000000000000..537cc1aa28db733d84be6cd9a54d81e4b3df0e61 GIT binary patch literal 608 zcmZ?wbhEHb6krfwIF`%+1dI#}%nY2Y91IMcJhF^zk}S;Z0?exH0t})ICaP+Bx;p0i za&8vZRyLOQHa0<)N_KWOQC3F24qDDG&aSRL?(X3pUT)q#p5ES}`QEbrfxgv2(xt(w zb)hOTG0|~xv2k&62_TRNgb9g2045TXfN*B0^MokFjs%B{j7%WN0^_W#%q$?t&RUh| zy&~DQsIai8sHnK8sJYO;xVWgexVWy=wY}7BTUJ1IbyanBbqxdpshXPEwI==5x%Kta zTAINSNVK%H0#R$r=7y-9tu{S9J-tBG3q-xWeZ75sy-WMNCr_C?b?TI|y zb7yN$oH+60M$P{lS^w|M{rqmj|8En(x_}ySBE_F9j9d)$3_75g0L2Ld``(87aA}~U z!|OY{q*e9I96W=f5~aJkmGsT+Jp!VWrcSGuRyMG3@CuBWvT%{gO#6wEs~1YE8JIid z2S+CJF|)IDNeGH?v9WS6@gx)$WiWI~%X5ciQ-aCV{-`es$;*r(!<`%9a{F>+u} +]> \ No newline at end of file diff --git a/enrol/spoken/pix/withoutkey.gif b/enrol/spoken/pix/withoutkey.gif new file mode 100644 index 0000000000000000000000000000000000000000..7fb004117d54f8ce17fbbed22ae78e14800063a4 GIT binary patch literal 625 zcmZ?wbhEHb6krfwIF`>46cP{`9vmJS8W9y1866%K6OoXRkdl&|mX?+U1UZ>GIXOi| zMWv;s6;a=N7rcIkRW9GD(GiT14HEZ_l+4E-4oI7{!y!mtI&!4{l2o}y? zxNza3MT-_MUc6+AK?OgA=?gWBeJ9q8cwSWKqLqKrkz|muejvYI8;>3yC=e3LPExvH+ z?8QsxE?qu<`O1YGH?Q8kd*|N$+YcVzeemes!$52wpya`Rc{%H?Q8mfBXE~ z^RHjOeEa_GI|zOM@#Fi?pFe*7{Q2t_5dHf78;E}Y`Sa)R-@pI<{rms_|NQy$NJWZ2 zSs1w(>KSwxfB+OH4D9C`>J?a9T3Z$JI`iroC4_i6*rXGZ;zYB%dZa}7IXMhcGE-Hv zW-=;D3iEI~re$YnFYWBnRg)1GbW2~a7w@~7(Lh7d*uZ^vhMt{Ga#oL#mWrgM$8J9h z3p*>P9#egFMIV>+>o;!L^_U-Wx4!!9*$YNX6CEi@@06_fH+wA2^yN)XBrIAQ!!PIj z>uHDMA?FUIkV8uju<5w>$+GLDJiL^8y47^Q7%Rso=B7!SVe1lBK5Fn)V^DHasT6YU Y^3~wFmGVjOk|4ie z1_6VDhW`EUD?R%}fnrXcE{-7;jL9orym*n3z|h2JahTDgLDFGO9801>=oUwbSF&pr z*a8+TK5|s@M8bOpMhj*=*6^MSeFhfgTe~DWM4f DWn4Tk literal 0 HcmV?d00001 diff --git a/enrol/spoken/pix/withoutkey.svg b/enrol/spoken/pix/withoutkey.svg new file mode 100644 index 00000000..4b0bf8da --- /dev/null +++ b/enrol/spoken/pix/withoutkey.svg @@ -0,0 +1,3 @@ + +]> \ No newline at end of file diff --git a/enrol/spoken/settings.php b/enrol/spoken/settings.php new file mode 100644 index 00000000..aecac82a --- /dev/null +++ b/enrol/spoken/settings.php @@ -0,0 +1,124 @@ +. + +/** + * spoken enrolment plugin settings and presets. + * + * @package enrol_spoken + * @copyright 2010 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($ADMIN->fulltree) { + + //--- general settings ----------------------------------------------------------------------------------- + $settings->add(new admin_setting_heading('enrol_spoken_settings', '', get_string('pluginname_desc', 'enrol_spoken'))); + + $settings->add(new admin_setting_configcheckbox('enrol_spoken/requirepassword', + get_string('requirepassword', 'enrol_spoken'), get_string('requirepassword_desc', 'enrol_spoken'), 0)); + + $settings->add(new admin_setting_configcheckbox('enrol_spoken/usepasswordpolicy', + get_string('usepasswordpolicy', 'enrol_spoken'), get_string('usepasswordpolicy_desc', 'enrol_spoken'), 0)); + + $settings->add(new admin_setting_configcheckbox('enrol_spoken/showhint', + get_string('showhint', 'enrol_spoken'), get_string('showhint_desc', 'enrol_spoken'), 0)); + + // Note: let's reuse the ext sync constants and strings here, internally it is very similar, + // it describes what should happend when users are not supposed to be enerolled any more. + $options = array( + ENROL_EXT_REMOVED_KEEP => get_string('extremovedkeep', 'enrol'), + ENROL_EXT_REMOVED_SUSPENDNOROLES => get_string('extremovedsuspendnoroles', 'enrol'), + ENROL_EXT_REMOVED_UNENROL => get_string('extremovedunenrol', 'enrol'), + ); + $settings->add(new admin_setting_configselect('enrol_spoken/expiredaction', get_string('expiredaction', 'enrol_spoken'), get_string('expiredaction_help', 'enrol_spoken'), ENROL_EXT_REMOVED_KEEP, $options)); + + $options = array(); + for ($i=0; $i<24; $i++) { + $options[$i] = $i; + } + $settings->add(new admin_setting_configselect('enrol_spoken/expirynotifyhour', get_string('expirynotifyhour', 'core_enrol'), '', 6, $options)); + + //--- enrol instance defaults ---------------------------------------------------------------------------- + $settings->add(new admin_setting_heading('enrol_spoken_defaults', + get_string('enrolinstancedefaults', 'admin'), get_string('enrolinstancedefaults_desc', 'admin'))); + + $settings->add(new admin_setting_configcheckbox('enrol_spoken/defaultenrol', + get_string('defaultenrol', 'enrol'), get_string('defaultenrol_desc', 'enrol'), 1)); + + $options = array(ENROL_INSTANCE_ENABLED => get_string('yes'), + ENROL_INSTANCE_DISABLED => get_string('no')); + $settings->add(new admin_setting_configselect('enrol_spoken/status', + get_string('status', 'enrol_spoken'), get_string('status_desc', 'enrol_spoken'), ENROL_INSTANCE_DISABLED, $options)); + + $options = array(1 => get_string('yes'), 0 => get_string('no')); + $settings->add(new admin_setting_configselect('enrol_spoken/newenrols', + get_string('newenrols', 'enrol_spoken'), get_string('newenrols_desc', 'enrol_spoken'), 1, $options)); + + $options = array(1 => get_string('yes'), + 0 => get_string('no')); + $settings->add(new admin_setting_configselect('enrol_spoken/groupkey', + get_string('groupkey', 'enrol_spoken'), get_string('groupkey_desc', 'enrol_spoken'), 0, $options)); + + if (!during_initial_install()) { + $options = get_default_enrol_roles(context_system::instance()); + $student = get_archetype_roles('student'); + $student = reset($student); + $settings->add(new admin_setting_configselect('enrol_spoken/roleid', + get_string('defaultrole', 'enrol_spoken'), + get_string('defaultrole_desc', 'enrol_spoken'), + $student->id ?? null, + $options)); + } + + $settings->add(new admin_setting_configduration('enrol_spoken/enrolperiod', + get_string('enrolperiod', 'enrol_spoken'), get_string('enrolperiod_desc', 'enrol_spoken'), 0)); + + $options = array(0 => get_string('no'), + 1 => get_string('expirynotifyenroller', 'enrol_spoken'), + 2 => get_string('expirynotifyall', 'enrol_spoken')); + $settings->add(new admin_setting_configselect('enrol_spoken/expirynotify', + get_string('expirynotify', 'core_enrol'), get_string('expirynotify_help', 'core_enrol'), 0, $options)); + + $settings->add(new admin_setting_configduration('enrol_spoken/expirythreshold', + get_string('expirythreshold', 'core_enrol'), get_string('expirythreshold_help', 'core_enrol'), 86400, 86400)); + + $options = array(0 => get_string('never'), + 1800 * 3600 * 24 => get_string('numdays', '', 1800), + 1000 * 3600 * 24 => get_string('numdays', '', 1000), + 365 * 3600 * 24 => get_string('numdays', '', 365), + 180 * 3600 * 24 => get_string('numdays', '', 180), + 150 * 3600 * 24 => get_string('numdays', '', 150), + 120 * 3600 * 24 => get_string('numdays', '', 120), + 90 * 3600 * 24 => get_string('numdays', '', 90), + 60 * 3600 * 24 => get_string('numdays', '', 60), + 30 * 3600 * 24 => get_string('numdays', '', 30), + 21 * 3600 * 24 => get_string('numdays', '', 21), + 14 * 3600 * 24 => get_string('numdays', '', 14), + 7 * 3600 * 24 => get_string('numdays', '', 7)); + $settings->add(new admin_setting_configselect('enrol_spoken/longtimenosee', + get_string('longtimenosee', 'enrol_spoken'), get_string('longtimenosee_help', 'enrol_spoken'), 0, $options)); + + $settings->add(new admin_setting_configtext('enrol_spoken/maxenrolled', + get_string('maxenrolled', 'enrol_spoken'), get_string('maxenrolled_help', 'enrol_spoken'), 0, PARAM_INT)); + + $settings->add(new admin_setting_configselect('enrol_spoken/sendcoursewelcomemessage', + get_string('sendcoursewelcomemessage', 'enrol_spoken'), + get_string('sendcoursewelcomemessage_help', 'enrol_spoken'), + ENROL_SEND_EMAIL_FROM_COURSE_CONTACT, + enrol_send_welcome_email_options())); +} diff --git a/enrol/spoken/tests/behat/key_holder.feature b/enrol/spoken/tests/behat/key_holder.feature new file mode 100644 index 00000000..eae9dd41 --- /dev/null +++ b/enrol/spoken/tests/behat/key_holder.feature @@ -0,0 +1,51 @@ +@enrol @enrol_self +Feature: Users can be defined as key holders in courses where self enrolment is allowed + In order to participate in courses + As a user + I need to auto enrol me in courses + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | manager1 | Manager | 1 | manager1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And I log in as "admin" + And I navigate to "Users > Permissions > Define roles" in site administration + And I click on "Add a new role" "button" + And I click on "Continue" "button" + And I set the following fields to these values: + | Short name | keyholder | + | Custom full name | Key holder | + | contextlevel50 | 1 | + | enrol/self:holdkey | 1 | + And I click on "Create this role" "button" + And I navigate to "Appearance > Courses" in site administration + And I set the following fields to these values: + | Key holder | 1 | + And I press "Save changes" + And the following "course enrolments" exist: + | user | course | role | + | manager1 | C1 | keyholder | + And I log out + + @javascript + Scenario: The key holder name is displayed on site home page + Given I log in as "admin" + And I am on "Course 1" course homepage + When I add "Self enrolment" enrolment method with: + | Custom instance name | Test student enrolment | + | Enrolment key | moodle_rules | + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "You should have received this enrolment key from:" + And I should see "Manager 1" + And I set the following fields to these values: + | Enrolment key | moodle_rules | + And I press "Enrol me" + Then I should see "Topic 1" + And I should not see "Enrolment options" + And I should not see "Enrol me in this course" diff --git a/enrol/spoken/tests/behat/self_enrolment.feature b/enrol/spoken/tests/behat/self_enrolment.feature new file mode 100644 index 00000000..91551f02 --- /dev/null +++ b/enrol/spoken/tests/behat/self_enrolment.feature @@ -0,0 +1,127 @@ +@enrol @enrol_self +Feature: Users can auto-enrol themself in courses where self enrolment is allowed + In order to participate in courses + As a user + I need to auto enrol me in courses + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + + # Note: Please keep the javascript tag on this Scenario to ensure that we + # test use of the singleselect functionality. + @javascript + Scenario: Self-enrolment enabled as guest + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I add "Self enrolment" enrolment method with: + | Custom instance name | Test student enrolment | + And I log out + When I am on "Course 1" course homepage + And I press "Log in as a guest" + Then I should see "Guests cannot access this course. Please log in." + And I press "Continue" + And I should see "Log in" + + Scenario: Self-enrolment enabled + Given I log in as "teacher1" + And I am on "Course 1" course homepage + When I add "Self enrolment" enrolment method with: + | Custom instance name | Test student enrolment | + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I press "Enrol me" + Then I should see "Topic 1" + And I should not see "Enrolment options" + + Scenario: Self-enrolment enabled requiring an enrolment key + Given I log in as "teacher1" + And I am on "Course 1" course homepage + When I add "Self enrolment" enrolment method with: + | Custom instance name | Test student enrolment | + | Enrolment key | moodle_rules | + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I set the following fields to these values: + | Enrolment key | moodle_rules | + And I press "Enrol me" + Then I should see "Topic 1" + And I should not see "Enrolment options" + And I should not see "Enrol me in this course" + + Scenario: Self-enrolment disabled + Given I log in as "student1" + When I am on "Course 1" course homepage + Then I should see "You cannot enrol yourself in this course" + + Scenario: Self-enrolment enabled requiring a group enrolment key + Given I log in as "teacher1" + And I am on "Course 1" course homepage + When I add "Self enrolment" enrolment method with: + | Custom instance name | Test student enrolment | + | Enrolment key | moodle_rules | + | Use group enrolment keys | Yes | + And I am on "Course 1" course homepage + And I navigate to "Users > Groups" in current page administration + And I press "Create group" + And I set the following fields to these values: + | Group name | Group 1 | + | Enrolment key | Test-groupenrolkey1 | + And I press "Save changes" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I set the following fields to these values: + | Enrolment key | Test-groupenrolkey1 | + And I press "Enrol me" + Then I should see "Topic 1" + And I should not see "Enrolment options" + And I should not see "Enrol me in this course" + + @javascript + Scenario: Edit a self-enrolled user's enrolment from the course participants page + Given I log in as "teacher1" + And I am on "Course 1" course homepage + When I add "Self enrolment" enrolment method with: + | Custom instance name | Test student enrolment | + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I press "Enrol me" + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to course participants + When I click on "//a[@data-action='editenrolment']" "xpath_element" in the "student1" "table_row" + And I should see "Edit Student 1's enrolment" + And I set the field "Status" to "Suspended" + And I click on "Save changes" "button" + Then I should see "Suspended" in the "student1" "table_row" + + @javascript + Scenario: Unenrol a self-enrolled student from the course participants page + Given I log in as "teacher1" + And I am on "Course 1" course homepage + When I add "Self enrolment" enrolment method with: + | Custom instance name | Test student enrolment | + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I press "Enrol me" + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to course participants + When I click on "//a[@data-action='unenrol']" "xpath_element" in the "student1" "table_row" + And I click on "Unenrol" "button" in the "Unenrol" "dialogue" + Then I should not see "Student 1" in the "participants" "table" diff --git a/enrol/spoken/tests/externallib_test.php b/enrol/spoken/tests/externallib_test.php new file mode 100644 index 00000000..b6f98abb --- /dev/null +++ b/enrol/spoken/tests/externallib_test.php @@ -0,0 +1,254 @@ +. + +/** + * spoken enrol external PHPunit tests + * + * @package enrol_spoken + * @copyright 2013 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.6 + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); +require_once($CFG->dirroot . '/enrol/spoken/externallib.php'); + +class enrol_spoken_external_testcase extends externallib_advanced_testcase { + + /** + * Test get_instance_info + */ + public function test_get_instance_info() { + global $DB; + + $this->resetAfterTest(true); + + // Check if spoken enrolment plugin is enabled. + $spokenplugin = enrol_get_plugin('spoken'); + $this->assertNotEmpty($spokenplugin); + + $studentrole = $DB->get_record('role', array('shortname'=>'student')); + $this->assertNotEmpty($studentrole); + + $coursedata = new stdClass(); + $coursedata->visible = 0; + $course = self::getDataGenerator()->create_course($coursedata); + + // Add enrolment methods for course. + $instanceid1 = $spokenplugin->add_instance($course, array('status' => ENROL_INSTANCE_ENABLED, + 'name' => 'Test instance 1', + 'customint6' => 1, + 'roleid' => $studentrole->id)); + $instanceid2 = $spokenplugin->add_instance($course, array('status' => ENROL_INSTANCE_DISABLED, + 'customint6' => 1, + 'name' => 'Test instance 2', + 'roleid' => $studentrole->id)); + + $instanceid3 = $spokenplugin->add_instance($course, array('status' => ENROL_INSTANCE_ENABLED, + 'roleid' => $studentrole->id, + 'customint6' => 1, + 'name' => 'Test instance 3', + 'password' => 'test')); + + $enrolmentmethods = $DB->get_records('enrol', array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED)); + $this->assertCount(3, $enrolmentmethods); + + $this->setAdminUser(); + $instanceinfo1 = enrol_spoken_external::get_instance_info($instanceid1); + $instanceinfo1 = external_api::clean_returnvalue(enrol_spoken_external::get_instance_info_returns(), $instanceinfo1); + + $this->assertEquals($instanceid1, $instanceinfo1['id']); + $this->assertEquals($course->id, $instanceinfo1['courseid']); + $this->assertEquals('spoken', $instanceinfo1['type']); + $this->assertEquals('Test instance 1', $instanceinfo1['name']); + $this->assertTrue($instanceinfo1['status']); + $this->assertFalse(isset($instanceinfo1['enrolpassword'])); + + $instanceinfo2 = enrol_spoken_external::get_instance_info($instanceid2); + $instanceinfo2 = external_api::clean_returnvalue(enrol_spoken_external::get_instance_info_returns(), $instanceinfo2); + $this->assertEquals($instanceid2, $instanceinfo2['id']); + $this->assertEquals($course->id, $instanceinfo2['courseid']); + $this->assertEquals('spoken', $instanceinfo2['type']); + $this->assertEquals('Test instance 2', $instanceinfo2['name']); + $this->assertEquals(get_string('canntenrol', 'enrol_spoken'), $instanceinfo2['status']); + $this->assertFalse(isset($instanceinfo2['enrolpassword'])); + + $instanceinfo3 = enrol_spoken_external::get_instance_info($instanceid3); + $instanceinfo3 = external_api::clean_returnvalue(enrol_spoken_external::get_instance_info_returns(), $instanceinfo3); + $this->assertEquals($instanceid3, $instanceinfo3['id']); + $this->assertEquals($course->id, $instanceinfo3['courseid']); + $this->assertEquals('spoken', $instanceinfo3['type']); + $this->assertEquals('Test instance 3', $instanceinfo3['name']); + $this->assertTrue($instanceinfo3['status']); + $this->assertEquals(get_string('password', 'enrol_spoken'), $instanceinfo3['enrolpassword']); + + // Try to retrieve information using a normal user for a hidden course. + $user = self::getDataGenerator()->create_user(); + $this->setUser($user); + try { + enrol_spoken_external::get_instance_info($instanceid3); + } catch (moodle_exception $e) { + $this->assertEquals('coursehidden', $e->errorcode); + } + } + + /** + * Test enrol_user + */ + public function test_enrol_user() { + global $DB; + + self::resetAfterTest(true); + + $user = self::getDataGenerator()->create_user(); + self::setUser($user); + + $course1 = self::getDataGenerator()->create_course(); + $course2 = self::getDataGenerator()->create_course(array('groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1)); + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + $user4 = self::getDataGenerator()->create_user(); + + $context1 = context_course::instance($course1->id); + $context2 = context_course::instance($course2->id); + + $spokenplugin = enrol_get_plugin('spoken'); + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + $instance1id = $spokenplugin->add_instance($course1, array('status' => ENROL_INSTANCE_ENABLED, + 'name' => 'Test instance 1', + 'customint6' => 1, + 'roleid' => $studentrole->id)); + $instance2id = $spokenplugin->add_instance($course2, array('status' => ENROL_INSTANCE_DISABLED, + 'customint6' => 1, + 'name' => 'Test instance 2', + 'roleid' => $studentrole->id)); + $instance1 = $DB->get_record('enrol', array('id' => $instance1id), '*', MUST_EXIST); + $instance2 = $DB->get_record('enrol', array('id' => $instance2id), '*', MUST_EXIST); + + self::setUser($user1); + + // spoken enrol me. + $result = enrol_spoken_external::enrol_user($course1->id); + $result = external_api::clean_returnvalue(enrol_spoken_external::enrol_user_returns(), $result); + + self::assertTrue($result['status']); + self::assertEquals(1, $DB->count_records('user_enrolments', array('enrolid' => $instance1->id))); + self::assertTrue(is_enrolled($context1, $user1)); + + // Add password. + $instance2->password = 'abcdef'; + $DB->update_record('enrol', $instance2); + + // Try instance not enabled. + try { + enrol_spoken_external::enrol_user($course2->id); + } catch (moodle_exception $e) { + self::assertEquals('canntenrol', $e->errorcode); + } + + // Enable the instance. + $spokenplugin->update_status($instance2, ENROL_INSTANCE_ENABLED); + + // Try not passing a key. + $result = enrol_spoken_external::enrol_user($course2->id); + $result = external_api::clean_returnvalue(enrol_spoken_external::enrol_user_returns(), $result); + self::assertFalse($result['status']); + self::assertCount(1, $result['warnings']); + self::assertEquals('4', $result['warnings'][0]['warningcode']); + + // Try passing an invalid key. + $result = enrol_spoken_external::enrol_user($course2->id, 'invalidkey'); + $result = external_api::clean_returnvalue(enrol_spoken_external::enrol_user_returns(), $result); + self::assertFalse($result['status']); + self::assertCount(1, $result['warnings']); + self::assertEquals('4', $result['warnings'][0]['warningcode']); + + // Try passing an invalid key with hint. + $spokenplugin->set_config('showhint', true); + $result = enrol_spoken_external::enrol_user($course2->id, 'invalidkey'); + $result = external_api::clean_returnvalue(enrol_spoken_external::enrol_user_returns(), $result); + self::assertFalse($result['status']); + self::assertCount(1, $result['warnings']); + self::assertEquals('3', $result['warnings'][0]['warningcode']); + + // Everything correct, now. + $result = enrol_spoken_external::enrol_user($course2->id, 'abcdef'); + $result = external_api::clean_returnvalue(enrol_spoken_external::enrol_user_returns(), $result); + + self::assertTrue($result['status']); + self::assertEquals(1, $DB->count_records('user_enrolments', array('enrolid' => $instance2->id))); + self::assertTrue(is_enrolled($context2, $user1)); + + // Try group password now, other user. + $instance2->customint1 = 1; + $instance2->password = 'zyx'; + $DB->update_record('enrol', $instance2); + + $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); + $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course2->id, 'enrolmentkey' => 'zyx')); + + self::setUser($user2); + // Try passing and invalid key for group. + $result = enrol_spoken_external::enrol_user($course2->id, 'invalidkey'); + $result = external_api::clean_returnvalue(enrol_spoken_external::enrol_user_returns(), $result); + self::assertFalse($result['status']); + self::assertCount(1, $result['warnings']); + self::assertEquals('2', $result['warnings'][0]['warningcode']); + + // Now, everything ok. + $result = enrol_spoken_external::enrol_user($course2->id, 'zyx'); + $result = external_api::clean_returnvalue(enrol_spoken_external::enrol_user_returns(), $result); + + self::assertTrue($result['status']); + self::assertEquals(2, $DB->count_records('user_enrolments', array('enrolid' => $instance2->id))); + self::assertTrue(is_enrolled($context2, $user2)); + + // Try multiple instances now, multiple errors. + $instance3id = $spokenplugin->add_instance($course2, array('status' => ENROL_INSTANCE_ENABLED, + 'customint6' => 1, + 'name' => 'Test instance 2', + 'roleid' => $studentrole->id)); + $instance3 = $DB->get_record('enrol', array('id' => $instance3id), '*', MUST_EXIST); + $instance3->password = 'abcdef'; + $DB->update_record('enrol', $instance3); + + self::setUser($user3); + $result = enrol_spoken_external::enrol_user($course2->id, 'invalidkey'); + $result = external_api::clean_returnvalue(enrol_spoken_external::enrol_user_returns(), $result); + self::assertFalse($result['status']); + self::assertCount(2, $result['warnings']); + + // Now, everything ok. + $result = enrol_spoken_external::enrol_user($course2->id, 'zyx'); + $result = external_api::clean_returnvalue(enrol_spoken_external::enrol_user_returns(), $result); + self::assertTrue($result['status']); + self::assertTrue(is_enrolled($context2, $user3)); + + // Now test passing an instance id. + self::setUser($user4); + $result = enrol_spoken_external::enrol_user($course2->id, 'abcdef', $instance3id); + $result = external_api::clean_returnvalue(enrol_spoken_external::enrol_user_returns(), $result); + self::assertTrue($result['status']); + self::assertTrue(is_enrolled($context2, $user3)); + self::assertCount(0, $result['warnings']); + self::assertEquals(1, $DB->count_records('user_enrolments', array('enrolid' => $instance3->id))); + } +} diff --git a/enrol/spoken/tests/spoken_test.php b/enrol/spoken/tests/spoken_test.php new file mode 100644 index 00000000..9245390c --- /dev/null +++ b/enrol/spoken/tests/spoken_test.php @@ -0,0 +1,790 @@ +. + +/** + * spoken enrolment plugin tests. + * + * @package enrol_spoken + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot.'/enrol/spoken/lib.php'); +require_once($CFG->dirroot.'/enrol/spoken/locallib.php'); + +class enrol_spoken_testcase extends advanced_testcase { + + public function test_basics() { + $this->assertTrue(enrol_is_enabled('spoken')); + $plugin = enrol_get_plugin('spoken'); + $this->assertInstanceOf('enrol_spoken_plugin', $plugin); + $this->assertEquals(1, get_config('enrol_spoken', 'defaultenrol')); + $this->assertEquals(ENROL_EXT_REMOVED_KEEP, get_config('enrol_spoken', 'expiredaction')); + } + + public function test_sync_nothing() { + global $SITE; + + $spokenplugin = enrol_get_plugin('spoken'); + + $trace = new null_progress_trace(); + + // Just make sure the sync does not throw any errors when nothing to do. + $spokenplugin->sync($trace, null); + $spokenplugin->sync($trace, $SITE->id); + } + + public function test_longtimnosee() { + global $DB; + $this->resetAfterTest(); + + $spokenplugin = enrol_get_plugin('spoken'); + $manualplugin = enrol_get_plugin('manual'); + $this->assertNotEmpty($manualplugin); + + $now = time(); + + $trace = new progress_trace_buffer(new text_progress_trace(), false); + + // Prepare some data. + + $studentrole = $DB->get_record('role', array('shortname'=>'student')); + $this->assertNotEmpty($studentrole); + $teacherrole = $DB->get_record('role', array('shortname'=>'teacher')); + $this->assertNotEmpty($teacherrole); + + $record = array('firstaccess'=>$now-60*60*24*800); + $record['lastaccess'] = $now-60*60*24*100; + $user1 = $this->getDataGenerator()->create_user($record); + $record['lastaccess'] = $now-60*60*24*10; + $user2 = $this->getDataGenerator()->create_user($record); + $record['lastaccess'] = $now-60*60*24*1; + $user3 = $this->getDataGenerator()->create_user($record); + $record['lastaccess'] = $now-10; + $user4 = $this->getDataGenerator()->create_user($record); + + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course3 = $this->getDataGenerator()->create_course(); + $context1 = context_course::instance($course1->id); + $context2 = context_course::instance($course2->id); + $context3 = context_course::instance($course3->id); + + $this->assertEquals(3, $DB->count_records('enrol', array('enrol'=>'spoken'))); + $instance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $id = $spokenplugin->add_instance($course3, array('status'=>ENROL_INSTANCE_ENABLED, 'roleid'=>$teacherrole->id)); + $instance3b = $DB->get_record('enrol', array('id'=>$id), '*', MUST_EXIST); + unset($id); + + $this->assertEquals($studentrole->id, $instance1->roleid); + $instance1->customint2 = 60*60*24*14; + $DB->update_record('enrol', $instance1); + $spokenplugin->enrol_user($instance1, $user1->id, $studentrole->id); + $spokenplugin->enrol_user($instance1, $user2->id, $studentrole->id); + $spokenplugin->enrol_user($instance1, $user3->id, $studentrole->id); + $this->assertEquals(3, $DB->count_records('user_enrolments')); + $DB->insert_record('user_lastaccess', array('userid'=>$user2->id, 'courseid'=>$course1->id, 'timeaccess'=>$now-60*60*24*20)); + $DB->insert_record('user_lastaccess', array('userid'=>$user3->id, 'courseid'=>$course1->id, 'timeaccess'=>$now-60*60*24*2)); + $DB->insert_record('user_lastaccess', array('userid'=>$user4->id, 'courseid'=>$course1->id, 'timeaccess'=>$now-60)); + + $this->assertEquals($studentrole->id, $instance3->roleid); + $instance3->customint2 = 60*60*24*50; + $DB->update_record('enrol', $instance3); + $spokenplugin->enrol_user($instance3, $user1->id, $studentrole->id); + $spokenplugin->enrol_user($instance3, $user2->id, $studentrole->id); + $spokenplugin->enrol_user($instance3, $user3->id, $studentrole->id); + $spokenplugin->enrol_user($instance3b, $user1->id, $teacherrole->id); + $spokenplugin->enrol_user($instance3b, $user4->id, $teacherrole->id); + $this->assertEquals(8, $DB->count_records('user_enrolments')); + $DB->insert_record('user_lastaccess', array('userid'=>$user2->id, 'courseid'=>$course3->id, 'timeaccess'=>$now-60*60*24*11)); + $DB->insert_record('user_lastaccess', array('userid'=>$user3->id, 'courseid'=>$course3->id, 'timeaccess'=>$now-60*60*24*200)); + $DB->insert_record('user_lastaccess', array('userid'=>$user4->id, 'courseid'=>$course3->id, 'timeaccess'=>$now-60*60*24*200)); + + $maninstance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'manual'), '*', MUST_EXIST); + $maninstance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'manual'), '*', MUST_EXIST); + + $manualplugin->enrol_user($maninstance2, $user1->id, $studentrole->id); + $manualplugin->enrol_user($maninstance3, $user1->id, $teacherrole->id); + + $this->assertEquals(10, $DB->count_records('user_enrolments')); + $this->assertEquals(9, $DB->count_records('role_assignments')); + $this->assertEquals(7, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id))); + $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id))); + + // Execute sync - this is the same thing used from cron. + + $spokenplugin->sync($trace, $course2->id); + $output = $trace->get_buffer(); + $trace->reset_buffer(); + $this->assertEquals(10, $DB->count_records('user_enrolments')); + $this->assertStringContainsString('No expired enrol_spoken enrolments detected', $output); + $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user1->id))); + $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user2->id))); + $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$instance3->id, 'userid'=>$user1->id))); + $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$instance3->id, 'userid'=>$user3->id))); + + $spokenplugin->sync($trace, null); + $output = $trace->get_buffer(); + $trace->reset_buffer(); + $this->assertEquals(6, $DB->count_records('user_enrolments')); + $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user1->id))); + $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user2->id))); + $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance3->id, 'userid'=>$user1->id))); + $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance3->id, 'userid'=>$user3->id))); + $this->assertStringContainsString('unenrolling user ' . $user1->id . ' from course ' . $course1->id . + ' as they did not log in for at least 14 days', $output); + $this->assertStringContainsString('unenrolling user ' . $user1->id . ' from course ' . $course3->id . + ' as they did not log in for at least 50 days', $output); + $this->assertStringContainsString('unenrolling user ' . $user2->id . ' from course ' . $course1->id . + ' as they did not access the course for at least 14 days', $output); + $this->assertStringContainsString('unenrolling user ' . $user3->id . ' from course ' . $course3->id . + ' as they did not access the course for at least 50 days', $output); + $this->assertStringNotContainsString('unenrolling user ' . $user4->id, $output); + + $this->assertEquals(6, $DB->count_records('role_assignments')); + $this->assertEquals(4, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id))); + $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id))); + } + + public function test_expired() { + global $DB; + $this->resetAfterTest(); + + $spokenplugin = enrol_get_plugin('spoken'); + $manualplugin = enrol_get_plugin('manual'); + $this->assertNotEmpty($manualplugin); + + $now = time(); + + $trace = new null_progress_trace(); + + // Prepare some data. + + $studentrole = $DB->get_record('role', array('shortname'=>'student')); + $this->assertNotEmpty($studentrole); + $teacherrole = $DB->get_record('role', array('shortname'=>'teacher')); + $this->assertNotEmpty($teacherrole); + $managerrole = $DB->get_record('role', array('shortname'=>'manager')); + $this->assertNotEmpty($managerrole); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $user4 = $this->getDataGenerator()->create_user(); + + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course3 = $this->getDataGenerator()->create_course(); + $context1 = context_course::instance($course1->id); + $context2 = context_course::instance($course2->id); + $context3 = context_course::instance($course3->id); + + $this->assertEquals(3, $DB->count_records('enrol', array('enrol'=>'spoken'))); + $instance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $this->assertEquals($studentrole->id, $instance1->roleid); + $instance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $this->assertEquals($studentrole->id, $instance2->roleid); + $instance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $this->assertEquals($studentrole->id, $instance3->roleid); + $id = $spokenplugin->add_instance($course3, array('status'=>ENROL_INSTANCE_ENABLED, 'roleid'=>$teacherrole->id)); + $instance3b = $DB->get_record('enrol', array('id'=>$id), '*', MUST_EXIST); + $this->assertEquals($teacherrole->id, $instance3b->roleid); + unset($id); + + $maninstance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'manual'), '*', MUST_EXIST); + $maninstance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'manual'), '*', MUST_EXIST); + + $manualplugin->enrol_user($maninstance2, $user1->id, $studentrole->id); + $manualplugin->enrol_user($maninstance3, $user1->id, $teacherrole->id); + + $this->assertEquals(2, $DB->count_records('user_enrolments')); + $this->assertEquals(2, $DB->count_records('role_assignments')); + $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id))); + $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id))); + + $spokenplugin->enrol_user($instance1, $user1->id, $studentrole->id); + $spokenplugin->enrol_user($instance1, $user2->id, $studentrole->id); + $spokenplugin->enrol_user($instance1, $user3->id, $studentrole->id, 0, $now-60); + + $spokenplugin->enrol_user($instance3, $user1->id, $studentrole->id, 0, 0); + $spokenplugin->enrol_user($instance3, $user2->id, $studentrole->id, 0, $now-60*60); + $spokenplugin->enrol_user($instance3, $user3->id, $studentrole->id, 0, $now+60*60); + $spokenplugin->enrol_user($instance3b, $user1->id, $teacherrole->id, $now-60*60*24*7, $now-60); + $spokenplugin->enrol_user($instance3b, $user4->id, $teacherrole->id); + + role_assign($managerrole->id, $user3->id, $context1->id); + + $this->assertEquals(10, $DB->count_records('user_enrolments')); + $this->assertEquals(10, $DB->count_records('role_assignments')); + $this->assertEquals(7, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id))); + $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id))); + + // Execute tests. + + $this->assertEquals(ENROL_EXT_REMOVED_KEEP, $spokenplugin->get_config('expiredaction')); + $spokenplugin->sync($trace, null); + $this->assertEquals(10, $DB->count_records('user_enrolments')); + $this->assertEquals(10, $DB->count_records('role_assignments')); + + + $spokenplugin->set_config('expiredaction', ENROL_EXT_REMOVED_SUSPENDNOROLES); + $spokenplugin->sync($trace, $course2->id); + $this->assertEquals(10, $DB->count_records('user_enrolments')); + $this->assertEquals(10, $DB->count_records('role_assignments')); + + $spokenplugin->sync($trace, null); + $this->assertEquals(10, $DB->count_records('user_enrolments')); + $this->assertEquals(7, $DB->count_records('role_assignments')); + $this->assertEquals(5, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id))); + $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id))); + $this->assertFalse($DB->record_exists('role_assignments', array('contextid'=>$context1->id, 'userid'=>$user3->id, 'roleid'=>$studentrole->id))); + $this->assertFalse($DB->record_exists('role_assignments', array('contextid'=>$context3->id, 'userid'=>$user2->id, 'roleid'=>$studentrole->id))); + $this->assertFalse($DB->record_exists('role_assignments', array('contextid'=>$context3->id, 'userid'=>$user1->id, 'roleid'=>$teacherrole->id))); + $this->assertTrue($DB->record_exists('role_assignments', array('contextid'=>$context3->id, 'userid'=>$user1->id, 'roleid'=>$studentrole->id))); + + + $spokenplugin->set_config('expiredaction', ENROL_EXT_REMOVED_UNENROL); + + role_assign($studentrole->id, $user3->id, $context1->id); + role_assign($studentrole->id, $user2->id, $context3->id); + role_assign($teacherrole->id, $user1->id, $context3->id); + $this->assertEquals(10, $DB->count_records('user_enrolments')); + $this->assertEquals(10, $DB->count_records('role_assignments')); + $this->assertEquals(7, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id))); + $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id))); + + $spokenplugin->sync($trace, null); + $this->assertEquals(7, $DB->count_records('user_enrolments')); + $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user3->id))); + $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance3->id, 'userid'=>$user2->id))); + $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance3b->id, 'userid'=>$user1->id))); + $this->assertEquals(6, $DB->count_records('role_assignments')); + $this->assertEquals(5, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id))); + $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id))); + } + + public function test_send_expiry_notifications() { + global $DB, $CFG; + $this->resetAfterTest(); + $this->preventResetByRollback(); // Messaging does not like transactions... + + /** @var $spokenplugin enrol_spoken_plugin */ + $spokenplugin = enrol_get_plugin('spoken'); + /** @var $manualplugin enrol_manual_plugin */ + $manualplugin = enrol_get_plugin('manual'); + $now = time(); + $admin = get_admin(); + + $trace = new null_progress_trace(); + + // Note: hopefully nobody executes the unit tests the last second before midnight... + + $spokenplugin->set_config('expirynotifylast', $now - 60*60*24); + $spokenplugin->set_config('expirynotifyhour', 0); + + $studentrole = $DB->get_record('role', array('shortname'=>'student')); + $this->assertNotEmpty($studentrole); + $editingteacherrole = $DB->get_record('role', array('shortname'=>'editingteacher')); + $this->assertNotEmpty($editingteacherrole); + $managerrole = $DB->get_record('role', array('shortname'=>'manager')); + $this->assertNotEmpty($managerrole); + + $user1 = $this->getDataGenerator()->create_user(array('lastname'=>'xuser1')); + $user2 = $this->getDataGenerator()->create_user(array('lastname'=>'xuser2')); + $user3 = $this->getDataGenerator()->create_user(array('lastname'=>'xuser3')); + $user4 = $this->getDataGenerator()->create_user(array('lastname'=>'xuser4')); + $user5 = $this->getDataGenerator()->create_user(array('lastname'=>'xuser5')); + $user6 = $this->getDataGenerator()->create_user(array('lastname'=>'xuser6')); + $user7 = $this->getDataGenerator()->create_user(array('lastname'=>'xuser6')); + $user8 = $this->getDataGenerator()->create_user(array('lastname'=>'xuser6')); + + $course1 = $this->getDataGenerator()->create_course(array('fullname'=>'xcourse1')); + $course2 = $this->getDataGenerator()->create_course(array('fullname'=>'xcourse2')); + $course3 = $this->getDataGenerator()->create_course(array('fullname'=>'xcourse3')); + $course4 = $this->getDataGenerator()->create_course(array('fullname'=>'xcourse4')); + + $this->assertEquals(4, $DB->count_records('enrol', array('enrol'=>'manual'))); + $this->assertEquals(4, $DB->count_records('enrol', array('enrol'=>'spoken'))); + + $maninstance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'manual'), '*', MUST_EXIST); + $instance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance1->expirythreshold = 60*60*24*4; + $instance1->expirynotify = 1; + $instance1->notifyall = 1; + $instance1->status = ENROL_INSTANCE_ENABLED; + $DB->update_record('enrol', $instance1); + + $maninstance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'manual'), '*', MUST_EXIST); + $instance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance2->expirythreshold = 60*60*24*1; + $instance2->expirynotify = 1; + $instance2->notifyall = 1; + $instance2->status = ENROL_INSTANCE_ENABLED; + $DB->update_record('enrol', $instance2); + + $maninstance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'manual'), '*', MUST_EXIST); + $instance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance3->expirythreshold = 60*60*24*1; + $instance3->expirynotify = 1; + $instance3->notifyall = 0; + $instance3->status = ENROL_INSTANCE_ENABLED; + $DB->update_record('enrol', $instance3); + + $maninstance4 = $DB->get_record('enrol', array('courseid'=>$course4->id, 'enrol'=>'manual'), '*', MUST_EXIST); + $instance4 = $DB->get_record('enrol', array('courseid'=>$course4->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance4->expirythreshold = 60*60*24*1; + $instance4->expirynotify = 0; + $instance4->notifyall = 0; + $instance4->status = ENROL_INSTANCE_ENABLED; + $DB->update_record('enrol', $instance4); + + $spokenplugin->enrol_user($instance1, $user1->id, $studentrole->id, 0, $now + 60*60*24*1, ENROL_USER_SUSPENDED); // Suspended users are not notified. + $spokenplugin->enrol_user($instance1, $user2->id, $studentrole->id, 0, $now + 60*60*24*5); // Above threshold are not notified. + $spokenplugin->enrol_user($instance1, $user3->id, $studentrole->id, 0, $now + 60*60*24*3 + 60*60); // Less than one day after threshold - should be notified. + $spokenplugin->enrol_user($instance1, $user4->id, $studentrole->id, 0, $now + 60*60*24*4 - 60*3); // Less than one day after threshold - should be notified. + $spokenplugin->enrol_user($instance1, $user5->id, $studentrole->id, 0, $now + 60*60); // Should have been already notified. + $spokenplugin->enrol_user($instance1, $user6->id, $studentrole->id, 0, $now - 60); // Already expired. + $manualplugin->enrol_user($maninstance1, $user7->id, $editingteacherrole->id); + $manualplugin->enrol_user($maninstance1, $user8->id, $managerrole->id); // Highest role --> enroller. + + $spokenplugin->enrol_user($instance2, $user1->id, $studentrole->id); + $spokenplugin->enrol_user($instance2, $user2->id, $studentrole->id, 0, $now + 60*60*24*1 + 60*3); // Above threshold are not notified. + $spokenplugin->enrol_user($instance2, $user3->id, $studentrole->id, 0, $now + 60*60*24*1 - 60*60); // Less than one day after threshold - should be notified. + + $manualplugin->enrol_user($maninstance3, $user1->id, $editingteacherrole->id); + $spokenplugin->enrol_user($instance3, $user2->id, $studentrole->id, 0, $now + 60*60*24*1 + 60); // Above threshold are not notified. + $spokenplugin->enrol_user($instance3, $user3->id, $studentrole->id, 0, $now + 60*60*24*1 - 60*60); // Less than one day after threshold - should be notified. + + $manualplugin->enrol_user($maninstance4, $user4->id, $editingteacherrole->id); + $spokenplugin->enrol_user($instance4, $user5->id, $studentrole->id, 0, $now + 60*60*24*1 + 60); + $spokenplugin->enrol_user($instance4, $user6->id, $studentrole->id, 0, $now + 60*60*24*1 - 60*60); + + // The notification is sent out in fixed order first individual users, + // then summary per course by enrolid, user lastname, etc. + $this->assertGreaterThan($instance1->id, $instance2->id); + $this->assertGreaterThan($instance2->id, $instance3->id); + + $sink = $this->redirectMessages(); + + $spokenplugin->send_expiry_notifications($trace); + + $messages = $sink->get_messages(); + + $this->assertEquals(2+1 + 1+1 + 1 + 0, count($messages)); + + // First individual notifications from course1. + $this->assertEquals($user3->id, $messages[0]->useridto); + $this->assertEquals($user8->id, $messages[0]->useridfrom); + $this->assertStringContainsString('xcourse1', $messages[0]->fullmessagehtml); + + $this->assertEquals($user4->id, $messages[1]->useridto); + $this->assertEquals($user8->id, $messages[1]->useridfrom); + $this->assertStringContainsString('xcourse1', $messages[1]->fullmessagehtml); + + // Then summary for course1. + $this->assertEquals($user8->id, $messages[2]->useridto); + $this->assertEquals($admin->id, $messages[2]->useridfrom); + $this->assertStringContainsString('xcourse1', $messages[2]->fullmessagehtml); + $this->assertStringNotContainsString('xuser1', $messages[2]->fullmessagehtml); + $this->assertStringNotContainsString('xuser2', $messages[2]->fullmessagehtml); + $this->assertStringContainsString('xuser3', $messages[2]->fullmessagehtml); + $this->assertStringContainsString('xuser4', $messages[2]->fullmessagehtml); + $this->assertStringContainsString('xuser5', $messages[2]->fullmessagehtml); + $this->assertStringNotContainsString('xuser6', $messages[2]->fullmessagehtml); + + // First individual notifications from course2. + $this->assertEquals($user3->id, $messages[3]->useridto); + $this->assertEquals($admin->id, $messages[3]->useridfrom); + $this->assertStringContainsString('xcourse2', $messages[3]->fullmessagehtml); + + // Then summary for course2. + $this->assertEquals($admin->id, $messages[4]->useridto); + $this->assertEquals($admin->id, $messages[4]->useridfrom); + $this->assertStringContainsString('xcourse2', $messages[4]->fullmessagehtml); + $this->assertStringNotContainsString('xuser1', $messages[4]->fullmessagehtml); + $this->assertStringNotContainsString('xuser2', $messages[4]->fullmessagehtml); + $this->assertStringContainsString('xuser3', $messages[4]->fullmessagehtml); + $this->assertStringNotContainsString('xuser4', $messages[4]->fullmessagehtml); + $this->assertStringNotContainsString('xuser5', $messages[4]->fullmessagehtml); + $this->assertStringNotContainsString('xuser6', $messages[4]->fullmessagehtml); + + // Only summary in course3. + $this->assertEquals($user1->id, $messages[5]->useridto); + $this->assertEquals($admin->id, $messages[5]->useridfrom); + $this->assertStringContainsString('xcourse3', $messages[5]->fullmessagehtml); + $this->assertStringNotContainsString('xuser1', $messages[5]->fullmessagehtml); + $this->assertStringNotContainsString('xuser2', $messages[5]->fullmessagehtml); + $this->assertStringContainsString('xuser3', $messages[5]->fullmessagehtml); + $this->assertStringNotContainsString('xuser4', $messages[5]->fullmessagehtml); + $this->assertStringNotContainsString('xuser5', $messages[5]->fullmessagehtml); + $this->assertStringNotContainsString('xuser6', $messages[5]->fullmessagehtml); + + + // Make sure that notifications are not repeated. + $sink->clear(); + + $spokenplugin->send_expiry_notifications($trace); + $this->assertEquals(0, $sink->count()); + + // use invalid notification hour to verify that before the hour the notifications are not sent. + $spokenplugin->set_config('expirynotifylast', time() - 60*60*24); + $spokenplugin->set_config('expirynotifyhour', '24'); + + $spokenplugin->send_expiry_notifications($trace); + $this->assertEquals(0, $sink->count()); + + $spokenplugin->set_config('expirynotifyhour', '0'); + $spokenplugin->send_expiry_notifications($trace); + $this->assertEquals(6, $sink->count()); + } + + public function test_show_enrolme_link() { + global $DB, $CFG; + $this->resetAfterTest(); + $this->preventResetByRollback(); // Messaging does not like transactions... + + /** @var $spokenplugin enrol_spoken_plugin */ + $spokenplugin = enrol_get_plugin('spoken'); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $studentrole = $DB->get_record('role', array('shortname'=>'student')); + $this->assertNotEmpty($studentrole); + + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course3 = $this->getDataGenerator()->create_course(); + $course4 = $this->getDataGenerator()->create_course(); + $course5 = $this->getDataGenerator()->create_course(); + $course6 = $this->getDataGenerator()->create_course(); + $course7 = $this->getDataGenerator()->create_course(); + $course8 = $this->getDataGenerator()->create_course(); + $course9 = $this->getDataGenerator()->create_course(); + $course10 = $this->getDataGenerator()->create_course(); + $course11 = $this->getDataGenerator()->create_course(); + + $cohort1 = $this->getDataGenerator()->create_cohort(); + $cohort2 = $this->getDataGenerator()->create_cohort(); + + // New enrolments are allowed and enrolment instance is enabled. + $instance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance1->customint6 = 1; + $DB->update_record('enrol', $instance1); + $spokenplugin->update_status($instance1, ENROL_INSTANCE_ENABLED); + + // New enrolments are not allowed, but enrolment instance is enabled. + $instance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance2->customint6 = 0; + $DB->update_record('enrol', $instance2); + $spokenplugin->update_status($instance2, ENROL_INSTANCE_ENABLED); + + // New enrolments are allowed , but enrolment instance is disabled. + $instance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance3->customint6 = 1; + $DB->update_record('enrol', $instance3); + $spokenplugin->update_status($instance3, ENROL_INSTANCE_DISABLED); + + // New enrolments are not allowed and enrolment instance is disabled. + $instance4 = $DB->get_record('enrol', array('courseid'=>$course4->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance4->customint6 = 0; + $DB->update_record('enrol', $instance4); + $spokenplugin->update_status($instance4, ENROL_INSTANCE_DISABLED); + + // Cohort member test. + $instance5 = $DB->get_record('enrol', array('courseid'=>$course5->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance5->customint6 = 1; + $instance5->customint5 = $cohort1->id; + $DB->update_record('enrol', $instance1); + $spokenplugin->update_status($instance5, ENROL_INSTANCE_ENABLED); + + $id = $spokenplugin->add_instance($course5, $spokenplugin->get_instance_defaults()); + $instance6 = $DB->get_record('enrol', array('id'=>$id), '*', MUST_EXIST); + $instance6->customint6 = 1; + $instance6->customint5 = $cohort2->id; + $DB->update_record('enrol', $instance1); + $spokenplugin->update_status($instance6, ENROL_INSTANCE_ENABLED); + + // Enrol start date is in future. + $instance7 = $DB->get_record('enrol', array('courseid'=>$course6->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance7->customint6 = 1; + $instance7->enrolstartdate = time() + 60; + $DB->update_record('enrol', $instance7); + $spokenplugin->update_status($instance7, ENROL_INSTANCE_ENABLED); + + // Enrol start date is in past. + $instance8 = $DB->get_record('enrol', array('courseid'=>$course7->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance8->customint6 = 1; + $instance8->enrolstartdate = time() - 60; + $DB->update_record('enrol', $instance8); + $spokenplugin->update_status($instance8, ENROL_INSTANCE_ENABLED); + + // Enrol end date is in future. + $instance9 = $DB->get_record('enrol', array('courseid'=>$course8->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance9->customint6 = 1; + $instance9->enrolenddate = time() + 60; + $DB->update_record('enrol', $instance9); + $spokenplugin->update_status($instance9, ENROL_INSTANCE_ENABLED); + + // Enrol end date is in past. + $instance10 = $DB->get_record('enrol', array('courseid'=>$course9->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance10->customint6 = 1; + $instance10->enrolenddate = time() - 60; + $DB->update_record('enrol', $instance10); + $spokenplugin->update_status($instance10, ENROL_INSTANCE_ENABLED); + + // Maximum enrolments reached. + $instance11 = $DB->get_record('enrol', array('courseid'=>$course10->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance11->customint6 = 1; + $instance11->customint3 = 1; + $DB->update_record('enrol', $instance11); + $spokenplugin->update_status($instance11, ENROL_INSTANCE_ENABLED); + $spokenplugin->enrol_user($instance11, $user2->id, $studentrole->id); + + // Maximum enrolments not reached. + $instance12 = $DB->get_record('enrol', array('courseid'=>$course11->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance12->customint6 = 1; + $instance12->customint3 = 1; + $DB->update_record('enrol', $instance12); + $spokenplugin->update_status($instance12, ENROL_INSTANCE_ENABLED); + + $this->setUser($user1); + $this->assertTrue($spokenplugin->show_enrolme_link($instance1)); + $this->assertFalse($spokenplugin->show_enrolme_link($instance2)); + $this->assertFalse($spokenplugin->show_enrolme_link($instance3)); + $this->assertFalse($spokenplugin->show_enrolme_link($instance4)); + $this->assertFalse($spokenplugin->show_enrolme_link($instance7)); + $this->assertTrue($spokenplugin->show_enrolme_link($instance8)); + $this->assertTrue($spokenplugin->show_enrolme_link($instance9)); + $this->assertFalse($spokenplugin->show_enrolme_link($instance10)); + $this->assertFalse($spokenplugin->show_enrolme_link($instance11)); + $this->assertTrue($spokenplugin->show_enrolme_link($instance12)); + + require_once("$CFG->dirroot/cohort/lib.php"); + cohort_add_member($cohort1->id, $user1->id); + + $this->assertTrue($spokenplugin->show_enrolme_link($instance5)); + $this->assertFalse($spokenplugin->show_enrolme_link($instance6)); + } + + /** + * This will check user enrolment only, rest has been tested in test_show_enrolme_link. + */ + public function test_can_spoken_enrol() { + global $DB, $CFG, $OUTPUT; + $this->resetAfterTest(); + $this->preventResetByRollback(); + + $spokenplugin = enrol_get_plugin('spoken'); + + $expectederrorstring = get_string('canntenrol', 'enrol_spoken'); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $guest = $DB->get_record('user', array('id' => $CFG->siteguest)); + + $studentrole = $DB->get_record('role', array('shortname'=>'student')); + $this->assertNotEmpty($studentrole); + $editingteacherrole = $DB->get_record('role', array('shortname'=>'editingteacher')); + $this->assertNotEmpty($editingteacherrole); + + $course1 = $this->getDataGenerator()->create_course(); + + $instance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'spoken'), '*', MUST_EXIST); + $instance1->customint6 = 1; + $DB->update_record('enrol', $instance1); + $spokenplugin->update_status($instance1, ENROL_INSTANCE_ENABLED); + $spokenplugin->enrol_user($instance1, $user2->id, $editingteacherrole->id); + + $this->setUser($guest); + $this->assertStringContainsString(get_string('noguestaccess', 'enrol'), + $spokenplugin->can_spoken_enrol($instance1, true)); + + $this->setUser($user1); + $this->assertTrue($spokenplugin->can_spoken_enrol($instance1, true)); + + // Active enroled user. + $this->setUser($user2); + $spokenplugin->enrol_user($instance1, $user1->id, $studentrole->id); + $this->setUser($user1); + $this->assertSame($expectederrorstring, $spokenplugin->can_spoken_enrol($instance1, true)); + } + + /** + * Test enrol_spoken_check_group_enrolment_key + */ + public function test_enrol_spoken_check_group_enrolment_key() { + global $DB; + self::resetAfterTest(true); + + // Test in course with groups. + $course = self::getDataGenerator()->create_course(array('groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1)); + + $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); + $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id, 'enrolmentkey' => 'thepassword')); + + $result = enrol_spoken_check_group_enrolment_key($course->id, 'invalidpassword'); + $this->assertFalse($result); + + $result = enrol_spoken_check_group_enrolment_key($course->id, 'thepassword'); + $this->assertTrue($result); + + // Test disabling group options. + $course->groupmode = NOGROUPS; + $course->groupmodeforce = 0; + $DB->update_record('course', $course); + + $result = enrol_spoken_check_group_enrolment_key($course->id, 'invalidpassword'); + $this->assertFalse($result); + + $result = enrol_spoken_check_group_enrolment_key($course->id, 'thepassword'); + $this->assertTrue($result); + + // Test without groups. + $othercourse = self::getDataGenerator()->create_course(); + $result = enrol_spoken_check_group_enrolment_key($othercourse->id, 'thepassword'); + $this->assertFalse($result); + + } + + /** + * Test get_welcome_email_contact(). + */ + public function test_get_welcome_email_contact() { + global $DB; + self::resetAfterTest(true); + + $user1 = $this->getDataGenerator()->create_user(['lastname' => 'Marsh']); + $user2 = $this->getDataGenerator()->create_user(['lastname' => 'Victoria']); + $user3 = $this->getDataGenerator()->create_user(['lastname' => 'Burch']); + $user4 = $this->getDataGenerator()->create_user(['lastname' => 'Cartman']); + $noreplyuser = core_user::get_noreply_user(); + + $course1 = $this->getDataGenerator()->create_course(); + $context = context_course::instance($course1->id); + + // Get editing teacher role. + $editingteacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']); + $this->assertNotEmpty($editingteacherrole); + + // Enable spoken enrolment plugin and set to send email from course contact. + $spokenplugin = enrol_get_plugin('spoken'); + $instance1 = $DB->get_record('enrol', ['courseid' => $course1->id, 'enrol' => 'spoken'], '*', MUST_EXIST); + $instance1->customint6 = 1; + $instance1->customint4 = ENROL_SEND_EMAIL_FROM_COURSE_CONTACT; + $DB->update_record('enrol', $instance1); + $spokenplugin->update_status($instance1, ENROL_INSTANCE_ENABLED); + + // We do not have a teacher enrolled at this point, so it should send as no reply user. + $contact = $spokenplugin->get_welcome_email_contact(ENROL_SEND_EMAIL_FROM_COURSE_CONTACT, $context); + $this->assertEquals($noreplyuser, $contact); + + // By default, course contact is assigned to teacher role. + // Enrol a teacher, now it should send emails from teacher email's address. + $spokenplugin->enrol_user($instance1, $user1->id, $editingteacherrole->id); + + // We should get the teacher email. + $contact = $spokenplugin->get_welcome_email_contact(ENROL_SEND_EMAIL_FROM_COURSE_CONTACT, $context); + $this->assertEquals($user1->username, $contact->username); + $this->assertEquals($user1->email, $contact->email); + + // Now let's enrol another teacher. + $spokenplugin->enrol_user($instance1, $user2->id, $editingteacherrole->id); + $contact = $spokenplugin->get_welcome_email_contact(ENROL_SEND_EMAIL_FROM_COURSE_CONTACT, $context); + $this->assertEquals($user1->username, $contact->username); + $this->assertEquals($user1->email, $contact->email); + + // Get manager role, and enrol user as manager. + $managerrole = $DB->get_record('role', ['shortname' => 'manager']); + $this->assertNotEmpty($managerrole); + $instance1->customint4 = ENROL_SEND_EMAIL_FROM_KEY_HOLDER; + $DB->update_record('enrol', $instance1); + $spokenplugin->enrol_user($instance1, $user3->id, $managerrole->id); + + // Give manager role holdkey capability. + assign_capability('enrol/spoken:holdkey', CAP_ALLOW, $managerrole->id, $context); + + // We should get the manager email contact. + $contact = $spokenplugin->get_welcome_email_contact(ENROL_SEND_EMAIL_FROM_KEY_HOLDER, $context); + $this->assertEquals($user3->username, $contact->username); + $this->assertEquals($user3->email, $contact->email); + + // Now let's enrol another manager. + $spokenplugin->enrol_user($instance1, $user4->id, $managerrole->id); + $contact = $spokenplugin->get_welcome_email_contact(ENROL_SEND_EMAIL_FROM_KEY_HOLDER, $context); + $this->assertEquals($user3->username, $contact->username); + $this->assertEquals($user3->email, $contact->email); + + $instance1->customint4 = ENROL_SEND_EMAIL_FROM_NOREPLY; + $DB->update_record('enrol', $instance1); + + $contact = $spokenplugin->get_welcome_email_contact(ENROL_SEND_EMAIL_FROM_NOREPLY, $context); + $this->assertEquals($noreplyuser, $contact); + } + + /** + * Test for getting user enrolment actions. + */ + public function test_get_user_enrolment_actions() { + global $CFG, $DB, $PAGE; + $this->resetAfterTest(); + + // Set page URL to prevent debugging messages. + $PAGE->set_url('/enrol/editinstance.php'); + + $pluginname = 'spoken'; + + // Only enable the spoken enrol plugin. + $CFG->enrol_plugins_enabled = $pluginname; + + $generator = $this->getDataGenerator(); + + // Get the enrol plugin. + $plugin = enrol_get_plugin($pluginname); + + // Create a course. + $course = $generator->create_course(); + + // Create a teacher. + $teacher = $generator->create_user(); + // Enrol the teacher to the course. + $enrolresult = $generator->enrol_user($teacher->id, $course->id, 'editingteacher', $pluginname); + $this->assertTrue($enrolresult); + // Create a student. + $student = $generator->create_user(); + // Enrol the student to the course. + $enrolresult = $generator->enrol_user($student->id, $course->id, 'student', $pluginname); + $this->assertTrue($enrolresult); + + // Login as the teacher. + $this->setUser($teacher); + require_once($CFG->dirroot . '/enrol/locallib.php'); + $manager = new course_enrolment_manager($PAGE, $course); + $userenrolments = $manager->get_user_enrolments($student->id); + $this->assertCount(1, $userenrolments); + + $ue = reset($userenrolments); + $actions = $plugin->get_user_enrolment_actions($manager, $ue); + // spoken enrol has 2 enrol actions -- edit and unenrol. + $this->assertCount(2, $actions); + } +} diff --git a/enrol/spoken/unenrolself.php b/enrol/spoken/unenrolself.php new file mode 100644 index 00000000..4a2c8611 --- /dev/null +++ b/enrol/spoken/unenrolself.php @@ -0,0 +1,63 @@ +. + +/** + * spoken enrolment plugin - support for user spoken unenrolment. + * + * @package enrol_spoken + * @copyright 2010 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require('../../config.php'); + +$enrolid = required_param('enrolid', PARAM_INT); +$confirm = optional_param('confirm', 0, PARAM_BOOL); + +$instance = $DB->get_record('enrol', array('id'=>$enrolid, 'enrol'=>'spoken'), '*', MUST_EXIST); +$course = $DB->get_record('course', array('id'=>$instance->courseid), '*', MUST_EXIST); +$context = context_course::instance($course->id, MUST_EXIST); + +require_login(); +if (!is_enrolled($context)) { + redirect(new moodle_url('/')); +} +require_login($course); + +$plugin = enrol_get_plugin('spoken'); + +// Security defined inside following function. +if (!$plugin->get_unenrolspoken_link($instance)) { + redirect(new moodle_url('/course/view.php', array('id'=>$course->id))); +} + +$PAGE->set_url('/enrol/spoken/unenrolspoken.php', array('enrolid'=>$instance->id)); +$PAGE->set_title($plugin->get_instance_name($instance)); + +if ($confirm and confirm_sesskey()) { + $plugin->unenrol_user($instance, $USER->id); + + \core\notification::success(get_string('youunenrolledfromcourse', 'enrol', $course->fullname)); + + redirect(new moodle_url('/index.php')); +} + +echo $OUTPUT->header(); +$yesurl = new moodle_url($PAGE->url, array('confirm'=>1, 'sesskey'=>sesskey())); +$nourl = new moodle_url('/course/view.php', array('id'=>$course->id)); +$message = get_string('unenrolspokenconfirm', 'enrol_spoken', format_string($course->fullname)); +echo $OUTPUT->confirm($message, $yesurl, $nourl); +echo $OUTPUT->footer(); diff --git a/enrol/spoken/version.php b/enrol/spoken/version.php new file mode 100644 index 00000000..9feaaea6 --- /dev/null +++ b/enrol/spoken/version.php @@ -0,0 +1,30 @@ +. + +/** + * spoken enrolment plugin version specification. + * + * @package enrol_spoken + * @copyright 2010 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2020061500; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2020060900; // Requires this Moodle version + +$plugin->component = 'enrol_spoken'; // Full name of the plugin (used for diagnostics) From 87b557621494fea1193bf0cef8cac2d44f3f950f Mon Sep 17 00:00:00 2001 From: Ganesh Mohite Date: Fri, 19 Mar 2021 22:47:30 +0530 Subject: [PATCH 5/5] saving grades in eventstatus spoken table --- mod/quiz/view.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod/quiz/view.php b/mod/quiz/view.php index 52972306..7f55bc36 100644 --- a/mod/quiz/view.php +++ b/mod/quiz/view.php @@ -209,7 +209,7 @@ $count = $result->num_rows; if ($count) { - $sql = "update training_eventteststatus set part_status = 2 where mdlemail = '".$USER->email."' and mdlcourse_id = ".$quiz->course." and mdlquiz_id = ".$quiz->id." and mdlattempt_id=".$a->id." and part_status = 1"; + $sql = "update training_eventteststatus set part_status = 2, mdlgrade= ".$viewobj->mygrade." where mdlemail = '".$USER->email."' and mdlcourse_id = ".$quiz->course." and mdlquiz_id = ".$quiz->id." and mdlattempt_id=".$a->id." and part_status = 1"; $result = $mysqli->query($sql); } }