Verified Commit 21b5fb83 authored by Lukas Wiest's avatar Lukas Wiest 🚂
Browse files

refactor: rework plugin for new backend and relabel to DTT

BREAKING CHANGE: can't process old JSON format anymore
parent 50e6400c
pipeline {
agent any
tools {
jdk 'Java11'
}
stages {
stage('Checkout') {
steps {
git branch: 'master',
url: 'https://transfer.hft-stuttgart.de/gitlab/HFTSoftwareProject/moodle-assignsubmission_modocot.git'
}
}
stage('Build') {
steps {
sh 'chmod 744 update_plugin_script.sh && ./update_plugin_script.sh'
}
}
}
}
# Moodle Dockerized Code Testing (MoDoCoT) Plugin
A Moodle plugin to assist teachers correcting JUnit exercises.
This plugin allows students to submit their Java exercises, let them be tested against
a set of JUnit tests (that have been priorly provided by the teacher) and receive immediate feedback
on the test results.
For this to work, the plugin communicates with an external webservice providing essentially the following services on the given paths:
* POST **/v1/unittest**: Expects the assignment id and a zip file containing a repo.txt with the repository-link and an optional line for credentials when using a private repository.
You need to add the credentials like this: username:passwort or username:auth-token.
* POST **/v1/tasks**: Expects the assignment id and a zip file containing a repo.txt with the repository-link and an optional line for credentials when using a private repository.
You need to add the credentials like this: username:passwort or username:auth-token.
Returns the results as JSON.
* DELETE **/v1/unittest?assignmentId={id}**: Triggers the deletion of the test files.
See here for an implementation of the webservice: [MoDoCoT Backend](https://transfer.hft-stuttgart.de/gitlab/HFTSoftwareProject/MoDoCoT-Backend.git)
## Installation/Configuration
* Install this plugin by using the Moodle web installer, or by extracting the plugin archive to {Moodle_Root}/mod/assign/submission/modocot and visting the admins notifications page.
* Visit the plugin's settings and configure the base URL of the web service to use. (You need a running webservice to use, see [MoDoCoT Backend](https://transfer.hft-stuttgart.de/gitlab/HFTSoftwareProject/MoDoCoT-Backend.git) for a working solution)
* Done!
## Usage (Teacher)
* Create an Assignment
* In the Assignment settings: Scroll to the section **Submission types** and check the type **Dockerized Code Testing**
* Once **Dockerized Code Testing** is checked, upload a *single* ZIP file containing your repository URL in the corresponding Excercise repository ZIP upload environment.
* View aggregated test results in the grading table column **Dockerized Code Testing**
* View detailed results of a particular submission by clicking the magnifyer icon in the respective cell of the Dockerized Code Testing column of the grading table
## Usage (Student)
* Navigate to the assignment
* Press **Add Submission** respectively **Edit Submission**
* Upload a *single* ZIP file containing your repository URL that you will to be tested and click **Save changes**
* View your test results in the **Dockerized Code Testing** row of the submission status table.
## Technical Details
For the communication between the plugin and the webservice the data is en/decoded as JSON.
Here is an example JSON response after uploading the task Java file.
```JSON
{
"testResults": [
{
"testName": "CalculatorTest",
"testCount": 5,
"failureCount": 0,
"successfulTests": [
"div",
"mult",
"sub",
"add",
"sum"
],
"testFailures": []
},
{
"testName": "CalculatorSecondTest",
"testCount": 5,
"failureCount": 1,
"successfulTests": [
"add2",
"sub2",
"div2",
"sum2"
],
"testFailures": [
{
"testHeader": "mult2(CalculatorSecondTest)",
"message": "expected:<15.0> but was:<10.0>",
"trace": "stacktrace (if existent)"
}
]
}
```
The above shows the result of two JUnit test files (CalculatorTest and CalculatorSecondTest). The field “test count” indicates the number of test methods within the test file. The field “Failure count” indicates how many tests have failed and the field “successful test” indicates the method names of passed tests. In case a test failed, the necessary information can be found as an entry in the "testFailures" array.
If there was an compilation error the relevant information is part of the "compilationErrors" array as shown below.
```JSON
"compilationErrors": [
{
"code": "compiler.err.expected",
"columnNumber": 0,
"kind": "ERROR",
"lineNumber": 0,
"message": "';' expected",
"position": 46,
"filePath": "/tmp/TaskNotCompilable.java",
"startPosition": 46,
"endPosition": 46
}
]
}
```
## Bugs and Improvements?
If you've found a bug or if you've made an improvement to this plugin and want to share your code, please
open an issue in our Github project:
https://transfer.hft-stuttgart.de/gitlab/HFTSoftwareProject/moodle-assignsubmission_modocot/-/issues
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/assign/submission/modocot/db" VERSION="20161201" COMMENT="XMLDB file for Moodle mod/assign/submission/modocot"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="assignsubmission_modocot" COMMENT="Info about JUnit executor submissions for assignments.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="assignment_id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="submission_id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="The unique id for this submission info."/>
<KEY NAME="fk_assignment" TYPE="foreign" FIELDS="assignment_id" REFTABLE="assign" REFFIELDS="id" COMMENT="The assignment instance this submission relates to"/>
<KEY NAME="fk_submission" TYPE="foreign" FIELDS="submission_id" REFTABLE="assign_submission" REFFIELDS="id" COMMENT="The submission this file submission relates to."/>
</KEYS>
</TABLE>
<TABLE NAME="modocot_testresult" COMMENT="Info about the JUnit executor test results.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="testname" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="The name of the test, corresponds to the filename of the Junit test file"/>
<FIELD NAME="testcount" TYPE="int" LENGTH="3" NOTNULL="false" SEQUENCE="false" COMMENT="The overall number of tests, inclusively the failed ones."/>
<FIELD NAME="modocot_id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="succtests" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Comma separated list of the successful test names"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="fk_modocot" TYPE="foreign" FIELDS="modocot_id" REFTABLE="assignsubmission_modocot" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="modocot_testfailure" COMMENT="Info about the failures occured during test execution.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="testresult_id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="testheader" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="message" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="trace" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="fk_testresult" TYPE="foreign" FIELDS="testresult_id" REFTABLE="modocot_testresult" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="modocot_compilationerror" COMMENT="Info about compilation errors while trying to compile the test classes.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="modocot_id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="columnnumber" TYPE="int" LENGTH="4" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="linenumber" TYPE="int" LENGTH="5" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="message" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="position" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The character offset from the beginning of the source object that indicates the location of the problem."/>
<FIELD NAME="filename" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="fk_modocot" TYPE="foreign" FIELDS="modocot_id" REFTABLE="assignsubmission_modocot" REFFIELDS="id"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/assign/submission/dtt/db" VERSION="20210107" COMMENT="XMLDB file for Moodle mod/assign/submission/dtt"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="assignsubmission_dtt_summary" COMMENT="DTT testrun summary">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="assignment_id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="submission_id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timestamp" TYPE="int" LENGTH="10"/>
<FIELD NAME="global_stacktrace" TYPE="text"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="fk_assignment" TYPE="foreign" FIELDS="assignment_id" REFTABLE="assign" REFFIELDS="id" COMMENT="The assignment instance this summary relates to"/>
<KEY NAME="fk_submission" TYPE="foreign" FIELDS="submission_id" REFTABLE="assign_submission" REFFIELDS="id" COMMENT="The submission this summary relates to."/>
</KEYS>
</TABLE>
<TABLE NAME="assignsubmission_dtt_result" COMMENT="DTT testrun single test results">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="assignment_id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="submission_id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true"/>
<FIELD NAME="state" TYPE="int" LENGTH="10" NOTNULL="true"/>
<FIELD NAME="failure_type" TYPE="char" LENGTH="255"/>
<FIELD NAME="failure_reason" TYPE="char" LENGTH="255"/>
<FIELD NAME="stacktrace" TYPE="text"/>
<FIELD NAME="column_number" TYPE="int" LENGTH="10"/>
<FIELD NAME="line_number" TYPE="int" LENGTH="10"/>
<FIELD NAME="position" TYPE="int" LENGTH="10"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id,assignment_id,submission_id"/>
<KEY NAME="fk_assignment" TYPE="foreign" FIELDS="assignment_id" REFTABLE="assign" REFFIELDS="id" COMMENT="The assignment instance this result relates to"/>
<KEY NAME="fk_submission" TYPE="foreign" FIELDS="submission_id" REFTABLE="assign_submission" REFFIELDS="id" COMMENT="The submission this result relates to."/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
\ No newline at end of file
<?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_modocot
* @copyright 2020 hft ip2
* @package assignsubmission_dtt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
......@@ -29,7 +14,7 @@ defined('MOODLE_INTERNAL') || die();
* @param int $oldversion
* @return bool
*/
function xmldb_assignsubmission_modocot_upgrade($oldversion) {
function xmldb_assignsubmission_dtt_upgrade($oldversion) {
global $CFG;
// Moodle v2.8.0 release upgrade line.
......
<?php
/**
* Strings for component "assignsubmission_dtt", language "en"
*
* @package assignsubmission_dtt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string["pluginname"] = "Dockerized Testing Toolkit";
$string["submission_label"] = "DTT submission configuration or zip-packed project";
$string["submission_label_help"] = "Either upload a single textfile containing one DTT 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"] = "DTT test configuration";
$string["submission_settings_label_help"] = "single text file with DTT test URI";
$string["backendHost_help"] = "Address/Name and Port of backend server";
$string["backendHost_not_set"] = "The Dockerized Testing Toolkit backend URL is not configured";
$string["enabled"] = $string["pluginname"];
$string["enabled_help"] = "If enabled, you'll have to upload a textfile containing a valid DTT 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 or as well by providing a textifle with a valid DTT URI pointing to the repository with their submission logic";
$string["no_submissionfile_warning"] = "Submission type is \"Dockerized Testing Toolkit\" but no configuration file or submission archive uploaded";
$string["no_testfile_warning"] = "Submission type is \"Dockerized Testing Toolkit\" but no configuration file 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";
<?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 modocot plugin
* This file contains the moodle hooks for the submission DTT plugin
*
* @package assignsubmission_modocot
* @package assignsubmission_dtt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
......@@ -33,12 +19,14 @@ defined('MOODLE_INTERNAL') || die();
* @param bool $forcedownload
* @return bool false if file not found, does not return if found - just send the file
*/
function assignsubmission_modocot_pluginfile($course,
$cm,
context $context,
$filearea,
$args,
$forcedownload) {
function assignsubmission_modocot_pluginfile(
$course,
$cm,
context $context,
$filearea,
$args,
$forcedownload
) {
global $DB, $CFG;
if ($context->contextlevel != CONTEXT_MODULE) {
......@@ -74,7 +62,7 @@ function assignsubmission_modocot_pluginfile($course,
$relativepath = implode('/', $args);
$fullpath = "/{$context->id}/assignsubmission_modocot/$filearea/$itemid/$relativepath";
$fullpath = "/{$context->id}/assignsubmission_dtt/$filearea/$itemid/$relativepath";
$fs = get_file_storage();
if (!($file = $fs->get_file_by_hash(sha1($fullpath))) || $file->is_directory()) {
......
<?php
defined('MOODLE_INTERNAL') || die();
// import various files logic is organized in
require_once($CFG->dirroot . '/mod/assign/submission/dtt/models/DttResult.php');
require_once($CFG->dirroot . '/mod/assign/submission/dtt/utils/database.php');
require_once($CFG->dirroot . '/mod/assign/submission/dtt/utils/backend.php');
require_once($CFG->dirroot . '/mod/assign/submission/dtt/utils/view.php');
/**
* library class for DTT submission plugin extending assign submission plugin base class
*
* @package assignsubmission_dtt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class assign_submission_dtt extends assign_submission_plugin {
// broadly used in logic, parametrized for easier change
const COMPONENT_NAME = "assignsubmission_dtt";
// draft file area for modocot tests to be uploaded by the teacher
const ASSIGNSUBMISSION_DTT_DRAFT_FILEAREA_TEST = "tests_draft_dtt";
// file area for modocot tests to be uploaded by the teacher
const ASSIGNSUBMISSION_DTT_FILEAREA_TEST = "tests_dtt";
// file area for modocot submission assignment
const ASSIGNSUBMISSION_DTT_FILEAREA_SUBMISSION = "submissions_dtt";
// ========== abstract mehtods to be implemented ========== //
/**
* get plugin name
* @return string
*/
public function get_name(): string {
return get_string("pluginname", self::COMPONENT_NAME);
}
// ========== end of section ========== //
// ========== parent methods overloaded ========== //
// ===== assignment settings ===== //
/**
* 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
"filemanager",
// unique element name in form
self::ASSIGNSUBMISSION_DTT_DRAFT_FILEAREA_TEST,
// label shown to user left of filemanager
get_string("submission_settings_label", self::COMPONENT_NAME),
// attributes
null,
// options array
$this->get_file_options(true)
);
// add help button to added filemanager
$mform->addHelpButton(
// form-unique element id to add button to
self::ASSIGNSUBMISSION_DTT_DRAFT_FILEAREA_TEST,
// key to search for
"submission_settings_label",
// language file to use
self::COMPONENT_NAME
);
// only show filemanager, if our plugin is enabled
$mform->hideIf(
// form-unique element id to hide
self::ASSIGNSUBMISSION_DTT_DRAFT_FILEAREA_TEST,
// condition to check
self::COMPONENT_NAME . '_enabled',
// state to match for hiding
'notchecked'
);
}
/**
* Allows the plugin to update the defaultvalues passed in to
* the settings form (needed to set up draft areas for editor
* and filemanager elements)
* @param array $defaultvalues
*/
public function data_preprocessing(&$defaultvalues): void {
$draftitemid = file_get_submitted_draft_itemid(self::ASSIGNSUBMISSION_DTT_DRAFT_FILEAREA_TEST);
// prepare draft area with created draft filearea
file_prepare_draft_area(
// draft filemanager form-unique id
$draftitemid,
// id of current assignment
$this->assignment->get_context()->id,
// component name
self::COMPONENT_NAME,
// proper filearea
self::ASSIGNSUBMISSION_DTT_FILEAREA_TEST,
// entry id
0,
// options array?
array('subdirs' => 0)
);
$defaultvalues[self::ASSIGNSUBMISSION_DTT_DRAFT_FILEAREA_TEST] = $draftitemid;
}
/**
* Save settings of assignment submission settings
*
* @param stdClass $data
* @return bool
*/
public function save_settings(stdClass $data): bool {
// if the assignment has no filemanager for our plugin just leave
$draftFileManagerId = self::ASSIGNSUBMISSION_DTT_DRAFT_FILEAREA_TEST;
if (!isset($data->$draftFileManagerId)) {
return true;
}
// store files from draft filearea to proper one
file_save_draft_area_files(
// form-unique element id of draft filemanager from the edit
$data->$draftFileManagerId,
// id of the assignment we edit right now
$this->assignment->get_context()->id,
// component name
self::COMPONENT_NAME,
// proper file area
self::ASSIGNSUBMISSION_DTT_FILEAREA_TEST,
// entry id
0
);
// get files from proper filearea
$fs = get_file_storage();
$files = $fs->get_area_files(
// id of current assignment
$this->assignment->get_context()->id,
// component name
self::COMPONENT_NAME,
// proper filearea
self::ASSIGNSUBMISSION_DTT_FILEAREA_TEST,
// entry id
0,
// ?
'id',
// ?
false
);
// check if a file is uploaded
if (empty($files)) {
\core\notification::error(get_string("no_testfile_warning", self::COMPONENT_NAME));
return true;
}
// get file
$file = reset($files);
// send file to backend
return DttBackendUtils::sendTestConfigToBackend($this->assignment, $file);
}
// ===== student submission ===== //
/**
* Add elements to submission form
*
* @param mixed $submission stdClass|null
* @param MoodleQuickForm $mform
* @param stdClass $data
* @param int $userid
* @return bool
*/
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::COMPONENT_NAME,
self::ASSIGNSUBMISSION_DTT_FILEAREA_SUBMISSION,
$submissionorgrade ? $submissionorgrade->id : 0
);
// add filemanager to form
$mform->addElement(
// filemanager
'filemanager',
// form-unique identifier
'tasks_filemanager',
// label to show next to filemanager
get_string("submission_label", self::COMPONENT_NAME),
// attributes
null,
// options
$this->get_file_options(false)
);
// add help button
$mform->addHelpButton(
// what form item to add a helpbutton
"tasks_filemanager",
// what key to use
"submission_label",
// in which language file to look in
self::COMPONENT_NAME
);
return true;
}
/**
* @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_DTT_FILEAREA_SUBMISSION) == 0;
}
/**
* Count the number of files in a filearea
*
* @param int $submissionId submission id to check
* @param string $areaId filearea id to count
* @return int
*/
private function count_files($submissionId, $areaId) {
$fs = get_file_storage();
$files = $fs->get_area_files($this->assignment->get_context()->id,
self::COMPONENT_NAME,
$areaId,
$submissionId,
'id',
false);
return count($files);
}
/**
* Save data to the database
*
* @param stdClass $submission
* @param stdClass $data
* @return bool
*/
public function save(stdClass $submission, stdClass $data) {
$data = file_postupdate_standard_filemanager(
$data,
'tasks',
$this->get_file_options(false),
$this->assignment->get_context(),
self::COMPONENT_NAME,
self::ASSIGNSUBMISSION_DTT_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(
// id of current assignment
$this->assignment->get_context()->id,
// component name
self::COMPONENT_NAME,
// proper filearea
self::ASSIGNSUBMISSION_DTT_FILEAREA_SUBMISSION,
// entry id
$submission->id,
// ?
'id',
// ?
false
);
// check if a file is uploaded
if (empty($files)) {
\core\notification::error(get_string("no_submissionfile_warning", self::COMPONENT_NAME));
return true;
}
// Get the file and post it to our backend.
$file = reset($files);
$response = DttBackendUtils::sendSubmissionToBackend($this->assignment, $file);
// if we got a null response, return with error
if (is_null($response)) {
return false;
}
// convert received json to valid class instances
$resultSummary = DttResultSummary::decodeJson($response);
// persist new results to database
DbUtils::storeResultSummaryToDatabase($this->assignment->get_instance()->id, $submission->id, $resultSummary);
return true;
}
// ===== view submission results ===== //
/**
* Display a short summary of the test results of the submission
* This is diplayed as default view, with the option to expand
* to the full detailed results.
*
* @param stdClass $submission to show
* @param bool $showviewlink configuration variable to show expand option
* @return string summary results html
*/
public function view_summary(stdClass $submission, & $showviewlink) {
$showviewlink = true;
return ViewSubmissionUtils::generateSummaryHtml(
$this->assignment->get_instance()->id,
$submission->id
);
}
/**
* Display detailed results
*
* @param stdClass $submission the submission the results are shown for.
* @return string detailed results html
*/
public function view(stdClass $submission) {
return ViewSubmissionUtils::generateDetailHtml(
$this->assignment->get_instance()->id,
$submission->id
);
}
// ========== end of section ========== //
/**
* generate array of allowed filetypes to upload.
*
* @param bool $settings switch to define if list for assignment settings
* or active submission should be returned
*
* @return array
*/
private function get_file_options(bool $settings): array {
$fileoptions = array('subdirs' => 0,
"maxfiles" => 1,
'accepted_types' => ($settings ? array(".txt") : array(".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() {
return array(
self::ASSIGNSUBMISSION_DTT_FILEAREA_SUBMISSION => get_string("modocot_submissions_fa", self::COMPONENT_NAME),
self::ASSIGNSUBMISSION_DTT_FILEAREA_TEST => get_string("modocot_tests_fa", self::COMPONENT_NAME)
);
}
/**
* Produce a list of files suitable for export that represent this feedback or submission
*
* @param stdClass $submission The submission
* @param stdClass $user The user record - unused
* @return array - return an array of files indexed by filename
*/
public function get_files(stdClass $submission, stdClass $user) {
$result = array();
$fs = get_file_storage();
$files = $fs->get_area_files($this->assignment->get_context()->id,
self::COMPONENT_NAME,
self::ASSIGNSUBMISSION_DTT_FILEAREA_SUBMISSION,
$submission->id,
'timemodified',
false);
foreach ($files as $file) {
// Do we return the full folder path or just the file name?
if (isset($submission->exportfullpath) && $submission->exportfullpath == false) {
$result[$file->get_filename()] = $file;
} else {
$result[$file->get_filepath().$file->get_filename()] = $file;
}
}
return $result;
}
/**
* The plugin is beeing uninstalled - cleanup
*
* @return bool
*/
public function delete_instance() {
DbUtils::uninstallPluginCleanUp();
return true;
}
}
<?php
class DttResult {
public $name;
/**
* State is defined like below
*
* 0 UNKNOWN
* 1 SUCCESS
* 2 FAILURE
* 3 COMPILATIONERROR
*/
public $state;
public $failureType;
public $failureReason;
public $stacktrace;
public $columnNumber;
public $lineNumber;
public $position;
/**
* @return name of state like defined
*/
public static function getStateName(int $state): string {
if ($state == 1) {
return "success";
} else if ($state == 2) {
return "failed";
} else if ($state == 3) {
return "compilation error";
} else {
return "unknown";
}
}
}
class DttResultSummary {
public $timestamp;
public $globalStacktrace;
public $results;
/**
* @param string $jsonString jsonString containing DttResultSummary
* @return DttResultSummary
*/
public static function decodeJson($jsonString): DttResultSummary {
$response = json_decode($jsonString);
$summary = new DttResultSummary();
$summary->timestamp = $response->timestamp;
$summary->globalStacktrace = $response->globalStacktrace;
$summary->results = self::decodeJsonResultArray($response->results);
return $summary;
}
/**
* @param array $jsonArray decoded json array of results array
* @return array of DttResult
*/
private static function decodeJsonResultArray($jsonArray): array {
$ret = array();
foreach ($jsonArray as $entry) {
$value = new DttResult();
$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;
}
/**
* @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;
}
public function compilationErrorCount(): int {
return $this->stateOccurenceCount(3);
}
public function failedCount(): int {
return $this->stateOccurenceCount(2);
}
public function resultCount(): int {
return count($this->results);
}
public function successfulCount(): int {
return $this->stateOccurenceCount(1);
}
public function unknownCount(): int {
return $this->stateOccurenceCount(0);
}
}
......@@ -3,17 +3,17 @@
/**
* This file defines the admin settings for this plugin
*
* @package assignsubmission_modocot
* @package assignsubmission_dtt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$settings->add(new admin_setting_configcheckbox("assignsubmission_modocot/default",
new lang_string("default", "assignsubmission_modocot"),
new lang_string("default_help", "assignsubmission_modocot"), 0));
$settings->add(new admin_setting_configcheckbox("assignsubmission_dtt/default",
new lang_string("default", "assignsubmission_dtt"),
new lang_string("default_help", "assignsubmission_dtt"), 0));
$settings->add(new admin_setting_configtext("assignsubmission_modocot/wsbase",
new lang_string("wsbase", "assignsubmission_modocot"),
new lang_string("wsbase_help", "assignsubmission_modocot"), ""));
$settings->add(new admin_setting_configtext("assignsubmission_dtt/backendHost",
new lang_string("backendHost", "assignsubmission_dtt"),
new lang_string("backendHost_help", "assignsubmission_dtt"), "http://backend:8080"));
/* Prevent word breaking in the grading table */
.dttSubmissionSummary {
white-space: nowrap;
}
.dttSubmissionSummary,
.dttSubmissionDetails {
margin-top: 15px;
}
/* empty div between summary and detail table */
.dttSpacer {
margin-top: 30px;
}
/****************************************************************
* Layout for the Detail view
****************************************************************/
.dttTable {
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;
}
.dttTableHeaderRow {
font-weight: bold;
color: white !important;
background-color: gray !important;
}
.dttTableRow {
background-color: unset !important;
border-top: .05rem solid lightgray;
}
.dttTableHeaderRow th,
.dttTableRow td {
padding: .9375em 1.25em;
}
.dttStacktraceDetails {
max-height: 300px;
max-width: 70%;
overflow: auto;
}
.dttResultUnknown {
border-left: 10px solid gray;
}
.dttResultSuccess {
border-left: 10px solid green;
}
.dttResultFailure {
border-left: 10px solid orange;
}
.dttResultCompilationError {
border-left: 10px solid red;
}
.dttTableRow:hover {
background-color: lightgray !important;
}
.dttTableSpacer {
border-bottom: 2px solid darkgray;
}
<?php
class DttBackendUtils {
/**
* @return string backend host base url
*/
private static function getBackendBaseUrl(): string {
$backendAddress = get_config(assign_submission_dtt::COMPONENT_NAME, "backendHost");
if (empty($backendAddress)) {
\core\notification::error(get_string("backendHost_not_set", assign_submission_dtt::COMPONENT_NAME));
}
return $backendAddress;
}
/**
* Sends the configuration textfile uploaded by prof to the backend
*
* @param $assignment assignment this test-config belongs to
* @param $file uploaded test-config
*
* @return bool true if no error occurred
*/
public static function sendTestConfigToBackend($assignment, $file): bool {
$backendAddress = self::getBackendBaseUrl();
if (empty($backendAddress)) {
return true;
}
// set endpoint for test upload
$url = $backendAddress . "/v1/unittest";
// prepare params
$params = array(
"unitTestFile" => $file,
"assignmentId" => $assignment->get_instance()->id
);
// if request returned null, return false to indicate failure
if (is_null(self::post($url, $params))) {
return false;
} else {
return true;
}
}
/**
* Sends sumbission config or archive to backend to be tested
*
* @param $assignment assignment this submission is done for
* @param $file submission config file or archive with submission
*
* @return string json string with testresults or null on error
*/
public static function sendSubmissionToBackend($assignment, $file): ?string {
$backendAddress = self::getBackendBaseUrl();
if (empty($backendAddress)) {
return true;
}
// set endpoint for test upload
$url = $backendAddress . "/v1/task";
// prepare params
$params = array(
"taskFile" => $file,
"assignmentId" => $assignment->get_instance()->id
);
return self::post($url, $params);
}
/**
* @param string $url full url to request to
* @param array $params parameters for http-request
*
* @return string received body on success or null on error
*/
private static function post($url, $params): ?string {
if (!isset($url) || !isset($params)) {
return false;
}
$options = array(
"CURLOPT_RETURNTRANSFER" => true
);
$curl = new curl();
$response = $curl->post($url, $params, $options);
// check state of request, if response code is a 2xx return the answer
$info = $curl->get_info();
if ($info["http_code"] >= 200 && $info["http_code"] < 300) {
return $response;
}
// Something went wrong, return null and give an error msg
debugging(assign_submission_dtt::COMPONENT_NAME . ": Post file to server was not successful: http_code=" . $info["http_code"]);
if ($info['http_code'] >= 400 && $info['http_code'] < 500) {
\core\notification::error(get_string("http_client_error_msg", assign_submission_dtt::COMPONENT_NAME));
return null;
} else if ($info['http_code'] >= 500 && $info['http_code'] < 600) {
\core\notification::error(get_string("http_server_error_msg", assign_submission_dtt::COMPONENT_NAME));
return null;
} else {
\core\notification::error(get_string("http_unknown_error_msg", assign_submission_dtt::COMPONENT_NAME));
return null;
}
}
}
<?php
class DbUtils {
// summary database table name
private const TABLE_SUMMARY = "assignsubmission_dtt_summary";
// result database table name
private const TABLE_RESULT = "assignsubmission_dtt_result";
/**
* get's summary with all corresponding result entries
*
* @param int $assignmentId assignment id to search for
* @param int $submissionId submission id to search for
* @return DttResultSummary representing given submission
*/
public static function getResultSummaryFromDatabase(
int $assignmentId,
int $submissionId
): DttResultSummary {
global $DB;
// fetch data from database
$summaryDbRecord = $DB->get_record(self::TABLE_SUMMARY, array(
"assignment_id" => $assignmentId,
"submission_id" => $submissionId
));
$resultsDbArray = $DB->get_records(self::TABLE_RESULT, array(
"assignment_id" => $assignmentId,
"submission_id" => $submissionId
));
// create summary instance
$summary = new DttResultSummary();
$summary->timestamp = $summaryDbRecord->timestamp;
$summary->globalStacktrace = $summaryDbRecord->global_stacktrace;
$summary->results = array();
// create result instances and add to array of summary instance
foreach($resultsDbArray as $rr) {
$result = new DttResult();
$result->name = $rr->name;
$result->state = $rr->state;
$result->failureType = $rr->failure_type;
$result->failureReason = $rr->failure_reason;
$result->stacktrace = $rr->stacktrace;
$result->columnNumber = $rr->column_number;
$result->lineNumber = $rr->line_number;
$result->position = $rr->position;
$summary->results[] = $result;
}
return $summary;
}
/**
* save given result summary and single results to database
* under given assignment and submission id
*
* @param int assignmentId assigment this is submission is linked to
* @param int submissionId submission of this result
* @param DttResultSummary instance to persist
*/
public static function storeResultSummaryToDatabase(
int $assignmentId,
int $submissionId,
DttResultSummary $summary
): void {
global $DB;
// prepare new database entries
$summaryRecord = new stdClass();
$summaryRecord->assignment_id = $assignmentId;
$summaryRecord->submission_id = $submissionId;
$summaryRecord->timestamp = $summary->timestamp;
$summaryRecord->global_stacktrace = $summary->globalStacktrace;
// prepare results to persist to array
$resultRecordArray = array();
foreach($summary->results as $r) {
$record = new stdClass();
$record->assignment_id = $assignmentId;
$record->submission_id = $submissionId;
$record->name = $r->name;
$record->state = $r->state;
$record->failure_type = $r->failureType;
$record->failure_reason = $r->failureReason;
$record->stacktrace = $r->stacktrace;
$record->column_number = $r->columnNumber;
$record->line_number = $r->lineNumber;
$record->position = $r->position;
$resultRecordArray[] = $record;
}
// if results exist yet, delete old values beforehand
$submission = $DB->get_record(self::TABLE_SUMMARY, array(
'assignment_id' => $assignmentId,
'submission_id' => $submissionId
));
if ($submission) {
$DB->delete_records(self::TABLE_RESULT, array(
'assignment_id' => $assignmentId,
'submission_id' => $submissionId
));
$DB->delete_records(self::TABLE_SUMMARY, array(
'assignment_id' => $assignmentId,
'submission_id' => $submissionId
));
}
// create summary and single result entries
$DB->insert_record(self::TABLE_SUMMARY, $summaryRecord);
foreach($resultRecordArray as $rr) {
$DB->insert_record(self::TABLE_RESULT, $rr);
}
}
/**
* cleans-up database if plugin get's uninstalled
*/
public static function uninstallPluginCleanUp(): void {
global $DB;
$DB->delete_records(self::TABLE_RESULT, null);
$DB->delete_records(self::TABLE_SUMMARY, null);
}
}
<?php
class ViewSubmissionUtils {
/**
* generates a short summary html
*
* @param int assignmentId assignment
* @param int submissionId submission to create a report for
* @return string html
*/
public static function generateSummaryHtml(
int $assignmentId,
int $submissionId
): string {
// fetch data
$summary = DbUtils::getResultSummaryFromDatabase($assignmentId, $submissionId);
$html = "";
// calculate success rate, if no unknown result states or compilation errors
$successRate = "?";
if ($summary->unknownCount() == 0 && $summary->compilationErrorCount() == 0) {
$successRate = round(($summary->successfulCount() / $summary->resultCount()) * 100, 2 );
}
// generate html
$html .= $summary->successfulCount() . "/";
$html .= ($summary->compilationErrorCount() == 0 && $summary->unknownCount() == 0)
? $summary->resultCount() . " (" . $successRate . "%)"
: "?";
$html .= " tests successful<br>";
if ($summary->compilationErrorCount() > 0) {
$html .= $summary->compilationErrorCount() . " compilation error(s)<br>";
}
if ($summary->unknownCount() > 0) {
$html .= $summary->unknownCount() . " test(s) with unknown state<br>";
}
return html_writer::div($html, "dttSubmissionSummary");
}
/**
* generates detailed view html
*
* @param int assignmentId assignment
* @param int submissionId submission to create a report for
*/
public static function generateDetailHtml(
int $assignmentId,
int $submissionId
): string {
// fetch data
$summary = DbUtils::getResultSummaryFromDatabase($assignmentId, $submissionId);
$html = "";
// define a few css classes and prepare html attribute arrays
$tableHeaderRowAttributes = array("class" => "dttTableHeaderRow");
$tableRowAttributes = array("class" => "dttTableRow");
$resultRowAttributes = $tableRowAttributes;
$unknownAttrib = 'dttResultUnknown';
$successAttrib = 'dttResultSuccess';
$failureAttrib = 'dttResultFailure';
$compErrorAttrib = 'dttResultCompilationError';
// summary table
$tmp = "";
$tmp .= html_writer::tag("th", "Summary", array("class" => "dttTableHeader"));
$tmp .= html_writer::empty_tag("th", array("class" => "dttTableHeader"));
$header = html_writer::tag("tr", $tmp, $tableHeaderRowAttributes);
$header = html_writer::tag("thead", $header);
$body = "";
$tmp = "";
$attributes = array("class" => "dttTableData");
$tmp .= html_writer::tag(
"td",
"result items in sum",
$attributes);
$tmp .= html_writer::tag(
"td",
$summary->resultCount(),
$attributes);
$resultRowAttributes = $tableRowAttributes;
$resultRowAttributes['class'] = $resultRowAttributes['class'] . " " . $unknownAttrib;
$body .= html_writer::tag("tr", $tmp, $resultRowAttributes);
$tmp = "";
$tmp .= html_writer::tag("td", "successes", $attributes);
$tmp .= html_writer::tag( "td", $summary->successfulCount(), $attributes);
$resultRowAttributes = $tableRowAttributes;
$successRate = "?";
if ($summary->unknownCount() > 0 || $summary->compilationErrorCount() > 0) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . " " . $unknownAttrib;
} else {
$successRate = round(($summary->successfulCount() / $summary->resultCount()) * 100, 2 );
if ($successRate < 50) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . " " . $compErrorAttrib;
} else if ($successRate < 75) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . " " . $failureAttrib;
} else {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . " " . $successAttrib;
}
}
$body .= html_writer::tag("tr", $tmp, $resultRowAttributes);
$tmp = "";
$tmp .= html_writer::tag("td", "failures", $attributes);
$tmp .= html_writer::tag("td", $summary->failedCount(), $attributes);
$resultRowAttributes = $tableRowAttributes;
if ($summary->failedCount() > 0) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . " " . $failureAttrib;
} else {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . " " . $successAttrib;
}
$body .= html_writer::tag("tr", $tmp, $resultRowAttributes);
$tmp = "";
$tmp .= html_writer::tag("td", "compilation errors", $attributes);
$tmp .= html_writer::tag("td", $summary->compilationErrorCount(), $attributes);
$resultRowAttributes = $tableRowAttributes;
if ($summary->compilationErrorCount() > 0) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . " " . $compErrorAttrib;
} else {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . " " . $successAttrib;
}
$body .= html_writer::tag("tr", $tmp, $resultRowAttributes);
$tmp = "";
$tmp .= html_writer::tag("td", "unknown state", $attributes);
$tmp .= html_writer::tag("td", $summary->unknownCount(), $attributes);
$resultRowAttributes = $tableRowAttributes;
if ($summary->unknownCount() > 0) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . " " . $unknownAttrib;
} else {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . " " . $successAttrib;
}
$body .= html_writer::tag("tr", $tmp, $resultRowAttributes);
$tmp = "";
$tmp .= html_writer::tag("td", html_writer::tag("b","success rate"), $attributes);
$tmp .= html_writer::tag(
"td",
html_writer::tag("b", $summary->successfulCount()
. "/" . (($summary->compilationErrorCount() == 0 && $summary->unknownCount() == 0) ? $summary->resultCount()
. " (" . $successRate . "%)"
: "?")),
$attributes);
$resultRowAttributes = $tableRowAttributes;
if ($summary->unknownCount() > 0 || $summary->compilationErrorCount() > 0) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . " " . $unknownAttrib;
} else {
if ($successRate < 50) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . " " . $compErrorAttrib;
} else if ($successRate < 75) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . " " . $failureAttrib;
} else {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . " " . $successAttrib;
}
}
$body .= html_writer::tag("tr", $tmp, $resultRowAttributes);
$body = html_writer::tag("tbody", $body);
$table = html_writer::tag("table", $header . $body, array("class" => "dttTable"));
$html .= $table;
// add empty div for spacing between summary and details table
$html .= html_writer::empty_tag("div", array("class" => "dttSpacer"));
// details table
$tmp = "";
$tmp .= html_writer::tag("th", "Details", array("class" => "dttTableHeader"));
$tmp .= html_writer::empty_tag("th", array("class" => "dttTableHeader"));
$header = html_writer::tag("tr", $tmp, $tableHeaderRowAttributes);
$header = html_writer::tag("thead", $header);
$body = "";
$spacerRow = null;
foreach($summary->results as $r) {
// add spacer first, if not null
if (!is_null($spacerRow)) {
$body .= $spacerRow;
}
// new copy of base attributes array
$resultRowAttributes = $tableRowAttributes;
// check which css class to add for the colored left-border according to resuls state
if ($r->state == 0) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . ' dttResultUnknown';
} else if ($r->state == 1) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . ' dttResultSuccess';
} else if ($r->state == 2) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . ' dttResultFailure';
} else if ($r->state == 3) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . ' dttResultCompilationError';
}
$tmp = "";
$tmp .= html_writer::tag(
"td",
"name",
$attributes);
$tmp .= html_writer::tag(
"td",
$r->name,
$attributes);
$body .= html_writer::tag("tr", $tmp, $resultRowAttributes);
$tmp = "";
$tmp .= html_writer::tag(
"td",
"state",
$attributes);
$tmp .= html_writer::tag(
"td",
DttResult::getStateName($r->state),
$attributes);
$body .= html_writer::tag("tr", $tmp, $resultRowAttributes);
// if state is something different than successful, show additional rows
if ($r->state != 1) {
$tmp = "";
$tmp .= html_writer::tag(
"td",
"failure type",
$attributes);
$tmp .= html_writer::tag(
"td",
$r->failureType,
$attributes);
$body .= html_writer::tag("tr", $tmp, $resultRowAttributes);
$tmp = "";
$tmp .= html_writer::tag(
"td",
"failure reason",
$attributes);
$tmp .= html_writer::tag(
"td",
$r->failureReason,
$attributes);
$body .= html_writer::tag("tr", $tmp, $resultRowAttributes);
// only show line, column and position if they have useful values
if (!is_null($r->lineNumber) && $r->lineNumber > 0) {
$tmp = "";
$tmp .= html_writer::tag(
"td",
"line number",
$attributes);
$tmp .= html_writer::tag(
"td",
$r->lineNumber,
$attributes);
$body .= html_writer::tag("tr", $tmp, $resultRowAttributes);
}
if (!is_null($r->columnNumber) && $r->columnNumber > 0) {
$tmp = "";
$tmp .= html_writer::tag(
"td",
"column number",
$attributes);
$tmp .= html_writer::tag(
"td",
$r->columnNumber,
$attributes);
$body .= html_writer::tag("tr", $tmp, $resultRowAttributes);
}
if (!is_null($r->position) && $r->position > 0) {
$tmp = "";
$tmp .= html_writer::tag(
"td",
"position",
$attributes);
$tmp .= html_writer::tag(
"td",
$r->position,
$attributes);
$body .= html_writer::tag("tr", $tmp, $resultRowAttributes);
}
$tmp = "";
$tmp .= html_writer::tag(
"td",
"stacktrace",
$attributes);
$tmp .= html_writer::tag(
"td",
html_writer::tag("details", $r->stacktrace, array("class" => "dttStacktraceDetails")),
$attributes);
$body .= html_writer::tag("tr", $tmp, $resultRowAttributes);
}
// set spacerRow value if null for next rount separation
if (is_null($spacerRow)) {
$spacerRow = html_writer::empty_tag("tr", array("class" => "dttTableSpacer"));
}
}
$html .= html_writer::tag("table", $header . $body, array("class" => "dttTable"));
// wrap generated html into final div
$html = html_writer::div($html, "dttSubmissionDetails");
return $html;
}
}
<?php
/**
* This file contains the version information for the onlinetext DTT plugin
*
* @package assignsubmission_dtt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2000000;
$plugin->requires = 2019111800; // Moodle 3.8.
$plugin->component = 'assignsubmission_dtt';
$plugin->maturity = MATURITY_STABLE;
$plugin->release = "2.0.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/>.
/**
* Strings for component "assignsubmission_modocot", language "en"
*
* @package assignsubmission_modocot
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string["pluginname"] = "Dockerized Code Testing";
$string["modocot"] = "Dockerized Code Testing";
$string["modocot_submissions_fa"] = "Dockerized Code Testing Submissions";
$string["modocot_tests_fa"] = "Dockerized Code Testing Tests";
$string['enabled'] = "Dockerized Code Testing";
$string['enabled_help'] = "If enabled, students are able to upload one ZIP file containing their repository with JUnit tasks which will then be tested against a teacher provided set of JUnit tests.";
$string["setting_unittests"] = "JUnit tests";
$string["setting_unittests_help"] = "A single ZIP file containg the JUnit tests, the students' submissions should be tested against.";
$string["wsbase_not_set"] = "The Dockerized Code Testing web service base URL is not configured.";
$string["unexpectederror"] = "An unexpected error occured.";
$string["badrequesterror"] = "The server could not process the request. Probably the submitted ZIP file is corrupted.";
$string["modocot_submission"] = "Excercise repository ZIP";
$string["modocot_submission_help"] = "A single ZIP file containing the repository with all the relevant java files and resources for this exercise.";
$string["no_testfile_warning"] = "Submission type is \"Dockerized Code Testing\" but no testfiles are uploaded.";
// 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["wsbase"] = "Base URL to the web service";
$string["wsbase_help"] = "The base URL to the web service, where all the tests and submissions will be sent and evaluated.";
\ No newline at end of file
This diff is collapsed.
/* Prevent word breaking in the grading table */
div.submissionmodocotgrading {
white-space: nowrap;
}
/****************************************************************
* Layout for the Detail view
****************************************************************/
div.failedTestWrapper {
overflow: auto;
}
div.failedtestcontent {
float: right;
width: 85%;
}
div.failedtestsidebar {
float: left;
width: 15%;
}
/****************************************************************
* Collapser for the stacktrace
****************************************************************/
.collapsible {
cursor: pointer;
display: block;
text-decoration: underline;
}
.collapsible + input{
display: none; /* hide the checkboxes */
}
.collapsible + input + div{
display:none;
}
.collapsible + input:checked + div{
display:block;
}
#!/bin/bash
docker exec -i moodle_compose bash <<EOF
git -C /bitnami/moodle/mod/assign/submission/modocot pull
echo "Updated"
exit
EOF
Markdown is supported
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