From a5d3e3c33b51acb67ebeb89f6bfb13529fcfdf45 Mon Sep 17 00:00:00 2001 From: toanlam Date: Tue, 4 Jun 2024 12:17:51 +0700 Subject: [PATCH] Questionnaire: deletion area (Recycle bin) for deleted/orphaned questions --- .../restore_questionnaire_stepslib.php | 4 +- classes/question/question.php | 10 +- classes/questions_form.php | 84 +++++++++- classes/search/question.php | 4 +- classes/task/cron_task.php | 54 +++++++ db/install.xml | 2 +- db/tasks.php | 11 +- db/upgrade.php | 29 ++++ lang/en/questionnaire.php | 15 +- locallib.php | 143 ++++++++++++++++-- questionnaire.class.php | 37 ++++- questions.php | 98 +++++++++--- settings.php | 4 + styles.css | 18 +++ tests/behat/behat_mod_questionnaire.php | 48 ++++-- tests/behat/deletion_questionnaire.feature | 84 ++++++++++ tests/deletion_question_test.php | 92 +++++++++++ .../backup-activity-questionnaire.mbz | Bin 0 -> 3496 bytes version.php | 2 +- 19 files changed, 667 insertions(+), 72 deletions(-) create mode 100644 classes/task/cron_task.php create mode 100644 tests/behat/deletion_questionnaire.feature create mode 100644 tests/deletion_question_test.php create mode 100644 tests/fixtures/backup-activity-questionnaire.mbz diff --git a/backup/moodle2/restore_questionnaire_stepslib.php b/backup/moodle2/restore_questionnaire_stepslib.php index d5235635..e5dcdd86 100644 --- a/backup/moodle2/restore_questionnaire_stepslib.php +++ b/backup/moodle2/restore_questionnaire_stepslib.php @@ -163,7 +163,9 @@ protected function process_questionnaire_question($data) { $data = (object)$data; $oldid = $data->id; $data->surveyid = $this->get_new_parentid('questionnaire_survey'); - + if (!empty($data->deleted)) { + $data->deleted = time(); + } // Insert the questionnaire_question record. $newitemid = $DB->insert_record('questionnaire_question', $data); $this->set_mapping('questionnaire_question', $oldid, $newitemid, true); diff --git a/classes/question/question.php b/classes/question/question.php index 7b4594b5..2019be49 100644 --- a/classes/question/question.php +++ b/classes/question/question.php @@ -103,8 +103,8 @@ abstract class question { /** @var boolean $required The required flag. */ public $required = 'n'; - /** @var boolean $deleted The deleted flag. */ - public $deleted = 'n'; + /** @var int $deleted The deleted flag. */ + public $deleted = null; /** @var mixed $extradata Any custom data for the question type. */ public $extradata = ''; @@ -577,7 +577,7 @@ public function response_complete($responsedata) { // If $responsedata is webform data, check that its not empty. $answered = isset($responsedata->{'q'.$this->id}) && ($responsedata->{'q'.$this->id} != ''); } - return !($this->required() && ($this->deleted == 'n') && !$answered); + return !($this->required() && (empty($this->deleted)) && !$answered); } /** @@ -642,8 +642,8 @@ public function add($questionrecord, array $choicerecords = null, $calcposition // Set the position to the end. $sql = 'SELECT MAX(position) as maxpos '. 'FROM {questionnaire_question} '. - 'WHERE surveyid = ? AND deleted = ?'; - $params = ['surveyid' => $questionrecord->surveyid, 'deleted' => 'n']; + 'WHERE surveyid = ? AND deleted IS NULL'; + $params = ['surveyid' => $questionrecord->surveyid]; if ($record = $DB->get_record_sql($sql, $params)) { $questionrecord->position = $record->maxpos + 1; } else { diff --git a/classes/questions_form.php b/classes/questions_form.php index 0bb2b469..e0ed6f6d 100644 --- a/classes/questions_form.php +++ b/classes/questions_form.php @@ -50,6 +50,7 @@ public function definition() { $sid = $questionnaire->survey->id; $mform =& $this->_form; + $qidrestore = optional_param(QUESTIONNAIRE_RESTORE_PARAM, 0, PARAM_INT); $mform->addElement('header', 'questionhdr', get_string('addquestions', 'questionnaire')); $mform->addHelpButton('questionhdr', 'questiontypes', 'questionnaire'); @@ -138,7 +139,7 @@ public function definition() { // No page break in first position! if ($tid == QUESPAGEBREAK && $pos == 1) { - $DB->set_field('questionnaire_question', 'deleted', 'y', ['id' => $qid, 'surveyid' => $sid]); + $DB->set_field('questionnaire_question', 'deleted', time(), ['id' => $qid, 'surveyid' => $sid]); if ($records = $DB->get_records_select('questionnaire_question', $select, null, 'position ASC')) { foreach ($records as $record) { $DB->set_field('questionnaire_question', 'position', $record->position - 1, array('id' => $record->id)); @@ -172,7 +173,12 @@ public function definition() { // Begin div qn-container with indent if questionnaire has child. $mform->addElement('html', '
'); } else { - $mform->addElement('html', '
'); // Begin div qn-container. + $containerclass = "qn-container"; + if (isset($qidrestore) && $qidrestore == $question->id) { + $containerclass .= " restored-question"; + } + // Begin div qn-container. + $mform->addElement('html', "
"); } $mextra = array('value' => $question->id, @@ -220,14 +226,15 @@ public function definition() { // Do not allow moving or deleting a page break if immediately followed by a child question // or immediately preceded by a question with a dependency and followed by a non-dependent question. if ($tid == QUESPAGEBREAK) { - if ($nextquestion = $DB->get_record('questionnaire_question', - ['surveyid' => $sid, 'position' => $pos + 1, 'deleted' => 'n'], 'id, name, content') ) { - + $select = 'surveyid = ? AND position = ? AND deleted IS NULL'; + $nextquestion = $DB->get_record_select('questionnaire_question', $select, + [$sid, $pos + 1], 'id, name, content'); + if ($nextquestion) { $nextquestiondependencies = $DB->get_records('questionnaire_dependency', ['questionid' => $nextquestion->id , 'surveyid' => $sid], 'id ASC'); - if ($previousquestion = $DB->get_record('questionnaire_question', - ['surveyid' => $sid, 'position' => $pos - 1, 'deleted' => 'n'], 'id, name, content')) { + if ($previousquestion = $DB->get_record_select('questionnaire_question', $select, + [$sid, $pos - 1], 'id, name, content')) { $previousquestiondependencies = $DB->get_records('questionnaire_dependency', ['questionid' => $previousquestion->id , 'surveyid' => $sid], 'id ASC'); @@ -359,6 +366,66 @@ public function definition() { } } + // Question deletion area. + $mform->addElement('header', 'deletionq', get_string('deletionquetions', 'questionnaire')); + $mform->addHelpButton('deletionq', 'deletionquetions', 'questionnaire'); + $mform->addElement('html', '
'); + if (isset($questionnaire->deletequestions)) { + $restoreimg = $questionnaire->renderer->image_url('i/up'); + $deleteimg = $questionnaire->renderer->image_url('t/delete'); + $rangetimecrontask = questionnaire_get_range_time_permanently(); + foreach ($questionnaire->deletequestions as $deletequestion) { + $delquestiongroup = []; + // Preparing deleted time to display time permanently question. + $timedeleted = $deletequestion->deleted ?? ""; + if ($rangetimecrontask == 0) { + $timedeleted = get_string('recylebindisabled', 'questionnaire'); + } else { + if (!empty($timedeleted)) { + $timedeleted = get_string('timedeletednext7days', 'questionnaire', + date("D j M, Y", $timedeleted + $rangetimecrontask)); + } + } + $qtypeandname = []; + $qtypeandname['name'] = $deletequestion->name; + $qtypeandname['type'] = questionnaire_get_type($deletequestion->type_id); + + $content = format_text( + file_rewrite_pluginfile_urls($deletequestion->content, 'pluginfile.php', + $deletequestion->context->id, + 'mod_questionnaire', 'question', $deletequestion->id), + FORMAT_HTML, ['noclean' => true] + ); + + $qnumber = '

NA

'; + $restorextra = [ + 'value' => $deletequestion->id, + 'alt' => get_string('restorebutton', 'questionnaire'), + 'title' => get_string('restorebutton', 'questionnaire'), + ]; + $deleleextra = [ + 'value' => $deletequestion->id, + 'alt' => get_string('deletepermanentlybutton', 'questionnaire'), + 'title' => get_string('deletepermanentlybutton', 'questionnaire') + ]; + $mform->addElement('html', '
'); // Begin div qn-container. + $delquestiongroup[] =& $mform->createElement('static', 'opentag_' . $deletequestion->id, '', ''); + $delquestiongroup[] =& $mform->createElement('image', 'restorebutton[' . $deletequestion->id . ']', + $restoreimg, $restorextra); + $delquestiongroup[] =& $mform->createElement('image', 'deletebutton[' . $deletequestion->id . ']', + $deleteimg, $deleleextra); + $delquestiongroup[] =& $mform->createElement('static', 'closetag_' . $deletequestion->id, '', ''); + $delquestiongroup[] =& $mform->createElement('static', 'qinfo_' . $deletequestion->id, '', + get_string('questiontypeandname', 'questionnaire', $qtypeandname)); + $delquestiongroup[] =& $mform->createElement('static', 'qinfo_' . $deletequestion->id, '', $timedeleted); + $mform->addGroup($delquestiongroup, 'delquestiongroup', '', ' ', false); + $mform->addElement('static', 'qcontent_'.$deletequestion->id, '', + $qnumber.'
'.$content.'
'); + $mform->addElement('html', '
'); // End div qn-container. + } + } + // Question deletion area. + if ($this->moveq) { $mform->addElement('hidden', 'moveq', $this->moveq); } @@ -373,6 +440,9 @@ public function definition() { $mform->setType('moveq', PARAM_RAW); $mform->addElement('html', '
'); + $mform->setExpanded('questionhdr'); + $mform->setExpanded('manageq'); + $mform->setExpanded('deletionq'); } /** diff --git a/classes/search/question.php b/classes/search/question.php index 0d5d4b60..f38700fb 100644 --- a/classes/search/question.php +++ b/classes/search/question.php @@ -74,8 +74,8 @@ public function get_document($record, $options = []) { // Because there is no database agnostic way to combine all of the possible question content data into one record in // get_recordset_by_timestamp, I need to grab it all now and add it to the document. - $recordset = $DB->get_recordset('questionnaire_question', ['surveyid' => $record->sid, 'deleted' => 'n'], - 'id', 'id,content'); + $recordset = $DB->get_recordset_select('questionnaire_question', + 'surveyid = ? AND deleted IS NULL', [$record->sid], 'id', 'id,content'); // If no question data, don't index this document. if (empty($recordset)) { diff --git a/classes/task/cron_task.php b/classes/task/cron_task.php new file mode 100644 index 00000000..d715d53b --- /dev/null +++ b/classes/task/cron_task.php @@ -0,0 +1,54 @@ +. + +namespace mod_questionnaire\task; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/mod/questionnaire/questionnaire.class.php'); +/** + * A schedule task for mod_questionnaire cron. + * + * @package mod_questionnaire + * @copyright 2022 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cron_task extends \core\task\scheduled_task { + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('cleanrecylebin', 'mod_questionnaire'); + } + + /** + * Run mod_questionnaire cron. + */ + public function execute() { + global $DB; + $rangetimecrontask = questionnaire_get_range_time_permanently(); + $sql = "SELECT * + FROM {questionnaire_question} + WHERE deleted IS NOT NULL + AND deleted < ?"; + if ($deletequestions = $DB->get_records_sql($sql, [time() - $rangetimecrontask])) { + foreach ($deletequestions as $question) { + questionnaire_delete_permanently_questions($question->id, $question->surveyid); + } + } + } +} diff --git a/db/install.xml b/db/install.xml index a1b7af0b..cf28929e 100644 --- a/db/install.xml +++ b/db/install.xml @@ -76,7 +76,7 @@ - + diff --git a/db/tasks.php b/db/tasks.php index 40d241ab..8629011c 100644 --- a/db/tasks.php +++ b/db/tasks.php @@ -36,5 +36,14 @@ 'day' => '*', 'month' => '*', 'dayofweek' => '*' - ) + ), + [ + 'classname' => 'mod_questionnaire\task\cron_task', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*/7', + 'month' => '*', + 'dayofweek' => '*' + ], ); diff --git a/db/upgrade.php b/db/upgrade.php index dd5fd989..12ce8f30 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -1002,6 +1002,35 @@ function xmldb_questionnaire_upgrade($oldversion=0) { upgrade_mod_savepoint(true, 2022121600.02, 'questionnaire'); } + if ($oldversion < 2024060300.00) { + $table = new xmldb_table('questionnaire_question'); + $index = new xmldb_index('quest_question_sididx', XMLDB_INDEX_NOTUNIQUE, ['surveyid', 'deleted']); + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + $field = new xmldb_field('deleted', XMLDB_TYPE_CHAR, '10', XMLDB_UNSIGNED, null, null, null, 'required'); + if ($dbman->field_exists($table, $field)) { + $dbman->change_field_type($table, $field); + } + unset($field); + + $field = new xmldb_field('deleted', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, 'required'); + if ($dbman->field_exists($table, $field)) { + $sql = "UPDATE {questionnaire_question} + SET deleted = ? + WHERE deleted = 'y'"; + $DB->execute($sql, [time()]); + $sql = "UPDATE {questionnaire_question} + SET deleted = null + WHERE deleted = 'n'"; + $DB->execute($sql); + $dbman->change_field_type($table, $field); + } + unset($field); + // Questionnaire savepoint reached. + upgrade_mod_savepoint(true, 2024060300.00, 'questionnaire'); + } + return true; } diff --git a/lang/en/questionnaire.php b/lang/en/questionnaire.php index f6e35b11..4a47a2fe 100644 --- a/lang/en/questionnaire.php +++ b/lang/en/questionnaire.php @@ -92,6 +92,7 @@ $string['closed'] = 'The questionnaire was closed on {$a}. Thanks.'; $string['closedate'] = 'Allow responses until'; $string['closeson'] = 'Questionnaire closes on {$a}'; +$string['cleanrecylebin'] = "Empty Questionnaire 'Recycle bin'"; $string['completionsubmit'] = 'Student must submit this questionnaire to complete it'; $string['condition'] = 'Condition'; $string['confalts'] = '- OR -
Confirmation page'; @@ -103,7 +104,8 @@ $string['confirmdelallresp'] = 'Are you sure you want to delete ALL the responses in this questionnaire?'; $string['confirmdelchildren'] = 'If you delete this question, its child(ren) question(s) will also be deleted:'; $string['confirmdelgroupresp'] = 'Are you sure you want to delete ALL the responses of {$a}?'; -$string['confirmdelquestion'] = 'Are you sure you want to delete the question at position {$a}?'; +$string['confirmdelquestion'] = 'Are you sure you want to move the question at position {$a} to the deletion area?'; +$string['confirmdelpermanentlyq'] = 'Are you sure you want to permanently delete this question?'; $string['confirmdelquestionresps'] = 'This will also delete the {$a} response(s) already given to that question.'; $string['confirmdelresp'] = 'Are you sure you want to delete the response by {$a} ?'; $string['confirmdeletesection'] = 'Are you sure you want to delete feedback section "{$a}"?'; @@ -134,7 +136,10 @@ $string['deletedresp'] = 'Deleted Response'; $string['deleteresp'] = 'Delete this Response'; $string['deletesection'] = 'Delete this section'; +$string['deletepermanentlybutton'] = 'Permanently delete question'; $string['deletingresp'] = 'Deleting Response'; +$string['deletequestionsolderthan'] = 'Delete questions older than'; +$string['deletesettingdescription'] = 'The scheduled task will delete questions in deletion area that are older than this many days'; $string['dependencies'] = 'Dependencies'; $string['dependquestion'] = 'Parent Question'; $string['dependquestion_help'] = 'You can select a parent question and a choice option for this question. A child question will only be displayed @@ -280,6 +285,8 @@ $string['leftpart'] = '{$a->min} is {$a->leftlabel}'; $string['leftpartdefault'] = '{$a->min} is minimum slider range'; $string['managequestions'] = 'Manage questions'; +$string['deletionquetions'] = 'Question deletion area'; +$string['deletionquetions_help'] = 'Deleted or otherwise orphaned questions will be first moved here rather than be outright deleted. The questions can be permanently deleted either manually through a user pressing the \'X\' icon, or the system will automatically remove any questions after a week. Use the \'up arrow\' button to restore the question.'; $string['managequestions_help'] = 'In the Manage questions section of the Edit Questions page, you can conduct a number of operations on a Questionnaire\'s questions.'; $string['managequestions_link'] = 'mod/questionnaire/questions#Manage_questions'; $string['mandatory'] = 'Mandatory - All these dependencies must be fulfilled.'; @@ -510,6 +517,7 @@ $string['questiontypes'] = 'Question types'; $string['questiontypes_help'] = 'See the Moodle Documentation below'; $string['questiontypes_link'] = 'mod/questionnaire/questions#Question_Types'; +$string['questiontypeandname'] = '[{$a->type}] ({$a->name})'; $string['radiobuttons'] = 'Radio Buttons'; $string['radiobuttons_help'] = 'In this question type, the respondent must select one out of the choices offered.'; $string['radiobuttons_link'] = 'mod/questionnaire/questions#Radio_Buttons'; @@ -517,6 +525,7 @@ $string['ratescale'] = 'Rate (scale 1..5)'; $string['ratescale_help'] = 'See the Moodle Documentation below'; $string['ratescale_link'] = 'mod/questionnaire/questions#Rate_.28scale_1..5.29'; +$string['recylebindisabled'] = 'Automatic deletion is disabled'; $string['realm'] = 'Questionnaire Type'; $string['realm_help'] = '* **There are three types of questionnaires:** * Private - belongs to the course it is defined in only. @@ -524,7 +533,7 @@ * Public - can be shared among courses.'; $string['realm_link'] = 'mod/questionnaire/qsettings#Questionnaire_Type'; $string['redirecturl'] = 'The URL to which a user is redirected after completing this questionnaire.'; -$string['remove'] = 'Delete'; +$string['remove'] = 'Move to deletion area'; $string['removenotinuse'] = 'This questionnaire used to depend on a Public questionnaire which has been deleted. It can no longer be used and should be deleted.'; $string['required'] = 'Response is required'; @@ -565,6 +574,7 @@ Users can leave the questionnaire unfinished and resume from the save point at a later date.'; $string['resume_link'] = 'mod/questionnaire/mod#Save/Resume_answers'; $string['resumesurvey'] = 'Resume questionnaire'; +$string['restorebutton'] = 'Restore this question'; $string['return'] = 'Return'; $string['rightlabel'] = 'Right label'; $string['rightpart'] = ' and {$a->max} is {$a->rightlabel}'; @@ -644,6 +654,7 @@ $string['thousands'] = 'Do not use thousands separators.'; $string['title'] = 'Title'; $string['title_help'] = 'Title of this questionnaire, which will appear at the top of every page. By default Title is set to the questionnaire Name, but you can edit it as you like.'; +$string['timedeletednext7days'] = 'Time of permanent deletion: night of {$a}'; $string['today'] = 'today'; $string['total'] = 'Total'; $string['totalofnumbers'] = 'Total of numbers entered'; diff --git a/locallib.php b/locallib.php index 4fce8590..f537070f 100644 --- a/locallib.php +++ b/locallib.php @@ -47,6 +47,9 @@ define('QUESTIONNAIRE_DEFAULT_PAGE_COUNT', 20); +define('QUESTIONNAIRE_CONFIRM_DELETE_PERMANENTLY', 'confirmdelpermanentlyq'); +define('QUESTIONNAIRE_RESTORE_PARAM', 'restoreq'); + global $questionnairetypes; $questionnairetypes = array (QUESTIONNAIREUNLIMITED => get_string('qtypeunlimited', 'questionnaire'), QUESTIONNAIREONCE => get_string('qtypeonce', 'questionnaire'), @@ -300,6 +303,85 @@ function questionnaire_delete_survey($sid, $questionnaireid) { return $status; } +/** + * Delete permanently questions and data reference. + * + * @param int $qid question id. + * @param int $sid survey question id. + * @return void + */ +function questionnaire_delete_permanently_questions($qid, $sid) { + global $DB; + $select = 'id = :id AND surveyid = :sid AND deleted IS NOT NULL'; + $DB->delete_records_select('questionnaire_question', $select , ['id' => $qid, 'sid' => $sid]); + $DB->delete_records('questionnaire_response', ['questionnaireid' => $qid]); + questionnaire_delete_responses($qid); + questionnaire_delete_dependencies($qid); +} + +/** + * Log question deleted event. + * + * @param int $cmid of module. + * @param string $questiontype of question. + * @param int $courseid of question. + * @return void. + */ +function questionnaire_observe_event_delete($cmid, $questiontype, $courseid) { + $context = context_module::instance($cmid); + $params = [ + 'context' => $context, + 'courseid' => $courseid, + 'other' => ['questiontype' => $questiontype] + ]; + $event = \mod_questionnaire\event\question_deleted::create($params); + $event->trigger(); +} + +/** + * Restore deleted questions. + * + * @param int $qid question id. + * @param int $sid survey id. + * @return void + */ +function questionnaire_restore_deleted_question($qid, $sid) { + global $DB; + // Get current deleted question and last position. + $sql = "SELECT *, ( + SELECT position + 1 + FROM {questionnaire_question} + WHERE surveyid = ? + AND deleted IS NULL + ORDER BY position DESC + LIMIT 1 ) as lastposition + FROM {questionnaire_question} + WHERE id = ? + AND surveyid = ? + AND deleted IS NOT NULL"; + $question = $DB->get_record_sql($sql, [$sid, $qid, $sid]); + if ($question) { + // Update question. + $updatesql = "UPDATE {questionnaire_question} + SET deleted = null, position = :position + WHERE id = :qid + AND surveyid = :sid"; + $params = [ + 'position' => $question->lastposition ?? 1, + 'qid' => $qid, + 'sid' => $sid + ]; + $DB->execute($updatesql, $params); + } +} + +/** + * Get range of time permanently in setup cron task. + */ +function questionnaire_get_range_time_permanently() { + return get_config('questionnaire_questiondeletion', 'duration'); +} + /** * Delete the response. * @param stdClass $response @@ -373,6 +455,21 @@ function questionnaire_delete_dependencies($qid) { return true; } +/** + * Delete all page break deleted. + * + * @param int $sid question survey id. + */ +function questionnaire_delete_pagebreaks($sid) { + global $DB; + $DB->delete_records_select('questionnaire_question', + 'surveyid = :sid AND deleted IS NOT NULL AND type_id = :type_id', + [ + 'sid' => $sid, + 'type_id' => QUESPAGEBREAK + ]); +} + /** * Get a survey selection records. * @param int $courseid @@ -750,7 +847,8 @@ function questionnaire_check_page_breaks($questionnaire) { $delpb = 0; $sid = $questionnaire->survey->id; $positions = array(); - if ($questions = $DB->get_records('questionnaire_question', ['surveyid' => $sid, 'deleted' => 'n'], 'position')) { + if ($questions = $DB->get_records_select('questionnaire_question', + 'surveyid = :sid AND deleted IS NULL', ['sid' => $sid], 'position')) { foreach ($questions as $key => $qu) { $newqu = new stdClass(); $newqu->question_id = $key; @@ -784,11 +882,15 @@ function questionnaire_check_page_breaks($questionnaire) { $delpb ++; $msg .= get_string("checkbreaksremoved", "questionnaire", $delpb).'
'; // Need to reload questions. - if ($questions = $DB->get_records('questionnaire_question', ['surveyid' => $sid, 'deleted' => 'n'], 'id')) { - $DB->set_field('questionnaire_question', 'deleted', 'y', ['id' => $qid, 'surveyid' => $sid]); - $select = 'surveyid = ' . $sid . ' AND deleted = \'n\' AND position > ' . - $questions[$qid]->position; - if ($records = $DB->get_records_select('questionnaire_question', $select, null, 'position ASC')) { + if ($questions = $DB->get_records_select('questionnaire_question', + 'surveyid = :sid AND deleted IS NULL', ['sid' => $sid], 'id')) { + + + $DB->set_field('questionnaire_question', 'deleted', time(), ['id' => $qid, 'surveyid' => $sid]); + $select = 'surveyid = :sid AND deleted IS NULL AND position > :pos'; + $records = $DB->get_records_select('questionnaire_question', $select, + ['sid' => $sid, 'pos' => $questions[$qid]->position], 'position ASC'); + if ($records) { foreach ($records as $record) { $DB->set_field('questionnaire_question', 'position', $record->position - 1, ['id' => $record->id]); } @@ -833,9 +935,11 @@ function questionnaire_check_page_breaks($questionnaire) { if (($prevtypeid != QUESPAGEBREAK && $diffdependencies != 0) || (!isset($qu['dependencies']) && isset($prevdependencies))) { - $sql = 'SELECT MAX(position) as maxpos FROM {questionnaire_question} ' . - 'WHERE surveyid = ' . $questionnaire->survey->id . ' AND deleted = \'n\''; - if ($record = $DB->get_record_sql($sql)) { + $sql = "SELECT MAX(position) as maxpos + FROM {questionnaire_question} + WHERE surveyid = :sid + AND deleted IS NULL"; + if ($record = $DB->get_record_sql($sql, ['sid' => $questionnaire->survey->id])) { $pos = $record->maxpos + 1; } else { $pos = 1; @@ -949,3 +1053,24 @@ function questionnaire_get_standard_page_items($id = null, $a = null) { return (array($cm, $course, $questionnaire)); } + + +/** + * Count responses already saved for that question. + * + * @param int $qid question id. + * @param int $qtype question type. + * @return int number or 0 if responses were not found. + */ +function count_reponses_question($qid, $qtype) { + global $DB; + + $countresps = 0; + if ($qtype != QUESSECTIONTEXT) { + $responsetable = $DB->get_field('questionnaire_question_type', 'response_table', array('typeid' => $qtype)); + if (!empty($responsetable)) { + $countresps = $DB->count_records('questionnaire_'.$responsetable, array('question_id' => $qid)); + } + } + return $countresps; +} diff --git a/questionnaire.class.php b/questionnaire.class.php index e366f70b..f50636f2 100644 --- a/questionnaire.class.php +++ b/questionnaire.class.php @@ -38,6 +38,11 @@ class questionnaire { */ public $questions = []; + /** + * @var \mod_questionnaire\question\question[] $deletequestions + */ + public $deletequestions = []; + /** * The survey record. * @var object $survey @@ -105,6 +110,27 @@ public function __construct(&$course, &$cm, $id = 0, $questionnaire = null, $add $this->responses = []; } + /** + * Get all delete questions by survey id. + * + * @return void + * @throws dml_exception + */ + public function get_delete_questions() { + global $DB; + $sql = "SELECT * + FROM {questionnaire_question} + WHERE deleted IS NOT NULL + AND surveyid = ? AND type_id != ? + ORDER BY deleted DESC"; + if ($records = $DB->get_records_sql($sql, [$this->sid, QUESPAGEBREAK])) { + foreach ($records as $record) { + $this->deletequestions[$record->id] = \mod_questionnaire\question\question::question_builder($record->type_id, + $record, $this->context); + } + } + } + /** * Adding a survey record to the object. * @param int $sid @@ -136,9 +162,8 @@ public function add_questions($sid = false) { $this->questionsbysec = []; } - $select = 'surveyid = ? AND deleted = ?'; - $params = [$sid, 'n']; - if ($records = $DB->get_records_select('questionnaire_question', $select, $params, 'position')) { + $select = 'surveyid = ? AND deleted IS NULL'; + if ($records = $DB->get_records_select('questionnaire_question', $select, [$sid], 'position')) { $sec = 1; $isbreak = false; foreach ($records as $record) { @@ -1998,8 +2023,8 @@ private function response_select_max_sec($rid) { global $DB; $pos = $this->response_select_max_pos($rid); - $select = 'surveyid = ? AND type_id = ? AND position < ? AND deleted = ?'; - $params = [$this->sid, QUESPAGEBREAK, $pos, 'n']; + $select = 'surveyid = ? AND type_id = ? AND position < ? AND deleted IS NULL'; + $params = [$this->sid, QUESPAGEBREAK, $pos]; $max = $DB->count_records_select('questionnaire_question', $select, $params) + 1; return $max; @@ -2021,7 +2046,7 @@ private function response_select_max_pos($rid) { 'WHERE a.response_id = ? AND '. 'q.id = a.question_id AND '. 'q.surveyid = ? AND '. - 'q.deleted = \'n\''; + 'q.deleted IS NULL'; if ($record = $DB->get_record_sql($sql, array($rid, $this->sid))) { $newmax = (int)$record->num; if ($newmax > $max) { diff --git a/questions.php b/questions.php index 6175512c..9977432e 100644 --- a/questions.php +++ b/questions.php @@ -33,6 +33,8 @@ $delq = optional_param('delq', 0, PARAM_INT); // Question id to delete. $qtype = optional_param('type_id', 0, PARAM_INT); // Question type. $currentgroupid = optional_param('group', 0, PARAM_INT); // Group id. +$delpermanentlyq = optional_param('delpermanentlyq', 0, PARAM_INT); // Question id to delete. +$restoreq = optional_param(QUESTIONNAIRE_RESTORE_PARAM, 0, PARAM_INT); // Question id to restore question. if (! $cm = get_coursemodule_from_id('questionnaire', $id)) { throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); @@ -59,6 +61,7 @@ $PAGE->set_context($context); $questionnaire = new questionnaire($course, $cm, 0, $questionnaire); +$questionnaire->get_delete_questions(); // Add renderer and page objects to the questionnaire object for display use. $questionnaire->add_renderer($PAGE->get_renderer('mod_questionnaire')); @@ -85,15 +88,26 @@ $questionnaireid = $questionnaire->id; // Need to reload questions before setting deleted question to 'y'. - $questions = $DB->get_records('questionnaire_question', ['surveyid' => $sid, 'deleted' => 'n'], 'id') ?? []; - $DB->set_field('questionnaire_question', 'deleted', 'y', ['id' => $qid, 'surveyid' => $sid]); + $questions = $DB->get_records_select('questionnaire_question', + 'surveyid = :sid AND deleted IS NULL', ['sid' => $sid], 'id'); + if (isset($questions[$qid]) && $questions[$qid]->type_id == QUESPAGEBREAK) { + $DB->delete_records('questionnaire_question', ['id' => $qid]); + } else { + $updatesql = "UPDATE {questionnaire_question} + SET deleted = ? + WHERE id = ? + AND surveyid = ?"; + $DB->execute($updatesql, [time(), $qid, $sid]); + } // Delete all dependency records for this question. questionnaire_delete_dependencies($qid); + // Delete all page break that references to question deleted. + questionnaire_delete_pagebreaks($sid); // Just in case the page is refreshed (F5) after a question has been deleted. if (isset($questions[$qid])) { - $select = 'surveyid = '.$sid.' AND deleted = \'n\' AND position > '. + $select = 'surveyid = '.$sid.' AND deleted IS NULL AND position > '. $questions[$qid]->position; } else { redirect($CFG->wwwroot.'/mod/questionnaire/questions.php?id='.$questionnaire->cm->id); @@ -113,21 +127,15 @@ questionnaire_delete_responses($qid); // If no questions left in this questionnaire, remove all responses. - if ($DB->count_records('questionnaire_question', ['surveyid' => $sid, 'deleted' => 'n']) == 0) { + if ($DB->count_records_select('questionnaire_question', + 'surveyid = :sid AND deleted IS NULL', ['sid' => $sid]) == 0) { $DB->delete_records('questionnaire_response', ['questionnaireid' => $qid]); } } // Log question deleted event. - $context = context_module::instance($questionnaire->cm->id); $questiontype = \mod_questionnaire\question\question::qtypename($questionnaire->questions[$qid]->type_id); - $params = array( - 'context' => $context, - 'courseid' => $questionnaire->course->id, - 'other' => array('questiontype' => $questiontype) - ); - $event = \mod_questionnaire\event\question_deleted::create($params); - $event->trigger(); + questionnaire_observe_event_delete($questionnaire->cm->id, $questiontype, $questionnaire->course->id); if ($questionnairehasdependencies) { $SESSION->questionnaire->validateresults = questionnaire_check_page_breaks($questionnaire); @@ -135,6 +143,30 @@ $reload = true; } +// Delete question permanently. +if ($delpermanentlyq) { + $qid = $delpermanentlyq; + $sid = $questionnaire->survey->id; + questionnaire_delete_permanently_questions($qid, $sid); + $questiontype = \mod_questionnaire\question\question::qtypename($questionnaire->deletequestions[$qid]->type_id); + questionnaire_observe_event_delete($questionnaire->cm->id, $questiontype, $questionnaire->course->id); + $url = new moodle_url('/mod/questionnaire/questions.php', ['id' => $questionnaire->cm->id]); + $PAGE->set_url($url->out(false)); + $reload = true; +} + +// Restore question. +if ($restoreq) { + $qid = $restoreq; + $qdeleted = isset($questionnaire->deletequestions[$qid]) ? $questionnaire->deletequestions[$qid] : false; + if ($qid && $qdeleted) { + questionnaire_restore_deleted_question($qid, $qdeleted->surveyid); + } + $url = new moodle_url('/mod/questionnaire/questions.php', ['id' => $questionnaire->cm->id]); + $PAGE->set_url($url->out(false)); + $reload = true; +} + if ($action == 'main') { $questionsform = new \mod_questionnaire\questions_form('questions.php', $moveq); $sdata = clone($questionnaire->survey); @@ -169,6 +201,10 @@ $qformdata->removebutton = $exformdata->removebutton; } else if (isset($exformdata->requiredbutton)) { $qformdata->requiredbutton = $exformdata->requiredbutton; + } else if (isset($exformdata->deletebutton)) { + $qformdata->deletebutton = $exformdata->deletebutton; + } else if (isset($exformdata->restorebutton)) { + $qformdata->restorebutton = $exformdata->restorebutton; } // Insert a section break. @@ -180,7 +216,8 @@ // Delete section breaks without asking for confirmation. if ($qtype == QUESPAGEBREAK) { - redirect($CFG->wwwroot.'/mod/questionnaire/questions.php?id='.$questionnaire->cm->id.'&delq='.$qid); + redirect(new \moodle_url('/mod/questionnaire/questions.php', + ['id' => $questionnaire->cm->id, 'delq' => $qid])); } $action = "confirmdelquestion"; @@ -259,6 +296,12 @@ // Validates page breaks for depend questions. $SESSION->questionnaire->validateresults = questionnaire_check_page_breaks($questionnaire); $reload = true; + } else if (isset($qformdata->deletebutton)) { + $action = QUESTIONNAIRE_CONFIRM_DELETE_PERMANENTLY; + } else if (isset($qformdata->restorebutton)) { + $qid = key($qformdata->restorebutton); + redirect(new moodle_url('/mod/questionnaire/questions.php', + ['id' => $questionnaire->cm->id, QUESTIONNAIRE_RESTORE_PARAM => $qid])); } } @@ -313,6 +356,7 @@ if ($reload) { unset($questionsform); $questionnaire = new questionnaire($course, $cm, $questionnaire->id, null); + $questionnaire->get_delete_questions(); // Add renderer and page objects to the questionnaire object for display use. $questionnaire->add_renderer($PAGE->get_renderer('mod_questionnaire')); $questionnaire->add_page(new \mod_questionnaire\output\questionspage()); @@ -359,14 +403,7 @@ $question = $questionnaire->questions[$qid]; $qtype = $question->type_id; - // Count responses already saved for that question. - $countresps = 0; - if ($qtype != QUESSECTIONTEXT) { - $responsetable = $DB->get_field('questionnaire_question_type', 'response_table', array('typeid' => $qtype)); - if (!empty($responsetable)) { - $countresps = $DB->count_records('questionnaire_'.$responsetable, array('question_id' => $qid)); - } - } + $countresps = count_reponses_question($qid, $qtype); // Needed to print potential media in question text. @@ -410,6 +447,25 @@ } $questionnaire->page->add_to_page('formarea', $questionnaire->renderer->confirm($msg, $buttonyes, $buttonno)); +} else if ($action === QUESTIONNAIRE_CONFIRM_DELETE_PERMANENTLY) { + $qid = key($qformdata->deletebutton); + $qtype = $questionnaire->deletequestions[$qid]->type_id; + $questiondelete = $questionnaire->deletequestions[$qid]; + $countresps = count_reponses_question($qid, $qtype); + + $urlno = new moodle_url("/mod/questionnaire/questions.php", ['id' => $questionnaire->cm->id]); + $urlyes = new moodle_url("/mod/questionnaire/questions.php", ['id' => $questionnaire->cm->id, "delpermanentlyq" => $qid]); + $buttonyes = new single_button($urlyes, get_string('yes')); + $buttonno = new single_button($urlno, get_string('no')); + $msg = '

'.get_string('confirmdelpermanentlyq', 'questionnaire').'

'; + if ($countresps !== 0) { + $msg .= '

'.get_string('confirmdelquestionresps', 'questionnaire', $countresps).'

'; + } + $msg .= '
'; + $msg .= '
NA ('. $questiondelete->name .') +
'.$questiondelete->content.'
'; + + $questionnaire->page->add_to_page('formarea', $questionnaire->renderer->confirm($msg, $buttonyes, $buttonno)); } else { $questionnaire->page->add_to_page('formarea', $questionsform->render()); } diff --git a/settings.php b/settings.php index 6a21c51d..7f6ba9c1 100644 --- a/settings.php +++ b/settings.php @@ -52,4 +52,8 @@ $settings->add(new admin_setting_configcheckbox('questionnaire/allowemailreporting', get_string('configemailreporting', 'questionnaire'), get_string('configemailreportinglong', 'questionnaire'), 0)); + + $settings->add(new admin_setting_configduration('questionnaire_questiondeletion/duration', + get_string('deletequestionsolderthan', 'questionnaire'), + get_string('deletesettingdescription', 'questionnaire'), 7 * 86400)); } diff --git a/styles.css b/styles.css index 41dcd324..81922ad8 100644 --- a/styles.css +++ b/styles.css @@ -439,6 +439,24 @@ td.selected { margin-left: 20px; } +#page-mod-questionnaire-questions .qcontainer .restored-question { + border: 1px solid #008196; + background: #fff2d8; + color: #343a40; +} + +#page-mod-questionnaire-questions .qcontainer .timedeletednext7days { + color: #f00; +} + +#page-mod-questionnaire-questions .generalbox .modal-content { + border: 1px solid #b0dfeb; +} + +#page-mod-questionnaire-questions #id_deletionq .qn-question { + margin-left: 50px; +} + .mod_questionnaire_flex-container { display: inline-flex; } diff --git a/tests/behat/behat_mod_questionnaire.php b/tests/behat/behat_mod_questionnaire.php index 57c97c58..5f779f80 100644 --- a/tests/behat/behat_mod_questionnaire.php +++ b/tests/behat/behat_mod_questionnaire.php @@ -203,6 +203,22 @@ public function has_questions_and_responses($questionnairename) { $this->add_response_data($questionnaire->id, $questionnaire->sid); } + /** + * Custom deleteddate for run cron task. + * + * @Given /^I custom deleted date in table questionnaire question for cron task$/ + */ + public function i_custom_deleted_date_in_table_questionnaire_question_for_cron_task() { + global $DB; + if (!$questionnaire = $DB->get_records_select("questionnaire_question", 'deleted IS NOT NULL')) { + throw new ExpectationException('Invalid questionnaire name specified.', $this->getSession()); + } + foreach ($questionnaire as $question) { + $question->deleted = $question->deleted - 8 * 24 * 60 * 60; + $DB->set_field('questionnaire_question', 'deleted', $question->deleted, ['id' => $question->id]); + } + } + /** * Adds a question data to the given survey id. * @@ -213,25 +229,25 @@ private function add_question_data($sid) { $questiondata = array( array("id", "surveyid", "name", "type_id", "result_id", "length", "precise", "position", "content", "required", "deleted", "dependquestion", "dependchoice"), - array("1", $sid, "own car", "1", null, "0", "0", "1", "

Do you own a car?

", "y", "n", "0", "0"), - array("2", $sid, "optional", "2", null, "20", "25", "3", "

What is the colour of your car?

", "y", "n", "121", + array("1", $sid, "own car", "1", null, "0", "0", "1", "

Do you own a car?

", "y", null, "0", "0"), + array("2", $sid, "optional", "2", null, "20", "25", "3", "

What is the colour of your car?

", "y", null, "121", "0"), - array("3", $sid, null, "99", null, "0", "0", "2", "break", "n", "n", "0", "0"), + array("3", $sid, null, "99", null, "0", "0", "2", "break", "n", null, "0", "0"), array("4", $sid, "optional2", "1", null, "0", "0", "5", "

Do you sometimes use public transport to go to work?

", - "y", "n", "0", "0"), - array("5", $sid, null, "99", null, "0", "0", "4", "break", "n", "n", "0", "0"), - array("6", $sid, "entertext", "2", null, "20", "10", "6", "

Enter no more than 10 characters.

", "n", "n", "0", + "y", null, "0", "0"), + array("5", $sid, null, "99", null, "0", "0", "4", "break", "n", null, "0", "0"), + array("6", $sid, "entertext", "2", null, "20", "10", "6", "

Enter no more than 10 characters.

", "n", null, "0", "0"), - array("7", $sid, "q7", "5", null, "0", "0", "7", "

Check all that apply

", "n", "n", "0", "0"), - array("8", $sid, "q8", "9", null, "0", "0", "8", "

Enter today's date

", "n", "n", "0", "0"), - array("9", $sid, "q9", "6", null, "0", "0", "9", "

Choose One

", "n", "n", "0", "0"), - array("10", $sid, "q10", "3", null, "5", "0", "10", "

Write an essay

", "n", "n", "0", "0"), - array("11", $sid, "q11", "10", null, "10", "0", "11", "

Enter a number

", "n", "n", "0", "0"), - array("12", $sid, "q12", "4", null, "1", "0", "13", "

Choose a colour

", "n", "n", "0", "0"), - array("13", $sid, "q13", "8", null, "5", "1", "14", "

Rate this.

", "n", "n", "0", "0"), - array("14", $sid, null, "99", null, "0", "0", "12", "break", "n", "y", "0", "0"), - array("15", $sid, null, "99", null, "0", "0", "12", "break", "n", "n", "0", "0"), - array("16", $sid, "Q1", "10", null, "3", "2", "15", "Enter a number


", "y", "n", "0", "0") + array("7", $sid, "q7", "5", null, "0", "0", "7", "

Check all that apply

", "n", null, "0", "0"), + array("8", $sid, "q8", "9", null, "0", "0", "8", "

Enter today's date

", "n", null, "0", "0"), + array("9", $sid, "q9", "6", null, "0", "0", "9", "

Choose One

", "n", null, "0", "0"), + array("10", $sid, "q10", "3", null, "5", "0", "10", "

Write an essay

", "n", null, "0", "0"), + array("11", $sid, "q11", "10", null, "10", "0", "11", "

Enter a number

", "n", null, "0", "0"), + array("12", $sid, "q12", "4", null, "1", "0", "13", "

Choose a colour

", "n", null, "0", "0"), + array("13", $sid, "q13", "8", null, "5", "1", "14", "

Rate this.

", "n", null, "0", "0"), + array("14", $sid, null, "99", null, "0", "0", "12", "break", "n", time(), "0", "0"), + array("15", $sid, null, "99", null, "0", "0", "12", "break", "n", null, "0", "0"), + array("16", $sid, "Q1", "10", null, "3", "2", "15", "Enter a number


", "y", null, "0", "0"), ); $choicedata = array( diff --git a/tests/behat/deletion_questionnaire.feature b/tests/behat/deletion_questionnaire.feature new file mode 100644 index 00000000..48a94250 --- /dev/null +++ b/tests/behat/deletion_questionnaire.feature @@ -0,0 +1,84 @@ +@mod @mod_questionnaire @_file_upload +Feature: Deletion questions area + In order to manage deletion question of questionnaire in a course + As a teacher + I need to manage the delete questions in questionnaire. + And as admin + I need to setup time for schedule task to run cron job to deteting questionnaire. + + 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 | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And I log in as "teacher1" + And I am on the "Course 1" "restore" page + And I press "Manage backup files" + And I upload "tests/fixtures/backup-activity-questionnaire.mbz" file to "Files" filemanager + And I press "Save changes" + Then I restore "backup-activity-questionnaire.mbz" backup into "Course 1" course using this options: + + @javascript + Scenario: Manage deletion questionnaire area + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "My Questionnaire 1" + And I navigate to "Questions" in current page administration + And I should see "Question deletion area" + And I should see "[Dropdown Box] (Demo dropdown 1)" + And I should see "Demo dropdown 1" + And I should see "[Numeric] (Demo numeric 1)" + And I should see "Demo numeric 1" + And I should see "NA" + And I click on "(//input[@type='image' and @title='Move to deletion area'])[1]" "xpath_element" + And I should see "Confirm" + And I should see "Are you sure you want to move the question at position 1 (Demo checkbox 1) to the deletion area?" + And I press "Yes" + And "//*[@id='id_manageq']//*[contains(.,'[Check Boxes] (Demo checkbox 1)')]" "xpath_element" should not exist + And I wait until the page is ready + And I click on "(//input[@type='image' and @title='Restore this question'])[2]" "xpath_element" + And I wait until the page is ready + And "//*[@class='qn-container restored-question']" "xpath_element" should exist + And I click on "(//input[@type='image' and @title='Permanently delete question'])[2]" "xpath_element" + And I should see "Are you sure you want to permanently delete this question?" + And I should see "Demo numeric 1" + And I should see "NA" + And I press "Yes" + Then "//*[@id='id_manageq']//*[contains(.,'[Numeric] (Demo numeric 1)')]" "xpath_element" should not exist + + @javascript + Scenario: Cron task for deletion questionnaire + And I custom deleted date in table questionnaire question for cron task + Given I log in as "admin" + And I navigate to "Server > Tasks > Scheduled tasks" in site administration + And I click on "Empty Questionnaire 'Recycle bin'" "link" + And I set the field "id_minute" to "*/1" + And I set the field "id_day" to "*" + And I press "Save changes" + Given I am on "Course 1" course homepage + And I follow "My Questionnaire 1" + Then I navigate to "Questions" in current page administration + Then I click on "(//input[@type='image' and @title='Move to deletion area'])[1]" "xpath_element" + Then I press "Yes" + And I wait "61" seconds + Then I trigger cron + Given I am on "Course 1" course homepage + And I follow "My Questionnaire 1" + Then I navigate to "Questions" in current page administration + And "//*[@id='id_deletionq']//*[contains(.,'[Dropdown Box] (Demo dropdown 1)')]" "xpath_element" should not exist + And "//*[@id='id_deletionq']//*[contains(.,'[Numeric] (Demo numeric 1)')]" "xpath_element" should not exist + And "//*[@id='id_deletionq']//*[contains(.,'[Check Boxes] (Demo checkbox 1)')]" "xpath_element" should exist + Then I navigate to "Plugins > Activity modules > Questionnaire" in site administration + And I set the field "Delete questions older than" to "0" + And I press "Save changes" + Given I am on "Course 1" course homepage + And I follow "My Questionnaire 1" + Then I navigate to "Questions" in current page administration + And I should see "Automatic deletion is disabled" diff --git a/tests/deletion_question_test.php b/tests/deletion_question_test.php new file mode 100644 index 00000000..f610a2b3 --- /dev/null +++ b/tests/deletion_question_test.php @@ -0,0 +1,92 @@ +. + +/** + * PHPUnit questionnaire generator tests + * + * @package mod_questionnaire + * @copyright 2022 Mike Churchward (mike@churchward.ca) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot.'/mod/questionnaire/locallib.php'); +require_once($CFG->dirroot.'/mod/questionnaire/classes/question/question.php'); +require_once($CFG->dirroot.'/mod/questionnaire/questionnaire.class.php'); + +/** + * Unit tests for {@link questionnaire_deletion_question_testcase} + * + * @group mod_questionnaire + */ +class deletion_question_test extends advanced_testcase { + public function setUp(): void { + $this->create_question_by_type(QUESDATE, + ['name' => 'DEMODATE1', 'content' => 'Demo date question 1', 'deleted' => time()]); + $this->create_question_by_type(QUESDATE, + ['name' => 'DEMODATE2', 'content' => 'Demo date question 2']); + $this->create_question_by_type(QUESDATE, + ['name' => 'DEMODATE3', 'content' => 'Demo date question 3', 'deleted' => time()]); + $this->create_question_by_type(QUESTEXT, + ['name' => 'DEMOTEXT4', 'content' => 'Demo text question 4']); + } + + /** + * Test restore deleted question function. + */ + public function test_restore_deleted_question() : void { + global $DB; + $question = $DB->get_record_select('questionnaire_question', 'name = ?', ['DEMODATE1']); + questionnaire_restore_deleted_question($question->id, $question->surveyid); + $question = $DB->get_record('questionnaire_question', ['id' => $question->id]); + $this->assertEquals($question->position, 1); + } + + /** + * Testing delete permanently question. + */ + public function test_delete_permanently_question() : void { + global $DB; + $question = $DB->get_record_select('questionnaire_question', 'name = ?', ['DEMODATE1']); + questionnaire_delete_permanently_questions($question->id, $question->surveyid); + $question = $DB->get_record('questionnaire_question', ['id' => $question->id]); + $this->assertEquals($question, false); + } + + /** + * Create a question by type of question. + * + * @param int question type. + * @param string class name of question. + * @param object data of question. + * @return void + */ + public function create_question_by_type($qtype, $qdata) :void { + $this->resetAfterTest(true); + $course = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_questionnaire'); + $questionnaire = $generator->create_instance(['course' => $course->id]); + $qdata['type_id'] = $qtype; + $qdata['surveyid'] = $questionnaire->sid; + $qdata['name'] = isset($qdata['name']) ? $qdata['name'] : 'Q1'; + $qdata['content'] = isset($qdata['content']) ? $qdata['content'] : 'Test content'; + $qdata['position'] = isset($qdata['position']) ? $qdata['position'] : 1; + $question = $generator->create_question($questionnaire, $qdata, null); + } +} \ No newline at end of file diff --git a/tests/fixtures/backup-activity-questionnaire.mbz b/tests/fixtures/backup-activity-questionnaire.mbz new file mode 100644 index 0000000000000000000000000000000000000000..8457b57a9562995b83a6e44f9ba62c2354b312c3 GIT binary patch literal 3496 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 = 2022121600.02; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2024060300.00; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2022112800.00; // Moodle version (4.1.0). $plugin->component = 'mod_questionnaire';