Commit 7365f869 authored by Abbassy's avatar Abbassy
Browse files

Merge branch 'feature/v2-classes-structure' into 'master'

Complete DTA plugin improvements

See merge request !3
parents df3f0323 24842271
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Upgrade code for install
*
* @package assignsubmission_dta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @copyright Gero Lueckemeyer and student project teams
*/
/**
* Stub for upgrade code
* @param int $oldversion
* @return bool
*/
function xmldb_assignsubmission_dta_upgrade($oldversion) {
global $CFG;
// Currently no adjustments necessary for Moodle upgrades. Works without changes since the first MoJEC version.
return true;
}
<?php
// This file is part of Moodle - http://moodle.org/.
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../../../../config.php');
require_once($CFG->dirroot . '/mod/assign/locallib.php');
require_once($CFG->libdir . '/externallib.php');
/**
* External API for assignsubmission_dta helper endpoints.
*
* This provides a minimal wrapper used by automation to move a draft file
* into the DTA submission filearea and trigger the backend processing.
*/
class assignsubmission_dta_external extends external_api {
public static function save_submission_parameters(): external_function_parameters {
return new external_function_parameters([
'assignmentid' => new external_value(PARAM_INT, 'Assignment id'),
'userid' => new external_value(PARAM_INT, 'User id of the submitter'),
'itemid' => new external_value(PARAM_INT, 'Draft itemid uploaded via webservice/upload.php')
]);
}
public static function save_submission(int $assignmentid, int $userid, int $itemid): array {
global $DB;
$params = self::validate_parameters(self::save_submission_parameters(), [
'assignmentid' => $assignmentid,
'userid' => $userid,
'itemid' => $itemid,
]);
$assignmentid = $params['assignmentid'];
$userid = $params['userid'];
$itemid = $params['itemid'];
// Locate course module and setup context.
$cm = get_coursemodule_from_instance('assign', $assignmentid, 0, false, MUST_EXIST);
// Use system context for validation so the service can operate without
// prior course enrolment (the token access is already restricted).
// This avoids require_login_exception for not-enrolled accounts during
// automated testing.
$context = context_system::instance();
self::validate_context($context);
// Impersonate the target user for file saving semantics.
$user = $DB->get_record('user', ['id' => $userid, 'deleted' => 0], '*', MUST_EXIST);
\core\session\manager::set_user($user);
// Build assign API instance.
$course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST);
$modcontext = context_module::instance($cm->id);
$assign = new assign($modcontext, $cm, $course);
// Ensure there is a submission object for the given user.
$submission = $assign->get_user_submission($userid, true);
// If the submission is new (no ID), save it first
if (empty($submission->id)) {
$submission->assignment = $assignmentid;
$submission->userid = $userid;
$submission->timecreated = time();
$submission->timemodified = time();
$submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
// Remove id property if it exists but is null/0, then insert
unset($submission->id);
$submission->id = $DB->insert_record('assign_submission', $submission);
}
// Mark as submitted and update
$submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
$submission->timemodified = time();
$DB->update_record('assign_submission', $submission);
// Move draft file(s) into the DTA submission area.
$data = new stdClass();
file_save_draft_area_files(
$itemid,
$modcontext->id,
'assignsubmission_dta',
'submissions_dta',
$submission->id,
['subdirs' => 0]
);
// After files are in place, trigger the plugin logic similar to save().
// We reuse the logic from the plugin by mimicking a form save.
$fs = get_file_storage();
$files = $fs->get_area_files($modcontext->id, 'assignsubmission_dta', 'submissions_dta', $submission->id, 'id', false);
if (empty($files)) {
return [
'status' => false,
'message' => 'No files found in submission area'
];
}
$file = reset($files);
// Send to backend and persist results using the plugin utils.
require_once(__DIR__ . '/classes/dta_backend_utils.php');
require_once(__DIR__ . '/classes/dta_db_utils.php');
require_once(__DIR__ . '/classes/models/dta_result_summary.php');
require_once(__DIR__ . '/classes/models/dta_recommendation.php');
$response = \assignsubmission_dta\dta_backend_utils::assignsubmission_dta_send_submission_to_backend(
$assign,
$submission->id,
$file
);
if (is_null($response)) {
return [
'status' => false,
'message' => 'Backend did not respond'
];
}
$summary = \assignsubmission_dta\models\dta_result_summary::assignsubmission_dta_decode_json($response);
$recs = \assignsubmission_dta\models\dta_recommendation::assignsubmission_dta_decode_json_recommendations($response);
\assignsubmission_dta\dta_db_utils::assignsubmission_dta_store_result_summary_to_database(
$assign->get_instance()->id,
$submission->id,
$summary
);
\assignsubmission_dta\dta_db_utils::assignsubmission_dta_store_recommendations_to_database(
$assign->get_instance()->id,
$submission->id,
$recs
);
return [
'status' => true,
'message' => 'Submission stored and backend invoked',
'submissionid' => $submission->id
];
}
public static function save_submission_returns(): external_single_structure {
return new external_single_structure([
'status' => new external_value(PARAM_BOOL, 'Operation status'),
'message' => new external_value(PARAM_TEXT, 'Status message'),
'submissionid' => new external_value(PARAM_INT, 'Submission ID')
]);
}
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for component "assignsubmission_dta", language "en"
*
* @package assignsubmission_dta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @copyright Gero Lueckemeyer and student project teams
*/
// General.
$string["pluginname"] = "Dockerized Testing Agent (DTA)";
$string["enabled"] = $string["pluginname"];
// Dialog field labels.
$string["submission_label"] = "DTA submission configuration or zip-packed project";
$string["submission_label_help"] = "Either upload a single textfile containing one DTA URI pointing to the repository with your submission or pack your project as zip and upload it directly. Using the textfile you can additionally add as many unified-ticketing URI (one per line) as you wish for feedback into one or more ticketsystems.";
$string["submission_settings_label"] = "DTA test configuration";
$string["submission_settings_label_help"] = "single text file with DTA test URI";
$string["enabled_help"] = "If enabled, you will have to upload a textfile containing a valid DTA URI pointing to the repository with your test logic and defining a docker image on dockerhub used as testrunner. Your students will have to either upload their code in a zip archive resembling the expected repository structure or as well by providing a textifle with a valid DTA URI pointing to the repository with their submission logic";
// Content.
$string["tests_successful"] = " successful";
$string["failures"] = " failure";
$string["compilation_errors"] = " compilation error";
$string["unknown_state"] = " with unknown state";
$string["success_competencies"] = "Successfully tested competency profile ";
$string["summary"] = "Summary";
$string["total_items"] = "Total items";
$string["success_rate"] = "Success rate";
$string["details"] = "Details";
$string["competencies"] = "Competencies";
$string["package_name"] = "Package Name";
$string["unit_name"] = "Unit Name";
$string["test_name"] = "Test Name";
$string["status"] = "Status";
$string["failure_type"] = "Failure Type";
$string["failure_reason"] = "Failure Reason";
$string["line_no"] = "Line Number";
$string["col_no"] = "Column Number";
$string["pos"] = "Position";
$string["stacktrace"] = "Stack Trace";
// Comeptencies.
$string["comp_statement"] = "Statement";
$string["comp_block"] = "Block";
$string["comp_flow"] = "Flow of Control";
$string["comp_loop"] = "Repetition";
$string["comp_const"] = "Constant";
$string["comp_var"] = "Variable";
$string["comp_type"] = "Type";
$string["comp_datastructure"] = "Data Structure";
$string["comp_interface"] = "Interface";
$string["comp_unit"] = "Unit of Code";
$string["comp_proc_usage"] = "Procedure Usage";
$string["comp_proc_sign"] = "Procedure Signature";
$string["comp_library"] = "Library Usage";
$string["comp_ext_api"] = "External API Usage";
$string["comp_simple"] = "Simplicity";
$string["comp_abstraction"] = "Abstraction";
// Competencies for index calculation.
$string["comp0"] = $string["comp_statement"];
$string["comp1"] = $string["comp_block"];
$string["comp2"] = $string["comp_flow"];
$string["comp3"] = $string["comp_loop"];
$string["comp4"] = $string["comp_const"];
$string["comp5"] = $string["comp_var"];
$string["comp6"] = $string["comp_type"];
$string["comp7"] = $string["comp_datastructure"];
$string["comp8"] = $string["comp_interface"];
$string["comp9"] = $string["comp_unit"];
$string["comp10"] = $string["comp_proc_usage"];
$string["comp11"] = $string["comp_proc_sign"];
$string["comp12"] = $string["comp_library"];
$string["comp13"] = $string["comp_ext_api"];
$string["comp14"] = $string["comp_simple"];
$string["comp15"] = $string["comp_abstraction"];
// Competency explanations.
$string["comp_statement_expl"] = "Formulate a syntactically correct statement that contributes to the solution of the given problem.";
$string["comp_block_expl"] = "Structure code into syntactically correct small unnamed units that contribute to the solution of the given problem.";
$string["comp_flow_expl"] = "Formulate syntax elements guiding the control flow such that it contributes to the solution of the given problem.";
$string["comp_loop_expl"] = "Use syntax elements repeating statements such that it contributes to the solution of the given problem.";
$string["comp_const_expl"] = "Identify and syntactically correctly define constants that contribute to the understanding and solution of the given problem.";
$string["comp_var_expl"] = "Identify and syntactically correctly define variables that contribute to the solution of the given problem.";
$string["comp_type_expl"] = "Define and/or choose appropriate data types for data elements such that they contribute to the solution of the given problem.";
$string["comp_datastructure_expl"] = "Define and/or choose appropriate data structures for data elements such that they contribute to the solution of the given problem.";
$string["comp_interface_expl"] = "Define and use interfaces for larger units of code such that it contributes to the solution of the given problem.";
$string["comp_unit_expl"] = "Define and larger units of code such that it contributes to the solution of the given problem.";
$string["comp_proc_usage_expl"] = "Use existing named structure blocks with a pre-defined behavior and signature such that it contributes to the solution of the given problem.";
$string["comp_proc_sign_expl"] = "Define named structure blocks with a pre-defined behavior and signature such that it contributes to the solution of the given problem.";
$string["comp_library_expl"] = "Use existing larger collections of named structure blocks with a pre-defined behavior and signature such that it contributes to the solution of the given problem.";
$string["comp_ext_api_expl"] = "Use standardized existing external collections of named structure blocks with a pre-defined behavior and signature such that it contributes to the solution of the given problem.";
$string["comp_simple_expl"] = "Create a simple solution of the given problem.";
$string["comp_abstraction_expl"] = "Create a sufficiently abstract solution for the given problem.";
// Competency explanations for index calculations.
$string["comp_expl0"] = $string["comp_statement_expl"];
$string["comp_expl1"] = $string["comp_block_expl"];
$string["comp_expl2"] = $string["comp_flow_expl"];
$string["comp_expl3"] = $string["comp_loop_expl"];
$string["comp_expl4"] = $string["comp_const_expl"];
$string["comp_expl5"] = $string["comp_var_expl"];
$string["comp_expl6"] = $string["comp_type_expl"];
$string["comp_expl7"] = $string["comp_datastructure_expl"];
$string["comp_expl8"] = $string["comp_interface_expl"];
$string["comp_expl9"] = $string["comp_unit_expl"];
$string["comp_expl10"] = $string["comp_proc_usage_expl"];
$string["comp_expl11"] = $string["comp_proc_sign_expl"];
$string["comp_expl12"] = $string["comp_library_expl"];
$string["comp_expl13"] = $string["comp_ext_api_expl"];
$string["comp_expl14"] = $string["comp_simple_expl"];
$string["comp_expl15"] = $string["comp_abstraction_expl"];
// Warnings.
$string["no_testfile_warning"] = "Submission type is \"Dockerized Testing Agent\" but no configuration file uploaded";
// Error messages.
$string["backendHost_not_set"] = "The Dockerized Testing Agent backend URL is not configured";
$string["no_submissionfile_warning"] = "Submission type is \"Dockerized Testing Agent\" but no configuration file or submission archive uploaded";
$string["http_client_error_msg"] = "A client error occured (HTTP 4xx)";
$string["http_server_error_msg"] = "A server error occured (HTTP 5xx)";
$string["http_unknown_error_msg"] = "An unknown HTTP error occured on backend transfer";
// Admin settings.
$string["default"] = "Enabled by default";
$string["default_help"] = "If set, this submission method will be enabled by default for all new assignments.";
$string["backendHost"] = "Backend Server Address";
$string["backendHost_help"] = "Address/Name and Port of backend server";
// Database field descriptions for privacy API.
$string["privacy:metadata:core_files"] = "Moodle core files.";
$string["privacy:metadata:assignsubmission_dta_summary:assignmentid"] = "ID of the assignment";
$string["privacy:metadata:assignsubmission_dta_summary:submissionid"] = "ID of the submission";
$string["privacy:metadata:assignsubmission_dta_summary:timestamp"] = "Date and time of the submission";
$string["privacy:metadata:assignsubmission_dta_summary"] = "Stack trace of the compilation and test if major problems occur";
$string["privacy:metadata:assignsubmission_dta_summary:successful_competencies"] = "List of the successfully tested competencies";
$string["privacy:metadata:assignsubmission_dta_summary:tested_competencies"] = "List of the tested competencies";
$string["privacy:metadata:assignsubmission_dta_summary"] = "Summary of Dockerized Test Agent (DTA) results";
$string["privacy:metadata:assignsubmission_dta_result:package_name"] = "Package name of individual test";
$string["privacy:metadata:assignsubmission_dta_result:class_name"] = "Class name of individual test";
$string["privacy:metadata:assignsubmission_dta_result:name"] = "Name of individual test";
$string["privacy:metadata:assignsubmission_dta_result:state"] = "State of individual test";
$string["privacy:metadata:assignsubmission_dta_result:failure_type"] = "Failure type of individual test";
$string["privacy:metadata:assignsubmission_dta_result:failure_reason"] = "Failure reason of individual test";
$string["privacy:metadata:assignsubmission_dta_result:stacktrace"] = "Stack trace of failed compilation or individual test";
$string["privacy:metadata:assignsubmission_dta_result:column_number"] = "Column number of failed individual compilation or test";
$string["privacy:metadata:assignsubmission_dta_result:line_number"] = "Line number of failed individual compilation or test";
$string["privacy:metadata:assignsubmission_dta_result:position"] = "Position of failed individual compilation or test";
$string["privacy:metadata:assignsubmission_dta_result"] = "Individual Dockerized Test Agent (DTA) results";
$string["privacy:metadata:assignsubmission_dta_recommendations:topic"] = "Topic of recommendation";
$string["privacy:metadata:assignsubmission_dta_recommendations:exercise_name"] = "Name of exercise";
$string["privacy:metadata:assignsubmission_dta_recommendations:url"] = "Url of recommended exercise";
$string["privacy:metadata:assignsubmission_dta_recommendations:difficulty"] = "Difficulty level of exercise";
$string["privacy:metadata:assignsubmission_dta_recommendations:score"] = "Score achieved in exercise";
$string["privacy:metadata:assignsubmission_dta_recommendations"] = "Individual Dockerized Test Agent (DTA) recommendations";
$string["privacy:metadata:dta_backend"] = "Dockerized Test Agent (DTA) backend ReST web service";
//PLUGIN
$string['recommendations'] = 'Recommendations';
$string['topic'] = 'Topic';
$string['exercise_name'] = 'Exercise Name';
$string['url'] = 'URL';
$string['difficulty'] = 'Difficulty';
$string['score'] = 'Score';
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file contains the moodle hooks for the submission DTA plugin
*
* @package assignsubmission_dta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @copyright Gero Lueckemeyer and student project teams
*/
/**
* Serves assignment submissions and other files.
*
* @param mixed $course course or id of the course
* @param mixed $cm course module or id of the course module
* @param context $context
* @param string $filearea
* @param array $args
* @param bool $forcedownload
* @return bool false if file not found, does not return if found - just send the file
*/
function assignsubmission_dta_pluginfile(
$course,
$cm,
context $context,
$filearea,
$args,
$forcedownload
) {
global $DB, $CFG;
if ($context->contextlevel != CONTEXT_MODULE) {
return false;
}
require_login($course, false, $cm);
$itemid = (int)array_shift($args);
$record = $DB->get_record('assign_submission',
['id' => $itemid],
'userid, assignment, groupid',
MUST_EXIST);
$userid = $record->userid;
$groupid = $record->groupid;
require_once($CFG->dirroot . '/mod/assign/locallib.php');
$assign = new assign($context, $cm, $course);
if ($assign->get_instance()->id != $record->assignment) {
return false;
}
if ($assign->get_instance()->teamsubmission &&
!$assign->can_view_group_submission($groupid)) {
return false;
}
if (!$assign->get_instance()->teamsubmission &&
!$assign->can_view_submission($userid)) {
return false;
}
$relativepath = implode('/', $args);
$fullpath = "/{$context->id}/assignsubmission_dta/$filearea/$itemid/$relativepath";
$fs = get_file_storage();
if (!($file = $fs->get_file_by_hash(sha1($fullpath))) || $file->is_directory()) {
return false;
}
// Download MUST be forced - security!
send_stored_file($file, 0, 0, true);
}
<?php
// This file is part of Moodle - http://moodle.org/.
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
use assignsubmission_dta\dta_db_utils;
use assignsubmission_dta\dta_backend_utils;
use assignsubmission_dta\dta_view_submission_utils;
use assignsubmission_dta\models\dta_result;
use assignsubmission_dta\models\dta_result_summary;
use assignsubmission_dta\models\dta_recommendation;
/**
* Library class for DTA submission plugin extending assign submission plugin base class.
*
* @package assignsubmission_dta
* @copyright 2023 Your Name or Organization
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class assign_submission_dta extends assign_submission_plugin {
/**
* Broadly used in logic, parametrized for easier change.
*/
public const ASSIGNSUBMISSION_DTA_COMPONENT_NAME = 'assignsubmission_dta';
/**
* Draft file area for DTA tests to be uploaded by the teacher.
*/
public const ASSIGNSUBMISSION_DTA_DRAFT_FILEAREA_TEST = 'tests_draft_dta';
/**
* File area for DTA tests to be uploaded by the teacher.
*/
public const ASSIGNSUBMISSION_DTA_FILEAREA_TEST = 'tests_dta';
/**
* File area for DTA submission assignment.
*/
public const ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION = 'submissions_dta';
/**
* Get plugin name.
*
* @return string
*/
public function get_name(): string {
return get_string('pluginname', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME);
}
/**
* Get default settings for assignment submission settings.
*
* @param MoodleQuickForm $mform Form to add elements to.
* @return void
*/
public function get_settings(MoodleQuickForm $mform): void {
// Add draft filemanager to form.
$mform->addElement(
'filemanager',
self::ASSIGNSUBMISSION_DTA_DRAFT_FILEAREA_TEST,
get_string(
'submission_settings_label',
self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME
),
null,
$this->get_file_options(true)
);
// Add help button to added filemanager.
$mform->addHelpButton(
// Form-unique element id to which to add button.
self::ASSIGNSUBMISSION_DTA_DRAFT_FILEAREA_TEST,
// Key.
'submission_settings_label',
// Language file to use.
self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME
);
// Only show filemanager if plugin is enabled.
$mform->hideIf(
// Form-unique element id to hide.
self::ASSIGNSUBMISSION_DTA_DRAFT_FILEAREA_TEST,
// Condition to check.
self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME . '_enabled',
// State to match for hiding.
'notchecked'
);
}
/**
* Allows the plugin to update the default values passed into
* the settings form (needed to set up draft areas for editor
* and filemanager elements).
*
* @param array $defaultvalues Default values to update.
*/
public function data_preprocessing(&$defaultvalues): void {
// Get id of draft area for file manager creation.
$draftitemid = file_get_submitted_draft_itemid(
self::ASSIGNSUBMISSION_DTA_DRAFT_FILEAREA_TEST
);
// Prepare draft area with created draft filearea.
file_prepare_draft_area(
$draftitemid,
$this->assignment->get_context()->id,
self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME,
self::ASSIGNSUBMISSION_DTA_FILEAREA_TEST,
0,
['subdirs' => 0]
);
$defaultvalues[self::ASSIGNSUBMISSION_DTA_DRAFT_FILEAREA_TEST] = $draftitemid;
}
/**
* Save settings of assignment submission settings.
*
* @param stdClass $data Form data.
* @return bool
*/
public function save_settings(stdClass $data): bool {
// If the assignment has no filemanager for our plugin, just leave.
$draftfilemanagerid = self::ASSIGNSUBMISSION_DTA_DRAFT_FILEAREA_TEST;
if (!isset($data->$draftfilemanagerid)) {
return true;
}
// Store files from draft filearea to final one.
file_save_draft_area_files(
// Form-unique element id of draft filemanager from the edit.
$data->$draftfilemanagerid,
// Id of the assignment in edit.
$this->assignment->get_context()->id,
self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME,
self::ASSIGNSUBMISSION_DTA_FILEAREA_TEST,
0
);
// Get files from proper filearea.
$fs = get_file_storage();
$files = $fs->get_area_files(
// Id of the current assignment.
$this->assignment->get_context()->id,
self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME,
self::ASSIGNSUBMISSION_DTA_FILEAREA_TEST,
0,
'id',
false
);
// Check if a file was uploaded.
if (empty($files)) {
\core\notification::error(
get_string('no_testfile_warning', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME)
);
return true;
}
// Get the file.
$file = reset($files);
// Send file to backend.
return dta_backend_utils::assignsubmission_dta_send_testconfig_to_backend(
$this->assignment,
$file
);
}
/**
* Add elements to submission form.
*
* @param stdClass|null $submissionorgrade Submission or grade to show in the form.
* @param MoodleQuickForm $mform Form for adding elements.
* @param stdClass $data Data for filling the elements.
* @param int $userid Current user.
* @return bool True if form elements added.
*/
public function get_form_elements_for_user(
$submissionorgrade,
MoodleQuickForm $mform,
stdClass $data,
$userid
): bool {
// Prepare submission filearea.
$data = file_prepare_standard_filemanager(
$data,
'tasks',
$this->get_file_options(false),
$this->assignment->get_context(),
self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME,
self::ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION,
$submissionorgrade ? $submissionorgrade->id : 0
);
// Add filemanager to form.
$mform->addElement(
'filemanager',
// Form-unique identifier.
'tasks_filemanager',
// Label to show next to the filemanager.
get_string('submission_label', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME),
null,
$this->get_file_options(false)
);
// Add help button.
$mform->addHelpButton(
'tasks_filemanager',
'submission_label',
self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME
);
return true;
}
/**
* Determines if a submission file area contains any files.
*
* @param stdClass $submission Submission to check.
* @return bool True if file count is zero.
*/
public function is_empty(stdClass $submission): bool {
return ($this->count_files(
$submission->id,
self::ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION
) === 0);
}
/**
* Counts the number of files in a filearea.
*
* @param int $submissionid Submission id to check.
* @param string $areaid Filearea id to count.
* @return int Number of files submitted in the filearea.
*/
private function count_files(int $submissionid, $areaid): int {
$fs = get_file_storage();
$files = $fs->get_area_files(
$this->assignment->get_context()->id,
self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME,
$areaid,
$submissionid,
'id',
false
);
return count($files);
}
/**
* Save data to the database.
*
* @param stdClass $submission Submission object.
* @param stdClass $data Data from the form.
* @return bool True if saved successfully.
*/
public function save(stdClass $submission, stdClass $data): bool {
$data = file_postupdate_standard_filemanager(
$data,
'tasks',
$this->get_file_options(false),
$this->assignment->get_context(),
self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME,
self::ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION,
$submission->id
);
// If submission is empty, leave directly.
if ($this->is_empty($submission)) {
return true;
}
// Get submitted files.
$fs = get_file_storage();
$files = $fs->get_area_files(
$this->assignment->get_context()->id,
self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME,
self::ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION,
$submission->id,
'id',
false
);
// Check if a file is uploaded.
if (empty($files)) {
\core\notification::error(
get_string('no_submissionfile_warning', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME)
);
return true;
}
// Get the file.
$file = reset($files);
// Send file to backend (split across lines to avoid exceeding length).
$response = \assignsubmission_dta\dta_backend_utils::assignsubmission_dta_send_submission_to_backend(
$this->assignment,
$submission->id,
$file
);
// With a null response, return an error.
if (is_null($response)) {
return false;
}
// Convert received JSON to valid class instances.
$resultsummary = dta_result_summary::assignsubmission_dta_decode_json($response);
// Decode recommendations from response.
$recommendations = dta_recommendation::assignsubmission_dta_decode_json_recommendations($response);
// Use Moodle debugging instead of error_log/print_r (only in DEBUG_DEVELOPER mode).
// debugging('Recommendations: ' . json_encode($recommendations), DEBUG_DEVELOPER);
// Persist new results to database (split long lines).
dta_db_utils::assignsubmission_dta_store_result_summary_to_database(
$this->assignment->get_instance()->id,
$submission->id,
$resultsummary
);
// Store the array of recommendations in the database.
dta_db_utils::assignsubmission_dta_store_recommendations_to_database(
$this->assignment->get_instance()->id,
$submission->id,
$recommendations
);
return true;
}
/**
* Display a short summary of the test results of the submission.
*
* @param stdClass $submission Submission to show.
* @param bool $showviewlink Whether to show expand option.
* @return string Summary results HTML.
*/
public function view_summary(stdClass $submission, &$showviewlink): string {
$showviewlink = false; // Disable the default magnifying glass icon
return dta_view_submission_utils::assignsubmission_dta_generate_summary_html(
$this->assignment->get_instance()->id,
$submission->id
);
}
/**
* Display detailed results.
*
* @param stdClass $submission The submission for which to show results.
* @return string Detailed results HTML.
*/
public function view(stdClass $submission): string {
// Redirect to the new classes/view.php
$cmid = $this->assignment->get_course_module()->id;
$url = new moodle_url('/mod/assign/submission/dta/classes/view.php', [
'cmid' => $cmid,
'sid' => $submission->id
]);
redirect($url);
}
/**
* Generate array of allowed file types to upload.
*
* @param bool $settings Whether this is for assignment settings or active submission.
* @return array
*/
private function get_file_options(bool $settings): array {
$fileoptions = [
'subdirs' => 0,
'maxfiles' => 1,
'accepted_types' => (
$settings
? ['.txt']
: [
'.txt',
'.zip',
]
),
'return_types' => FILE_INTERNAL,
];
return $fileoptions;
}
/**
* Get file areas returns a list of areas this plugin stores files.
*
* @return array An array of fileareas (keys) and descriptions (values).
*/
public function get_file_areas(): array {
return [
self::ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION =>
get_string('dta_submissions_fa', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME),
self::ASSIGNSUBMISSION_DTA_FILEAREA_TEST =>
get_string('dta_tests_fa', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME),
];
}
/**
* Produce a list of files suitable for export that represent this feedback or submission.
*
* @param stdClass $submission The submission object.
* @param stdClass $user The user record (unused).
* @return array An array of files indexed by filename.
*/
public function get_files(stdClass $submission, stdClass $user): array {
$result = [];
$fs = get_file_storage();
$files = $fs->get_area_files(
$this->assignment->get_context()->id,
self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME,
self::ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION,
$submission->id,
'timemodified',
false
);
foreach ($files as $fileobj) {
// Do we return the full folder path or just the file name?
if (isset($submission->exportfullpath) && $submission->exportfullpath === false) {
$result[$fileobj->get_filename()] = $fileobj;
} else {
$result[$fileobj->get_filepath() . $fileobj->get_filename()] = $fileobj;
}
}
return $result;
}
/**
* The plugin is being uninstalled - cleanup.
*
* @return bool
*/
public function delete_instance(): bool {
dta_db_utils::assignsubmission_dta_uninstall_plugin_cleaup();
return true;
}
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* entity classes for DTA submission plugin result summary and test results
*
* @package assignsubmission_dta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @copyright Gero Lueckemeyer and student project teams
*/
defined('MOODLE_INTERNAL') || die();
/**
* entity class for DTA submission plugin result
*
* @package assignsubmission_dta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @copyright Gero Lueckemeyer and student project teams
*/
class DtaResult {
/**
* Broadly used in logic, parametrized for easier change.
*/
const COMPONENT_NAME = "assignsubmission_dta";
/**
* @var $packagename Package name of the test.
*/
public $packagename;
/**
* @var $classname Unit name of the test.
*/
public $classname;
/**
* @var $name Name of the test.
*/
public $name;
/**
* @var $state State is defined like below
*
* 0 UNKNOWN
* 1 SUCCESS
* 2 FAILURE
* 3 COMPILATIONERROR
*/
public $state;
/**
* @var $failuretype Type of test failure if applicable, "" otherwise.
*/
public $failuretype;
/**
* @var $failurereason Reason of test failure if applicable, "" otherwise.
*/
public $failurereason;
/**
* @var $stacktrace Stack trace of test failure if applicable, "" otherwise.
*/
public $stacktrace;
/**
* @var $columnnumber Column number of compile failure if applicable, "" otherwise.
*/
public $columnnumber;
/**
* @var $linenumber Line number of compile failure if applicable, "" otherwise.
*/
public $linenumber;
/**
* @var $position Position of compile failure if applicable, "" otherwise.
*/
public $position;
/**
* Returns the name of a state with the given number of display.
* @param int $state number of the state
* @return string name of state as defined
*/
public static function getstatename(int $state): string {
if ($state == 1) {
return get_string("tests_successful", self::COMPONENT_NAME);
} else if ($state == 2) {
return get_string("failures", self::COMPONENT_NAME);
} else if ($state == 3) {
return get_string("compilation_errors", self::COMPONENT_NAME);
} else {
return get_string("unknown_state", self::COMPONENT_NAME);
}
}
}
/**
* entity class for DTA submission plugin result
*
* @package assignsubmission_dta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @copyright Gero Lueckemeyer and student project teams
*/
class DtaResultSummary {
/**
* @var $timestamp Result timestamp for chronological ordering and deletion of previous results.
*/
public $timestamp;
/**
* @var $globalstacktrace Global stack trace if applicable, "" otherwise.
*/
public $globalstacktrace;
/**
* @var $successfultestcompetencies Successfully tested competencies according to tests and weights, "" otherwise.
*/
public $successfultestcompetencies;
/**
* @var overalltestcompetencies Overall tested competencies according to tests and weights, "" otherwise.
*/
public $overalltestcompetencies;
/**
* @var results List of detail results.
*/
public $results;
/**
* Decodes the JSON result summary returned by the backend service call into the plugin PHP data structure.
* @param string $jsonstring jsonString containing DtaResultSummary
* @return DtaResultSummary the result summary
*/
public static function decodejson(string $jsonstring): DtaResultSummary {
$response = json_decode($jsonstring);
$summary = new DtaResultSummary();
$summary->timestamp = $response->timestamp;
$summary->globalstacktrace = $response->globalstacktrace;
$summary->successfultestcompetencies = $response->successfulTestCompetencyProfile;
$summary->overalltestcompetencies = $response->overallTestCompetencyProfile;
$summary->results = self::decodejsonresultarray($response->results);
return $summary;
}
/**
* Decodes the array of JSON detail results returned by the backend service call into the plugin PHP data structure.
* @param array $jsonarray decoded json array of results array
* @return array of DtaResult
*/
private static function decodejsonresultarray($jsonarray): array {
$ret = [];
foreach ($jsonarray as $entry) {
$value = new DtaResult();
$value->packagename = $entry->packageName;
$value->classname = $entry->className;
$value->name = $entry->name;
$value->state = $entry->state;
$value->failuretype = $entry->failureType;
$value->failurereason = $entry->failureReason;
$value->stacktrace = $entry->stacktrace;
$value->columnnumber = $entry->columnNumber;
$value->linenumber = $entry->lineNumber;
$value->position = $entry->position;
$ret[] = $value;
}
return $ret;
}
/**
* Returns the number of detail results attached to the summary.
* @return int count of occurences
*/
public function resultcount(): int {
return count($this->results);
}
/**
* Returns the number of detail results with the given state attached to the summary.
* @param int $state state ordinal number
* @return int count of occurences provided state has
*/
public function stateoccurencecount(int $state): int {
$num = 0;
foreach ($this->results as $r) {
if ($r->state == $state) {
$num++;
}
}
return $num;
}
/**
* Returns the number of detail results with compilation errors attached to the summary.
* @return int count of occurences
*/
public function compilationerrorcount(): int {
return $this->stateoccurencecount(3);
}
/**
* Returns the number of detail results with test failures attached to the summary.
* @return int count of occurences
*/
public function failedcount(): int {
return $this->stateoccurencecount(2);
}
/**
* Returns the number of detail results with successful tests attached to the summary.
* @return int count of occurences
*/
public function successfulcount(): int {
return $this->stateoccurencecount(1);
}
/**
* Returns the number of detail results with an unknown result - mostly due to compile errors - attached to the summary.
* @return int count of occurences
*/
public function unknowncount(): int {
return $this->stateoccurencecount(0);
}
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file defines the admin settings for this plugin
*
* @package assignsubmission_dta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @copyright Gero Lueckemeyer and student project teams
*/
defined('MOODLE_INTERNAL') || die();
$settings->add(new admin_setting_configcheckbox("assignsubmission_dta/default",
new lang_string("default", "assignsubmission_dta"),
new lang_string("default_help", "assignsubmission_dta"), 0));
$settings->add(new admin_setting_configtext("assignsubmission_dta/backendHost",
new lang_string("backendHost", "assignsubmission_dta"),
new lang_string("backendHost_help", "assignsubmission_dta"), "http://backend:8080"));
/* Prevent word breaking in the grading table */
.dtaSubmissionSummary {
white-space: nowrap;
}
.dtaSubmissionSummary,
.dtaSubmissionDetails {
margin-top: 15px;
}
/* empty div between summary and detail table */
.dtaSpacer {
margin-top: 30px;
}
/****************************************************************
* Layout for the Detail view
****************************************************************/
.dtaTable {
display: inline-block;
max-width: 100%;
overflow: auto;
background: white !important;
border-radius: .1rem;
box-shadow: 0 .2rem .5rem rgba(0,0,0,.05),0 0 .05rem rgba(0,0,0,.1);
overflow-x: hidden;
}
.dtaTableHeaderRow {
font-weight: bold;
color: white !important;
background-color: gray !important;
}
.dtaTableRow {
background-color: unset !important;
border-top: .05rem solid lightgray;
}
.dtaTableHeaderRow th,
.dtaTableRow td {
padding: .9375em 1.25em;
}
.dtaStacktraceDetails {
max-height: 300px;
max-width: 70%;
overflow: auto;
}
.dtaResultUnknown {
border-left: 10px solid gray;
}
.dtaResultSuccess {
border-left: 10px solid green;
}
.dtaResultFailure {
border-left: 10px solid orange;
}
.dtaResultCompilationError {
border-left: 10px solid red;
}
.dtaTableRow:hover {
background-color: lightgray !important;
}
.dtaTableSpacer {
border-bottom: 2px solid darkgray;
}
/* More Details Button - Match Grade button style */
.dtaMoreDetailsButton {
background-color: #0f6cbf;
color: white;
border: 1px solid #0f6cbf;
padding: 6px 12px;
border-radius: 3px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
text-decoration: none;
display: inline-block;
transition: all 0.2s ease;
margin: 0;
vertical-align: top;
line-height: 1.4;
text-align: center;
min-width: 80px;
}
.dtaMoreDetailsButton:hover {
background-color: #0a5a9c;
border-color: #0a5a9c;
color: white;
text-decoration: none;
}
.dtaMoreDetailsButton:active {
background-color: #084a82;
border-color: #084a82;
}
.dtaMoreDetailsButton:focus {
outline: 2px solid #0f6cbf;
outline-offset: 2px;
}
/* Test Result Labels */
.dtaTestLabel {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
margin-right: 8px;
margin-bottom: 4px;
}
.dtaTestLabelSuccess {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.dtaTestLabelError {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.dtaTestLabelFailure {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
/* Test Results Container */
.dtaTestResults {
margin-top: 6px;
line-height: 1.4;
display: block;
padding-top: 0;
}
.dtaTestResults br {
margin-bottom: 4px;
}
/* Container for button and labels alignment */
.dtaSubmissionSummary {
white-space: nowrap;
vertical-align: top;
padding-top: 0;
margin-top: 0;
}
.dtaSubmissionSummary .dtaMoreDetailsButton {
margin-bottom: 8px;
margin-top: 0;
display: block;
position: relative;
top: -2px;
}
\ No newline at end of file
<div class="card my-3">
{{#title}}<div class="card-header">{{.}}</div>{{/title}}
<div class="card-body p-0">
<table class="generaltable table table-striped m-0">
{{#headings.0}}
<thead>
<tr>
{{#headings}}<th>{{.}}</th>{{/headings}}
</tr>
</thead>
{{/headings.0}}
<tbody>
{{#rows}}
<tr>
{{#cells}}<td>{{{text}}}</td>{{/cells}}
</tr>
{{/rows}}
{{^rows}}
<tr><td colspan="{{headingscount}}">{{#str}} nothingtodisplay, core {{/str}}</td></tr>
{{/rows}}
</tbody>
</table>
</div>
</div>
<div class="card my-3">
{{#title}}<div class="card-header">{{.}}</div>{{/title}}
<div class="card-body p-0">
<table class="generaltable table table-striped m-0">
{{#headings.0}}
<thead>
<tr>
{{#headings}}
<th class="dtaTableHeader">
{{#sortable}}
<form method="post" style="display:inline">
{{text}}
<input type="submit" name="sortbutton" value="{{#sortdir}}{{#asc}}↑{{/asc}}{{^asc}}↓{{/asc}}{{/sortdir}}" class="sort-button">
<input type="hidden" name="sortby" value="{{sortby}}">
<input type="hidden" name="sortdir" value="{{#sortdir}}{{#asc}}desc{{/asc}}{{^asc}}asc{{/asc}}{{/sortdir}}">
</form>
{{/sortable}}
{{^sortable}}
{{text}}
{{/sortable}}
</th>
{{/headings}}
</tr>
</thead>
{{/headings.0}}
<tbody>
{{#rows}}
<tr>
{{#cells}}<td>{{text}}</td>{{/cells}}
</tr>
{{/rows}}
{{^rows}}
<tr><td colspan="{{headingscount}}">{{#str}} nothingtodisplay, core {{/str}}</td></tr>
{{/rows}}
</tbody>
</table>
</div>
</div>
<div class="card my-3">
{{#title}}<div class="card-header">{{.}}</div>{{/title}}
<div class="card-body p-0">
<table class="generaltable table table-striped m-0">
<tbody>
{{#rows}}
<tr{{#cssclass}} class="{{.}}"{{/cssclass}}>
<th scope="row" class="w-50">{{label}}</th>
<td>{{value}}</td>
</tr>
{{/rows}}
{{^rows}}
<tr><td colspan="2">{{#str}} nothingtodisplay, core {{/str}}</td></tr>
{{/rows}}
</tbody>
</table>
</div>
</div>
This diff is collapsed.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file contains the version information for the onlinetext DTA plugin
*
* @package assignsubmission_dta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @copyright Gero Lueckemeyer and student project teams
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2023123001;
$plugin->requires = 2020061525; // Moodle 3.9 LTS. Older unsupported versions from the 3.x branch should also work.
$plugin->component = 'assignsubmission_dta';
$plugin->maturity = MATURITY_STABLE;
$plugin->release = "3.1.0";
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment