Skip to content

Commit

Permalink
reworked and refactored view page + code improvements + fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
Glutamat42 committed May 6, 2024
1 parent 8658c7a commit 41765ff
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 14 deletions.
19 changes: 19 additions & 0 deletions classes/event/question_answered.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

// unused, just for reference. Don't commit

namespace mod_adleradaptivity\event;

use core\event\base as core_event_base;

class question_answered extends core_event_base {

protected function init() {
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}

public static function get_name() {
return get_string('event_question_answered', 'mod_adleradaptivity');
}
}
20 changes: 13 additions & 7 deletions classes/external/answer_questions.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use external_value;
use invalid_parameter_exception;
use mod_adleradaptivity\local\completion_helpers;
use mod_adleradaptivity\local\db\adleradaptivity_task_repository;
use mod_adleradaptivity\local\helpers;
use mod_adleradaptivity\moodle_core;
use moodle_database;
Expand Down Expand Up @@ -124,7 +125,10 @@ public static function execute(array $module, array $questions): array {
$params = self::validate_parameters(self::execute_parameters(), ['module' => $module, 'questions' => $questions]);
$module = external_helpers::validate_module_params_and_get_module($params['module']);
$context = context_module::instance($module->id);

// permission checks
static::validate_context($context);
require_capability('mod/adleradaptivity:create_and_edit_own_attempt', $context);

$questions = self::validate_and_enhance_questions($questions, $module->instance);

Expand Down Expand Up @@ -157,9 +161,10 @@ public static function execute(array $module, array $questions): array {
* @throws invalid_parameter_exception If a question does not exist.
*/
protected static function validate_and_enhance_questions(array $questions, string $instance_id): array {
$task_repository = new adleradaptivity_task_repository();
foreach ($questions as $key => $question) {
try {
$task = helpers::get_task_by_question_uuid($question['uuid'], $instance_id);
$task = $task_repository->get_task_by_question_uuid($question['uuid'], $instance_id);
$questions[$key]['task'] = $task;
} catch (moodle_exception $e) {
throw new invalid_parameter_exception('Question with uuid ' . $question['uuid'] . ' does not exist.');
Expand All @@ -168,6 +173,7 @@ protected static function validate_and_enhance_questions(array $questions, strin
return $questions;
}

// TODO: move somewhere else, it is now not only used by this external class, but also in a moodle view
/**
* Determines the completion status of the module.
* This is the completion state of the module, saved in the database and not with the currently submitted answers.
Expand All @@ -176,7 +182,7 @@ protected static function validate_and_enhance_questions(array $questions, strin
* @param stdClass $module Moodle module instance.
* @return string Completion status.
*/
protected static function determine_module_completion_status(completion_info $completion, stdClass $module): string {
public static function determine_module_completion_status(completion_info $completion, stdClass $module): string {
$completionState = $completion->get_data($module)->completionstate;
return ($completionState == COMPLETION_COMPLETE || $completionState == COMPLETION_COMPLETE_PASS)
? completion_helpers::STATUS_CORRECT
Expand Down Expand Up @@ -303,7 +309,7 @@ protected static function process_questions(array $questions, int $time_at_reque

// start processing the questions
foreach ($questions as $key => $question) {
self::process_individual_question($question, $time_at_request_start, $quba);
self::process_single_question($question, $time_at_request_start, $quba);
}

// save current questions usage
Expand All @@ -321,12 +327,12 @@ protected static function process_questions(array $questions, int $time_at_reque
/**
* Processes an individual question.
*
* @param array $question Question data.
* @param array $question Question data containing: uuid, answer (json encoded array of booleans)
* @param int $time_at_request_start Timestamp at the start of the request.
* @param question_usage_by_activity $quba Question usage by activity object.
* @throws invalid_parameter_exception If an unsupported question type is encountered.
*/
private static function process_individual_question(array &$question, int $time_at_request_start, question_usage_by_activity $quba) {
public static function process_single_question(array $question, int $time_at_request_start, question_usage_by_activity $quba) {
$question['question_object'] = $quba->get_question(helpers::get_slot_number_by_uuid($question['uuid'], $quba));
$question_type_class = get_class($question['question_object']->qtype);

Expand Down Expand Up @@ -357,12 +363,12 @@ private static function process_individual_question(array &$question, int $time_
* @return completion_info Completion information after update.
* @throws moodle_exception If completion is not enabled for the module.
*/
private static function update_module_completion(stdClass $module): completion_info {
public static function update_module_completion(stdClass $module, int $user_id = 0): completion_info {
$course = moodle_core::get_course($module->course);
$completion = new completion_info($course);

if ($completion->is_enabled($module)) {
$completion->update_state($module, COMPLETION_COMPLETE);
$completion->update_state($module, COMPLETION_COMPLETE, $user_id);
} else {
throw new moodle_exception('Completion is not enabled for this module.');
}
Expand Down
29 changes: 29 additions & 0 deletions classes/local/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,35 @@ public static function load_adleradaptivity_attempt_by_cmid($cmid) {
return $DB->get_records_sql($sql, [$modulecontext->id]);
}

/**
* Retrieves the course module ID (cmid) for a given question usage ID.
*
* @param int $quid The ID of the question usage.
* @return int The course module ID (cmid) associated with the question usage.
* @throws dml_exception If there's an error with the database query.
* @throws dml_missing_record_exception If the expected records are not found.
*/
public static function get_cmid_for_question_usage($quid) {
global $DB;

// First, retrieve the contextid from the question_usages table using the question usage ID
$contextid = $DB->get_field('question_usages', 'contextid', ['id' => $quid], MUST_EXIST);

if (!$contextid) {
throw new dml_missing_record_exception('context not found for the provided question usage ID');
}

// Now, use the contextid to find the corresponding cmid in the context table
// Note: CONTEXT_MODULE is a constant equal to 80, representing the context level for course modules in Moodle.
$cmid = $DB->get_field_select('context', 'instanceid', "contextlevel = ? AND id = ?", [CONTEXT_MODULE, $contextid]);

if (!$cmid) {
throw new dml_missing_record_exception('course module (cmid) not found for the provided context ID');
}

return $cmid;
}

/** Gets the attempt object (question usage aka $quba) for the given cm and given user.
* If there is no attempt object for the given cm and user, a new attempt object is created.
* If there is more than one attempt object for the given cm and user, an exception is thrown.
Expand Down
14 changes: 7 additions & 7 deletions classes/local/output/pages/view_page.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public function __construct() {
$this->check_attempt_permissions($module_context, $adleradaptivity_attempt);

// continue setting up variables
$tasks = $this->load_tasks_with_questions_sorted_by_difficulty($quba, $module_instance);
$tasks = $this->load_tasks_with_questions_sorted_by_difficulty($quba, $module_instance, $module_context);


// Trigger course_module_viewed event and completion.
Expand Down Expand Up @@ -215,12 +215,12 @@ private function define_page_meta_information(stdClass $cm, stdClass $module_ins
* @throws dml_exception
* @throws moodle_exception
*/
private function load_tasks_with_questions(question_usage_by_activity $quba, stdClass $module_instance): array {
private function load_tasks_with_questions(question_usage_by_activity $quba, stdClass $module_instance, context_module $module_context): array {
$slots = $quba->get_slots();

$tasks = []; // This will be an array of tasks, each containing its questions
foreach ($slots as $slot) {
$this->insert_question_from_slot_into_tasks_array($quba, $slot, $module_instance, $tasks);
$this->insert_question_from_slot_into_tasks_array($quba, $slot, $module_instance, $tasks, $module_context);
}
return $tasks;
}
Expand All @@ -234,8 +234,8 @@ private function load_tasks_with_questions(question_usage_by_activity $quba, std
* @throws dml_exception
* @throws moodle_exception
*/
private function load_tasks_with_questions_sorted_by_difficulty(question_usage_by_activity $quba, stdClass $module_instance): array {
$tasks = $this->load_tasks_with_questions($quba, $module_instance);
private function load_tasks_with_questions_sorted_by_difficulty(question_usage_by_activity $quba, stdClass $module_instance, context_module $module_context): array {
$tasks = $this->load_tasks_with_questions($quba, $module_instance, $module_context);
return view_page::sort_questions_in_tasks_by_difficulty($tasks);
}

Expand All @@ -247,9 +247,9 @@ private function load_tasks_with_questions_sorted_by_difficulty(question_usage_b
* @throws dml_exception
* @throws moodle_exception
*/
private function insert_question_from_slot_into_tasks_array(question_usage_by_activity $quba, int $slot, stdClass $module_instance, array &$tasks): void {
private function insert_question_from_slot_into_tasks_array(question_usage_by_activity $quba, int $slot, stdClass $module_instance, array &$tasks, context_module $module_context): void {
$question = $quba->get_question($slot);
$adaptivity_question = $this->question_repository->get_adleradaptivity_question_by_question_bank_entries_id($question->questionbankentryid);
$adaptivity_question = $this->question_repository->get_adleradaptivity_question_by_question_bank_entries_id($question->questionbankentryid, $module_context);
$task = $this->task_repository->get_task_by_question_uuid($question->idnumber, $module_instance->id);

if (!isset($tasks[$task->id])) {
Expand Down
142 changes: 142 additions & 0 deletions classes/output/view_renderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php

namespace mod_adleradaptivity\output;

defined('MOODLE_INTERNAL') || die();

use coding_exception;
use completion_info;
use html_writer;
use mod_adleradaptivity\external\answer_questions;
use mod_adleradaptivity\local\completion_helpers;
use moodle_exception;
use moodle_url;
use plugin_renderer_base;
use qbank_previewquestion\question_preview_options;
use question_usage_by_activity;
use stdClass;

class view_renderer extends plugin_renderer_base {
/**
* Renders the form with the tasks with its questions.
*
* @param array $tasks
* @param question_usage_by_activity $quba
* @return string
*/
public function render_module_view_page(array $tasks, question_usage_by_activity $quba, stdClass $cm, stdClass $course): string {
$cmid = $cm->id;

$slots = $quba->get_slots();
// Define the URL for form submission
$actionurl = new moodle_url('/mod/adleradaptivity/processattempt.php', ['id' => $cmid, 'attempt' => $quba->get_id()]);

// start generating output
$output = '';


// Start the question form with the action URL
$output .= html_writer::start_tag('form', ['method' => 'post', 'action' => $actionurl->out(false), 'enctype' => 'multipart/form-data', 'id' => 'responseform']);

// Hidden fields for sesskey and possibly other data need to pass through the form to the processattempt page
$output .= html_writer::start_tag('div');
$output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]);
$output .= html_writer::end_tag('div');

$output .= $this->render_content($tasks, $quba, $cm, $course);

$output .= html_writer::end_tag('form');


return $output;
}

/**
* Renders the tasks with its questions of the adleradaptivity module.
*
* @param array $tasks Array of task objects with associated questions.
* @return string HTML to output.
* @throws coding_exception If getting the translation string failed
* @throws moodle_exception If the question cannot be rendered.
*/
private function render_content($tasks, question_usage_by_activity $quba, stdClass $cm, stdClass $course): string {
$completion = new completion_info($course);

$data = [
'is_behat_mode' => defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING,
'module_completed' => answer_questions::determine_module_completion_status($completion, $cm) == completion_helpers::STATUS_CORRECT,
'tasks' => []
];

foreach ($tasks as $task_id => $task) {
$task_status = completion_helpers::check_task_status($quba, $task_id, $task['required_difficulty']);

$taskData = [
'title' => $task['title'],
'optional' => $task['required_difficulty'] === null,
'difficulty' => $this->get_difficulty_label($task['required_difficulty']),
'status_success' => in_array($task_status, [completion_helpers::STATUS_CORRECT, completion_helpers::STATUS_OPTIONAL_INCORRECT, completion_helpers::STATUS_OPTIONAL_NOT_ATTEMPTED]),
'status_message' => get_string($this->get_task_status_message_translation_key($task_status), 'mod_adleradaptivity'),
'status_behat_class' => $this->get_task_status_behat_class($task_status),
'questions' => []
];

foreach ($task['questions'] as $question) {
$options = new question_preview_options($question['question']);
$options->load_user_defaults();
$options->set_from_request();

$taskData['questions'][] = [
'content' => $quba->render_question($question['slot'], $options, $this->get_difficulty_label($question['difficulty'])),
'status_best_try' => completion_helpers::check_question_answered_correctly_once($quba->get_question_attempt($question['slot'])),
];
}

$data['tasks'][] = $taskData;
}

return $this->render_from_template('mod_adleradaptivity/questions', $data);
}

/**
* @param string $status One of the STATUS_* constants from completion_helpers.
* @return string The translation key for the status message.
*/
private function get_task_status_message_translation_key(string $status): string {
return match ($status) {
completion_helpers::STATUS_NOT_ATTEMPTED => 'view_task_status_not_attempted',
completion_helpers::STATUS_CORRECT => 'view_task_status_correct',
completion_helpers::STATUS_INCORRECT => 'view_task_status_incorrect',
completion_helpers::STATUS_OPTIONAL_NOT_ATTEMPTED => 'view_task_status_optional_not_attempted',
completion_helpers::STATUS_OPTIONAL_INCORRECT => 'view_task_status_optional_incorrect',
default => 'view_task_status_unknown',
};
}

private function get_task_status_behat_class(string $status): string {
return match ($status) {
completion_helpers::STATUS_NOT_ATTEMPTED => 'behat_task-not-attempted',
completion_helpers::STATUS_CORRECT => 'behat_task-correct',
completion_helpers::STATUS_INCORRECT => 'behat_task-incorrect',
completion_helpers::STATUS_OPTIONAL_NOT_ATTEMPTED => 'behat_task-optional-not-attempted',
completion_helpers::STATUS_OPTIONAL_INCORRECT => 'behat_task-optional-incorrect',
default => 'unknown',
};
}

/**
* Converts a difficulty code to a human-readable label.
*
* @param int $difficulty The difficulty code.
* @return string The difficulty label.
*/
private function get_difficulty_label($difficulty) {
$difficulties = [
0 => get_string('difficulty_0', 'mod_adleradaptivity'),
100 => get_string('difficulty_100', 'mod_adleradaptivity'),
200 => get_string('difficulty_200', 'mod_adleradaptivity')
];

return $difficulties[$difficulty] ?? 'unknown';
}
}
51 changes: 51 additions & 0 deletions templates/questions.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<div class="mod_adleradaptivity_questions">
<!-- Module completion messages -->
{{#module_completed}}
<div class="alert alert-success {{#is_behat_mode}}behat_module-success{{/is_behat_mode}}" role="alert">
{{#str}}view_module_completed_success, mod_adleradaptivity{{/str}}
</div>
{{/module_completed}}
{{^module_completed}}
<div class="alert alert-warning {{#is_behat_mode}}behat_module-failure{{/is_behat_mode}}" role="alert">
{{#str}}view_module_completed_no, mod_adleradaptivity{{/str}}
</div>
{{/module_completed}}

<!-- Tasks -->
{{#tasks}}
<div class="task">
<hr />
<h3>{{#str}}view_task_title, mod_adleradaptivity{{/str}}: {{title}}</h3>
{{#optional}}
<p><em>{{#str}}view_task_optional, mod_adleradaptivity{{/str}}</em></p>
{{/optional}}
{{^optional}}
<p>{{#str}}view_task_required_difficulty, mod_adleradaptivity{{/str}}: <strong>{{difficulty}}</strong></p>
{{/optional}}

<!-- Task Status Handling -->
{{#status_success}}
<p class="{{#is_behat_mode}}{{{status_behat_class}}}{{/is_behat_mode}}">✅ {{{status_message}}}</p>
{{/status_success}}
{{^status_success}}
<div class="alert alert-warning {{#is_behat_mode}}{{{status_behat_class}}}{{/is_behat_mode}}" role="alert">
{{{status_message}}}
</div>
{{/status_success}}

<!-- Questions -->
<div class="questions">
{{#questions}}
<div class="question">
<hr />
<!-- TODO: class always, not behat exclusive-->
{{#status_best_try}}
<p class="{{#is_behat_mode}}behat_question-status-success{{/is_behat_mode}}">✅ {{#str}}view_question_success, mod_adleradaptivity{{/str}}</p>
{{/status_best_try}}
<div class="question-content">{{{content}}}</div>
</div>
{{/questions}}
</div>
</div>
{{/tasks}}
</div>

0 comments on commit 41765ff

Please sign in to comment.