'); // 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,68 @@ 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'),
+ 'class' => 'mod_questionnaire_recycleq',
+ ];
+ $deleleextra = [
+ 'value' => $deletequestion->id,
+ 'alt' => get_string('deletepermanentlybutton', 'questionnaire'),
+ 'title' => get_string('deletepermanentlybutton', 'questionnaire'),
+ 'class' => 'mod_questionnaire_recycleq',
+ ];
+ $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 +442,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/responsetype/responsetype.php b/classes/responsetype/responsetype.php
index aa9c5dd25..841ea11d4 100644
--- a/classes/responsetype/responsetype.php
+++ b/classes/responsetype/responsetype.php
@@ -287,6 +287,7 @@ protected function user_fields_sql() {
$userfieldsarr = get_all_user_name_fields();
}
$userfieldsarr = array_merge($userfieldsarr, ['username', 'department', 'institution']);
+ $userfieldsarr = array_merge($userfieldsarr, ['username', 'department', 'institution', 'idnumber']);
$userfields = '';
foreach ($userfieldsarr as $field) {
$userfields .= $userfields === '' ? '' : ', ';
diff --git a/classes/search/question.php b/classes/search/question.php
index 0d5d4b60d..f38700fbc 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 000000000..d715d53b3
--- /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 e5892b26b..49f51f097 100644
--- a/db/install.xml
+++ b/db/install.xml
@@ -77,7 +77,7 @@
-
+
diff --git a/db/tasks.php b/db/tasks.php
index 40d241abc..8629011cb 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 b07cea8c2..756582807 100644
--- a/db/upgrade.php
+++ b/db/upgrade.php
@@ -1016,6 +1016,32 @@ function xmldb_questionnaire_upgrade($oldversion=0) {
upgrade_mod_savepoint(true, 2022121601.01, 'questionnaire');
}
+ if ($oldversion < 2024080100.01) {
+ $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)) {
+ // Instead of updating all 'deleted' = 'y' to a timestamp (which could be slow on large tables),
+ // simply delete those records, as they were not restorable before this upgrade.
+ $DB->delete_records('questionnaire_question', ['deleted' => 'y']);
+ // Optionally, clear 'deleted' = 'n' to null if required by new logic.
+ $DB->set_field('questionnaire_question', 'deleted', null, ['deleted' => 'n']);
+ $dbman->change_field_type($table, $field);
+ }
+ unset($field);
+ // Questionnaire savepoint reached.
+ upgrade_mod_savepoint(true, 2024080100.01, 'questionnaire');
+ }
+
return true;
}
diff --git a/lang/en/questionnaire.php b/lang/en/questionnaire.php
index e28d93520..2da21d836 100644
--- a/lang/en/questionnaire.php
+++ b/lang/en/questionnaire.php
@@ -99,6 +99,7 @@
$string['closed'] = 'Closed on {$a}.';
$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';
@@ -110,7 +111,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}"?';
@@ -142,7 +144,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
@@ -264,7 +269,8 @@
$string['gradesdeleted'] = 'Questionnaire grades deleted';
$string['headingtext'] = 'Heading text';
$string['horizontal'] = 'Horizontal';
-$string['id'] = 'ID';
+$string['id'] = 'User id';
+$string['useridnumber'] = 'User idnumber';
$string['includerankaverages'] = 'Include rank question averages';
$string['includechoicecodes'] = 'Include choice codes';
$string['includechoicetext'] = 'Include choice text';
@@ -290,6 +296,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.';
@@ -526,6 +534,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';
@@ -533,6 +542,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.
@@ -540,7 +550,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['responsesnotsubmitted'] = 'Responses not submitted';
@@ -569,6 +579,7 @@
$string['responseformat'] = 'Response format';
$string['responseoptions'] = 'Response options';
$string['responses'] = 'Responses';
+$string['responses_breakdown'] = '(Submissions: {$a->responses} | In progress: {$a->incomplete})';
$string['responseview'] = 'Students can view ALL responses';
$string['responseview_help'] = 'You can specify who can see the responses of all respondents to submitted questionnaires (general statistics tables).';
$string['responseview_link'] = 'mod/questionnaire/mod#Response_viewing';
@@ -582,6 +593,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['removeoldresponsesdefault'] = 'Never remove';
$string['removeoldresponses'] = 'Delete old responses';
@@ -666,6 +678,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 4ed962ec8..4685b6fa7 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,78 @@ 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 using Moodle API. Only 'id' is needed for update_record.
+ $question->deleted = null;
+ $question->position = $question->lastposition ?? 1;
+ $DB->update_record('questionnaire_question', $question);
+ }
+}
+
+/**
+ * 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 +448,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 +840,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 +875,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 +928,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;
@@ -951,6 +1048,28 @@ function questionnaire_get_standard_page_items($id = null, $a = null) {
}
+/**
+ * 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(int $qid, int $qtype): int {
+ global $DB;
+
+ $countresps = 0;
+ if ($qtype != QUESSECTIONTEXT) {
+ $responsetable = $DB->get_field('questionnaire_question_type', 'response_table', ['typeid' => $qtype]);
+ if (!empty($responsetable)) {
+ $countresps = $DB->count_records('questionnaire_'.$responsetable, ['question_id' => $qid]);
+ }
+ }
+
+ return $countresps;
+}
+
+
/**
* Create options for remove old responses in the questionare.
*
diff --git a/questionnaire.class.php b/questionnaire.class.php
index 5fd3bebee..b117e281f 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) {
@@ -423,6 +448,10 @@ public function view_response($rid, $referer= '', $resps = '', $compare = false,
}
$pdf = ($outputtarget == 'pdf') ? true : false;
foreach ($this->questions as $question) {
+ // Only show eligible questions in the response.
+ if (!$question->dependency_fulfilled($rid, $this->questions)) {
+ continue;
+ }
if ($question->type_id < QUESPAGEBREAK) {
$i++;
}
@@ -885,7 +914,7 @@ private function has_required($section = 0) {
return true;
}
}
- } else {
+ } else if (key_exists($section, $this->questionsbysec)) {
foreach ($this->questionsbysec[$section] as $questionid) {
if ($this->questions[$questionid]->required()) {
return true;
@@ -1326,21 +1355,23 @@ private function survey_render(&$formdata, $section = 1, $message = '') {
$this->page->add_to_page('progressbar',
$this->renderer->render_progress_bar($section, $this->questionsbysec));
}
- foreach ($this->questionsbysec[$section] as $questionid) {
- if ($this->questions[$questionid]->is_numbered()) {
- $i++;
- }
- // Need questionnaire id to get the questionnaire object in sectiontext (Label) question class.
- $formdata->questionnaire_id = $this->id;
- if (isset($formdata->rid) && !empty($formdata->rid)) {
- $this->add_response($formdata->rid);
- } else {
- $this->add_response_from_formdata($formdata);
+ if (key_exists($section, $this->questionsbysec)) {
+ foreach ($this->questionsbysec[$section] as $questionid) {
+ if ($this->questions[$questionid]->is_numbered()) {
+ $i++;
+ }
+ // Need questionnaire id to get the questionnaire object in sectiontext (Label) question class.
+ $formdata->questionnaire_id = $this->id;
+ if (isset($formdata->rid) && !empty($formdata->rid)) {
+ $this->add_response($formdata->rid);
+ } else {
+ $this->add_response_from_formdata($formdata);
+ }
+ $this->page->add_to_page('questions',
+ $this->renderer->question_output($this->questions[$questionid],
+ (isset($this->responses[$formdata->rid]) ? $this->responses[$formdata->rid] : []),
+ $i, $this->usehtmleditor, []));
}
- $this->page->add_to_page('questions',
- $this->renderer->question_output($this->questions[$questionid],
- (isset($this->responses[$formdata->rid]) ? $this->responses[$formdata->rid] : []),
- $i, $this->usehtmleditor, []));
}
$this->print_survey_end($section, $numsections);
@@ -2002,8 +2033,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;
@@ -2025,7 +2056,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) {
@@ -2520,7 +2551,7 @@ private function response_goto_thankyou() {
$this->page->add_to_page('progressbar',
$this->renderer->render_progress_bar(count($this->questionsbysec) + 1, $this->questionsbysec));
}
- $this->page->add_to_page('title', $thankhead);
+ $this->page->add_to_page('title', format_string($thankhead));
$this->page->add_to_page('addinfo',
format_text(file_rewrite_pluginfile_urls($thankbody, 'pluginfile.php',
$this->context->id, 'mod_questionnaire', 'thankbody', $this->survey->id), FORMAT_HTML, ['noclean' => true]));
@@ -2887,6 +2918,23 @@ public function survey_results($rid = '', $uid=false, $pdf = false, $currentgrou
if ($userview === 'y') {
$respondentstring = get_string('submissions', 'questionnaire');
}
+ if (!$userview) {
+ $completedcount = 0;
+ $inprogresscount = 0;
+
+ foreach ($rows as $row) {
+ if ($row->complete === 'y') {
+ $completedcount++;
+ } else if ($row->complete === 'n') {
+ $inprogresscount++;
+ }
+ }
+ $numresps .= ' ' . get_string('responses_breakdown', 'questionnaire',
+ [
+ 'responses' => $completedcount,
+ 'incomplete' => $inprogresscount,
+ ]);
+ }
$this->page->add_to_page('respondentinfo',
' ' . $respondentstring . ': ' . $numresps . '');
if (empty($rows)) {
@@ -3012,6 +3060,7 @@ protected function user_fields() {
$userfieldsarr = get_all_user_name_fields();
}
$userfieldsarr = array_merge($userfieldsarr, ['username', 'department', 'institution']);
+ $userfieldsarr = array_merge($userfieldsarr, ['username', 'department', 'institution', 'idnumber']);
return $userfieldsarr;
}
@@ -3182,6 +3231,9 @@ protected function process_csv_row(array &$row,
if (in_array('id', $options)) {
array_push($positioned, $uid);
}
+ if (in_array('useridnumber', $options)) {
+ array_push($positioned, $user->idnumber);
+ }
if (in_array('fullname', $options)) {
array_push($positioned, $fullname);
}
@@ -3241,7 +3293,7 @@ public function generate_csv($currentgroupid, $rid='', $userid='', $choicecodes=
$columns = array();
$types = array();
foreach ($options as $option) {
- if (in_array($option, array('response', 'submitted', 'id'))) {
+ if (in_array($option, array('response', 'submitted', 'id', 'useridnumber'))) {
$columns[] = get_string($option, 'questionnaire');
$types[] = 0;
} else if ($option == 'useridentityfields') {
diff --git a/questions.php b/questions.php
index 6175512c5..1bbe7d559 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 5c81c84a9..a627ec4bb 100755
--- a/report.php
+++ b/report.php
@@ -727,6 +727,13 @@
}
}
+ // Add group filter dropdown.
+ if ($groupmode > 0) {
+ $groupselect = groups_print_activity_menu($cm, $url->out(), true);
+ $questionnaire->page->add_to_page('respondentinfo', $groupselect);
+ $currentgroupid = groups_get_activity_group($cm);
+ }
+
if ($byresponse || $rid) {
// Available group modes (0 = no groups; 1 = separate groups; 2 = visible groups).
if ($groupmode > 0) {
@@ -765,12 +772,7 @@
$rid = $rids[0];
}
- if ($noresponses) {
- $questionnaire->page->add_to_page('respondentinfo',
- get_string('group') . ' ' . groups_get_group_name($currentgroupid) . ': ' .
- get_string('noresponses', 'questionnaire'));
-
- } else if ($outputtarget == 'pdf') {
+ if ($outputtarget == 'pdf') {
$pdf = questionnaire_report_start_pdf();
if ($currentgroupid > 0) {
$groupname = get_string('group') . ': ' . groups_get_group_name($currentgroupid) . '';
@@ -789,6 +791,12 @@
error_reporting($errorreporting);
} else { // Default to HTML.
+ if ($noresponses) {
+ $questionnaire->page->add_to_page('respondentinfo',
+ get_string('group') . ' ' . groups_get_group_name($currentgroupid) . ': ' .
+ get_string('noresponses', 'questionnaire'));
+ }
+
// Print the page header.
$PAGE->set_title(get_string('questionnairereport', 'questionnaire'));
$PAGE->set_heading(format_string($course->fullname));
diff --git a/settings.php b/settings.php
index 30e640574..dc94193c5 100644
--- a/settings.php
+++ b/settings.php
@@ -43,6 +43,7 @@
'course' => get_string('course'),
'group' => get_string('group'),
'id' => get_string('id', 'questionnaire'),
+ 'useridnumber' => get_string('useridnumber', 'questionnaire'),
'fullname' => get_string('fullname'),
'username' => get_string('username'),
'useridentityfields' => get_string('showuseridentity', 'admin')
@@ -54,6 +55,10 @@
$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));
+
// Delete old responses after. The default value is 24 months.
$options = [
'0' => new lang_string('disabled', 'questionnaire'),
diff --git a/styles.css b/styles.css
index 831b4bd3b..86164eb54 100644
--- a/styles.css
+++ b/styles.css
@@ -477,6 +477,28 @@ 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;
+}
+
+input[type=image].mod_questionnaire_recycleq {
+ max-height: 1em;
+}
+
.mod_questionnaire_flex-container {
display: inline-flex;
}
diff --git a/templates/question_container.mustache b/templates/question_container.mustache
index f29c93acf..97ebd1a96 100644
--- a/templates/question_container.mustache
+++ b/templates/question_container.mustache
@@ -58,7 +58,7 @@
{{{required}}}