Skip to content

Commit

Permalink
Include attempt attachments (e.g., essay file submissions) in quiz ar…
Browse files Browse the repository at this point in the history
…chive (default on)
  • Loading branch information
ngandrass committed Nov 29, 2023
1 parent 85f034f commit fdb64e1
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 11 deletions.
79 changes: 77 additions & 2 deletions classes/Report.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class Report {
"general_feedback",
"rightanswer",
"history",
"attachments",
];

/** @var array Dependencies of report sections */
Expand All @@ -66,6 +67,7 @@ class Report {
"general_feedback" => ["question"],
"rightanswer" => ["question"],
"history" => ["question"],
"attachments" => ["question"],
];

/** @var string[] Available paper formats for attempt PDFs */
Expand Down Expand Up @@ -271,6 +273,77 @@ public static function build_report_sections_from_formdata(object $archive_quiz_
return $report_sections;
}

/**
* Returns a list of all files that were attached to questions inside the
* given attempt
*
* @param int $attemptid ID of the attempt to get the files from
* @return array containing all files that are attached to the questions
* inside the given attempt.
*
* @throws \dml_exception
* @throws \moodle_exception
*/
public function get_attempt_attachments(int $attemptid): array {
// Prepare
$files = [];
$ctx = \context_module::instance($this->cm->id);
$attemptobj = quiz_create_attempt_handling_errors($attemptid, $this->cm->id);

// Get all files from all questions inside this attempt
foreach ($attemptobj->get_slots() as $slot) {
$qa = $attemptobj->get_question_attempt($slot);
$qa_files = $qa->get_last_qt_files('attachments', $ctx->id);

foreach ($qa_files as $qa_file) {
$files[] = [
'usageid' => $qa->get_usage_id(),
'slot' => $slot,
'file' => $qa_file,
];
}
}

return $files;
}

/**
* Returns a list of metadata for all files that were attached to questions
* inside the given attempt to be used within the webservice API
*
* @param int $attemptid ID of the attempt to get the files from
* @return array containing the metadata of all files that are attached to
* the questions inside the given attempt.
*
* @throws \dml_exception
* @throws \moodle_exception
*/
public function get_attempt_attechments_metadata(int $attemptid): array {
$res = [];

foreach ($this->get_attempt_attachments($attemptid) as $attachment) {
$downloadurl = strval(\moodle_url::make_webservice_pluginfile_url(
$attachment['file']->get_contextid(),
$attachment['file']->get_component(),
$attachment['file']->get_filearea(),
"{$attachment['usageid']}/{$attachment['slot']}/{$attachment['file']->get_itemid()}", # YES, this is the abomination of a non-numeric itemid that question_attempt::get_response_file_url() creates and while eating innocent programmers for breakfast ...
$attachment['file']->get_filepath(),
$attachment['file']->get_filename()
));

$res[] = (object) [
'slot' => $attachment['slot'],
'filename' => $attachment['file']->get_filename(),
'filesize' => $attachment['file']->get_filesize(),
'mimetype' => $attachment['file']->get_mimetype(),
'contenthash' => $attachment['file']->get_contenthash(),
'downloadurl' => $downloadurl,
];
}

return $res;
}

/**
* Generates a HTML representation of the quiz attempt
*
Expand All @@ -285,6 +358,7 @@ public static function build_report_sections_from_formdata(object $archive_quiz_
*/
public function generate(int $attemptid, array $sections): string {
global $DB, $PAGE;
$ctx = \context_module::instance($this->cm->id);
$renderer = $PAGE->get_renderer('mod_quiz');
$html = '';

Expand All @@ -293,7 +367,7 @@ public function generate(int $attemptid, array $sections): string {
$attempt = $attemptobj->get_attempt();
$quiz = $attemptobj->get_quiz();
$options = \mod_quiz\question\display_options::make_from_quiz($this->quiz, quiz_attempt_state($quiz, $attempt));
$options->flags = quiz_get_flag_option($attempt, \context_module::instance($this->cm->id));
$options->flags = quiz_get_flag_option($attempt, $ctx);
$overtime = 0;

if ($attempt->state == quiz_attempt::FINISHED) {
Expand Down Expand Up @@ -433,6 +507,7 @@ public function generate(int $attemptid, array $sections): string {
if ($sections['question']) {
$slots = $attemptobj->get_slots();
foreach ($slots as $slot) {
// Define display options for this question
$originalslot = $attemptobj->get_original_slot($slot);
$number = $attemptobj->get_question_number($originalslot);
$displayoptions = $attemptobj->get_display_options_with_edit_link(true, $slot, "");
Expand All @@ -448,13 +523,13 @@ public function generate(int $attemptid, array $sections): string {
$displayoptions->flags = 1;
$displayoptions->manualcommentlink = 0;

// Render question as HTML
if ($slot != $originalslot) {
$attemptobj->get_question_attempt($slot)->set_max_mark(
$attemptobj->get_question_attempt($originalslot)->get_max_mark());
}
$quba = \question_engine::load_questions_usage_by_activity($attemptobj->get_uniqueid());
$html .= $quba->render_question($slot, $displayoptions, $number);

}
}

Expand Down
54 changes: 45 additions & 9 deletions classes/external/generate_attempt_report.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use quiz_archiver\Report;
Expand Down Expand Up @@ -50,6 +51,7 @@ public static function execute_parameters(): external_function_parameters {
'Sections to include in the report',
VALUE_REQUIRED
),
'attachments' => new external_value(PARAM_BOOL, 'Whether to check for attempts and include metadata if present', VALUE_OPTIONAL)
]);
}

Expand All @@ -64,6 +66,18 @@ public static function execute_returns(): external_single_structure {
'quizid' => new external_value(PARAM_INT, 'ID of the quiz', VALUE_OPTIONAL),
'attemptid' => new external_value(PARAM_INT, 'ID of the quiz attempt', VALUE_OPTIONAL),
'report' => new external_value(PARAM_RAW, 'HTML DOM of the generated quiz attempt report', VALUE_OPTIONAL),
'attachments' => new external_multiple_structure(
new external_single_structure([
'slot' => new external_value(PARAM_INT, 'Number of the quiz slot this file is attached to', VALUE_REQUIRED),
'filename' => new external_value(PARAM_TEXT, 'Filename of the attachment', VALUE_REQUIRED),
'filesize' => new external_value(PARAM_INT, 'Filesize of the attachment', VALUE_REQUIRED),
'mimetype' => new external_value(PARAM_TEXT, 'Mimetype of the attachment', VALUE_REQUIRED),
'contenthash' => new external_value(PARAM_TEXT, 'Contenthash (SHA-1) of the attachment', VALUE_REQUIRED),
'downloadurl' => new external_value(PARAM_TEXT, 'URL to download the attachment', VALUE_REQUIRED),
]),
'Files attached to the quiz attempt',
VALUE_OPTIONAL
),
'status' => new external_value(PARAM_TEXT, 'Status of the executed wsfunction', VALUE_REQUIRED),
]);
}
Expand All @@ -76,14 +90,22 @@ public static function execute_returns(): external_single_structure {
* @param int $quizid_raw ID of the quiz
* @param int $attemptid_raw ID of the quiz attempt
* @param array $sections_raw Sections to include in the report
* @param bool $attachments_raw Whether to check for attempts and include metadata if present
*
* @return array According to execute_returns()
*
* @throws \dml_exception
* @throws \dml_transaction_exception
* @throws \moodle_exception
*/
public static function execute(int $courseid_raw, int $cmid_raw, int $quizid_raw, int $attemptid_raw, $sections_raw): array {
public static function execute(
int $courseid_raw,
int $cmid_raw,
int $quizid_raw,
int $attemptid_raw,
array $sections_raw,
bool $attachments_raw
): array {
global $DB;

// Validate request
Expand All @@ -93,6 +115,7 @@ public static function execute(int $courseid_raw, int $cmid_raw, int $quizid_raw
'quizid' => $quizid_raw,
'attemptid' => $attemptid_raw,
'sections' => $sections_raw,
'attachments' => $attachments_raw,
]);

// Check capabilities
Expand All @@ -110,6 +133,14 @@ public static function execute(int $courseid_raw, int $cmid_raw, int $quizid_raw
throw new \invalid_parameter_exception("No quiz with given quizid found");
}

// Prepare response
$res = [
'courseid' => $params['courseid'],
'cmid' => $params['cmid'],
'quizid' => $params['quizid'],
'attemptid' => $params['attemptid'],
];

// Generate report
$report = new Report($course, $cm, $quiz);
if (!$report->has_access(optional_param('wstoken', null, PARAM_TEXT))) {
Expand All @@ -121,14 +152,19 @@ public static function execute(int $courseid_raw, int $cmid_raw, int $quizid_raw
throw new \invalid_parameter_exception("No attempt with given attemptid found");
}

return [
'courseid' => $params['courseid'],
'cmid' => $params['cmid'],
'quizid' => $params['quizid'],
'attemptid' => $params['attemptid'],
'report' => $report->generate_full_page($params['attemptid'], $params['sections']),
'status' => 'OK',
];
$res['report'] = $report->generate_full_page($params['attemptid'], $params['sections']);

// Check for attachments
if ($params['attachments']) {
$res['attachments'] = $report->get_attempt_attechments_metadata($params['attemptid']);
} else {
$res['attachments'] = [];
}

// Return response
$res['status'] = 'OK';

return $res;
}

}
2 changes: 2 additions & 0 deletions lang/de/quiz_archiver.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
$string['export_report_section_general_feedback_help'] = 'Allgemeines Fragenfeedback im Bericht einschließen';
$string['export_report_section_history'] = 'Bearbeitungsverlauf einschließen';
$string['export_report_section_history_help'] = 'Bearbeitungsverlauf der Testfragen im Bericht einschließen';
$string['export_report_section_attachments'] = 'Dateiabgaben einschließen';
$string['export_report_section_attachments_help'] = 'Alle Dateiabgaben (z.B. von Aufsätzen/Essay Aufgaben) im Archiv einschließen. Warnung: Dies kann die Archivgröße erheblich erhöhen.';
$string['job_overview'] = 'Testarchive';
$string['num_attempts'] = 'Anzahl Testversuche';

Expand Down
2 changes: 2 additions & 0 deletions lang/en/quiz_archiver.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
$string['export_report_section_general_feedback_help'] = 'Display the general feedback for each question inside the report.';
$string['export_report_section_history'] = 'Include answer history';
$string['export_report_section_history_help'] = 'Display the answer history for each question inside the report.';
$string['export_report_section_attachments'] = 'Include file attachments';
$string['export_report_section_attachments_help'] = 'Include all file attachments (e.g., essay file submissions) inside the archive. Warning: This can significantly increase the archive size.';
$string['job_overview'] = 'Archives';
$string['num_attempts'] = 'Number of attempts';

Expand Down
2 changes: 2 additions & 0 deletions res/moodle_role_quiz_archiver.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
<allowswitch/>
<allowview/>
<permissions>
<allow>mod/quiz:reviewmyattempts</allow>
<allow>mod/quiz:view</allow>
<allow>mod/quiz:viewreports</allow>
<allow>mod/quiz_archiver:use_webservice</allow>
<allow>moodle/backup:anonymise</allow>
<allow>moodle/backup:backupactivity</allow>
Expand Down

0 comments on commit fdb64e1

Please sign in to comment.