'); // 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.
+ }
+ }
+
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 7de8d9f7..7eabf425 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..f5486179 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'),
@@ -65,10 +68,10 @@
global $questionnaireresponseviewers;
$questionnaireresponseviewers = array (
+ QUESTIONNAIRE_STUDENTVIEWRESPONSES_NEVER => get_string('responseviewstudentsnever', 'questionnaire'),
QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENANSWERED => get_string('responseviewstudentswhenanswered', 'questionnaire'),
QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENCLOSED => get_string('responseviewstudentswhenclosed', 'questionnaire'),
- QUESTIONNAIRE_STUDENTVIEWRESPONSES_ALWAYS => get_string('responseviewstudentsalways', 'questionnaire'),
- QUESTIONNAIRE_STUDENTVIEWRESPONSES_NEVER => get_string('responseviewstudentsnever', 'questionnaire'));
+ QUESTIONNAIRE_STUDENTVIEWRESPONSES_ALWAYS => get_string('responseviewstudentsalways', 'questionnaire'));
global $autonumbering;
$autonumbering = array (0 => get_string('autonumberno', '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..ef1d68b7 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) {
@@ -826,9 +851,10 @@ public function count_submissions($userid=false, $groupid=0) {
*
* @param int|bool $userid
* @param int $groupid
+ * @param int|bool $includeincomplete
* @return array
*/
- public function get_responses($userid=false, $groupid=0) {
+ public function get_responses($userid=false, $groupid=0, $includeincomplete=false) {
global $DB;
$params = [];
@@ -840,6 +866,12 @@ public function get_responses($userid=false, $groupid=0) {
$params['groupid'] = $groupid;
}
+ $statuscnd = '';
+ if (!$includeincomplete) {
+ $statuscnd = ' AND r.complete = :status ';
+ $params['status'] = 'y';
+ }
+
// Since submission can be across questionnaires in the case of public questionnaires, need to check the realm.
// Public questionnaires can have responses to multiple questionnaire instances.
if ($this->survey_is_public_master()) {
@@ -848,16 +880,14 @@ public function get_responses($userid=false, $groupid=0) {
'INNER JOIN {questionnaire} q ON r.questionnaireid = q.id ' .
'INNER JOIN {questionnaire_survey} s ON q.sid = s.id ' .
$groupsql .
- 'WHERE s.id = :surveyid AND r.complete = :status' . $groupcnd;
+ 'WHERE s.id = :surveyid' . $statuscnd . $groupcnd;
$params['surveyid'] = $this->sid;
- $params['status'] = 'y';
} else {
$sql = 'SELECT r.* ' .
'FROM {questionnaire_response} r ' .
$groupsql .
- 'WHERE r.questionnaireid = :questionnaireid AND r.complete = :status' . $groupcnd;
+ 'WHERE r.questionnaireid = :questionnaireid' . $statuscnd . $groupcnd;
$params['questionnaireid'] = $this->id;
- $params['status'] = 'y';
}
if ($userid) {
$sql .= ' AND r.userid = :userid';
@@ -1998,8 +2028,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 +2051,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..a926b60f 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,33 @@
$reload = true;
}
+// Delete question permanently.
+if ($delpermanentlyq) {
+ $qid = $delpermanentlyq;
+ $sid = $questionnaire->survey->id;
+ questionnaire_delete_permanently_questions($qid, $sid);
+ $deletedQuestion = $questionnaire->deletequestions[$qid] ?? null;
+ if ($deletedQuestion !== null) {
+ $questionType = \mod_questionnaire\question\question::qtypename($deletedQuestion->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 +204,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 +219,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 +299,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 +359,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 +406,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 +450,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/report.php b/report.php
index b7b16152..ea8331a8 100755
--- a/report.php
+++ b/report.php
@@ -267,6 +267,9 @@
case 'delallresp': // Delete all responses? Ask for confirmation.
require_capability('mod/questionnaire:deleteresponses', $context);
+ // Get all responses including incompletes.
+ $respsallparticipants = $questionnaire->get_responses(false, 0, true);
+
if (!empty($respsallparticipants)) {
// Print the page header.
@@ -357,6 +360,9 @@
throw new \moodle_exception('surveyowner', 'mod_questionnaire');
}
+ // Get all responses including incompletes.
+ $respsallparticipants = $questionnaire->get_responses(false, 0, true);
+
// Available group modes (0 = no groups; 1 = separate groups; 2 = visible groups).
if ($groupmode > 0) {
switch ($currentgroupid) {
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..e7abffef
--- /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 "mod/questionnaire/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 dropdown 1"
+ And I should see "NA"
+ And I press "Yes"
+ Then "//*[@id='id_manageq']//*[contains(.,'[Dropdown Box] (Demo dropdown 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"
+ And 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
+ And I am on "Course 1" course homepage
+ And I follow "My Questionnaire 1"
+ When I navigate to "Questions" in current page administration
+ Then "//*[@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
+ And 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"
+ 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 "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 00000000..8457b57a
Binary files /dev/null and b/tests/fixtures/backup-activity-questionnaire.mbz differ
diff --git a/version.php b/version.php
index ac5eb9bf..1437b5f9 100644
--- a/version.php
+++ b/version.php
@@ -25,7 +25,7 @@
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2022121600.02; // The current module version (Date: YYYYMMDDXX).
+$plugin->version = 2024082200.00; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2022112800.00; // Moodle version (4.1.0).
$plugin->component = 'mod_questionnaire';