Commit 5ff5fafd authored by Lückemeyer's avatar Lückemeyer
Browse files

initial commit renamed from DTT to DTA with BREAKING change added result...

initial commit renamed from DTT to DTA with BREAKING change added result fields for competency assessment
1 merge request!1Coding style and recommendations
Showing with 2110 additions and 87 deletions
+2110 -87
LICENSE 0 → 100644
This diff is collapsed.
# MoodleDTA
# DTT Moodle Plugin
Dockerized Test Agent (DTA) plugin for Moodle LMS
This is the source code repository for the DTT Moodle Plugin.
## Getting started
Documentation for this is done with GitLab Pages using MkDocs and is available here:
[https://transfer.hft-stuttgart.de/pages/dtt/documentation][pages-link]
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
If you don't want to open the Webpage or it is not available for whatever reason,
the documentation root is found [here][docs-root]
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
[docs-root]: https://transfer.hft-stuttgart.de/gitlab/dtt/documentation/-/blob/master/docs/index.md
[pages-link]: https://transfer.hft-stuttgart.de/pages/dtt/documentation
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin https://transfer.hft-stuttgart.de/gitlab/HFTSoftwareProject/moodledta.git
git branch -M master
git push -uf origin master
```
## Integrate with your tools
- [ ] [Set up project integrations](https://transfer.hft-stuttgart.de/gitlab/HFTSoftwareProject/moodledta/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
dta.zip 0 → 100644
File added
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/assign/submission/dta/db" VERSION="20210107" COMMENT="XMLDB file for Moodle mod/assign/submission/dta"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="assignsubmission_dta_summary" COMMENT="DTA 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="successful_competencies" TYPE="char" LENGTH="80" NOTNULL="false"/>
<FIELD NAME="tested_competencies" TYPE="char" LENGTH="80" NOTNULL="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_dta_result" COMMENT="DTA 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="package_name" TYPE="char" LENGTH="255" NOTNULL="false"/>
<FIELD NAME="class_name" TYPE="char" LENGTH="255" NOTNULL="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
/**
* Upgrade code for install
*
* @package assignsubmission_dta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Stub for upgrade code
* @param int $oldversion
* @return bool
*/
function xmldb_assignsubmission_dta_upgrade($oldversion) {
global $CFG;
// Moodle v2.8.0 release upgrade line.
// Put any upgrade step following this.
// Moodle v2.9.0 release upgrade line.
// Put any upgrade step following this.
// Moodle v3.0.0 release upgrade line.
// Put any upgrade step following this.
// Moodle v3.1.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
<?php
/**
* Strings for component "assignsubmission_dta", language "en"
*
* @package assignsubmission_dta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string["pluginname"] = "Dockerized Testing Agent";
$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["backendHost_help"] = "Address/Name and Port of backend server";
$string["backendHost_not_set"] = "The Dockerized Testing Agent backend URL is not configured";
$string["enabled"] = $string["pluginname"];
$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";
$string["no_submissionfile_warning"] = "Submission type is \"Dockerized Testing Agent\" but no configuration file or submission archive uploaded";
$string["no_testfile_warning"] = "Submission type is \"Dockerized Testing Agent\" 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 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
*/
defined('MOODLE_INTERNAL') || die();
/**
* 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',
array('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);
}
\ No newline at end of file
<?php
defined('MOODLE_INTERNAL') || die();
// import various files logic is organized in
require_once($CFG->dirroot . '/mod/assign/submission/dta/models/DtaResult.php');
require_once($CFG->dirroot . '/mod/assign/submission/dta/utils/database.php');
require_once($CFG->dirroot . '/mod/assign/submission/dta/utils/backend.php');
require_once($CFG->dirroot . '/mod/assign/submission/dta/utils/view.php');
/**
* library class for DTA submission plugin extending assign submission plugin base class
*
* @package assignsubmission_dta
* @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
const COMPONENT_NAME = "assignsubmission_dta";
// draft file area for dta tests to be uploaded by the teacher
const ASSIGNSUBMISSION_DTA_DRAFT_FILEAREA_TEST = "tests_draft_dta";
// file area for dta tests to be uploaded by the teacher
const ASSIGNSUBMISSION_DTA_FILEAREA_TEST = "tests_dta";
// file area for dta submission assignment
const ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION = "submissions_dta";
// ========== abstract methods 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_DTA_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_DTA_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_DTA_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_DTA_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_DTA_FILEAREA_TEST,
// entry id
0,
// options array?
array('subdirs' => 0)
);
$defaultvalues[self::ASSIGNSUBMISSION_DTA_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_DTA_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_DTA_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_DTA_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 DtaBackendUtils::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_DTA_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_DTA_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_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(
// id of current assignment
$this->assignment->get_context()->id,
// component name
self::COMPONENT_NAME,
// proper filearea
self::ASSIGNSUBMISSION_DTA_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 = DtaBackendUtils::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 = DtaResultSummary::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_DTA_FILEAREA_SUBMISSION => get_string("dta_submissions_fa", self::COMPONENT_NAME),
self::ASSIGNSUBMISSION_DTA_FILEAREA_TEST => get_string("dta_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_DTA_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 DtaResult {
public $packageName;
public $className;
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 DtaResultSummary {
public $timestamp;
public $successfulTestCompetencyProfile;
public $overallTestCompetencyProfile;
public $globalStacktrace;
public $results;
/**
* @param string $jsonString jsonString containing DtaResultSummary
* @return DtaResultSummary
*/
public static function decodeJson($jsonString): DtaResultSummary {
$response = json_decode($jsonString);
$summary = new DtaResultSummary();
$summary->timestamp = $response->timestamp;
$summary->globalStacktrace = $response->globalStacktrace;
$summary->successfulTestCompetencyProfile = self::decodeJsonCompetencyArray($response->successfulTestCompetencyProfile);
$summary->overallTestCompetencyProfile = self::decodeJsonCompetencyArray($response->overallTestCompetencyProfile);
$summary->results = self::decodeJsonResultArray($response->results);
return $summary;
}
private static function decodeJsonCompetencyArray($jsonArray): array {
$ret = array();
foreach ($jsonArray as $entry) {
$ret[] = $entry;
}
return $ret;
}
/**
* @param array $jsonArray decoded json array of results array
* @return array of DtaResult
*/
private static function decodeJsonResultArray($jsonArray): array {
$ret = array();
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;
}
/**
* @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);
}
}
<?php
/**
* 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
*/
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://dtabackend: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 DtaBackendUtils {
/**
* @return string backend host base url
*/
private static function getBackendBaseUrl(): string {
$backendAddress = get_config(assign_submission_dta::COMPONENT_NAME, "backendHost");
if (empty($backendAddress)) {
\core\notification::error(get_string("backendHost_not_set", assign_submission_dta::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_dta::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_dta::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_dta::COMPONENT_NAME));
return null;
} else {
\core\notification::error(get_string("http_unknown_error_msg", assign_submission_dta::COMPONENT_NAME) . $info["http_code"] . $response);
return null;
}
}
}
<?php
class DbUtils {
// summary database table name
private const TABLE_SUMMARY = "assignsubmission_dta_summary";
// result database table name
private const TABLE_RESULT = "assignsubmission_dta_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
): DtaResultSummary {
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 DtaResultSummary();
$summary->timestamp = $summaryDbRecord->timestamp;
$summary->globalStacktrace = $summaryDbRecord->global_stacktrace;
$summary->successfulTestCompetencyProfile = $summaryDbRecord->successful_competencies;
$summary->overallTestCompetencyProfile = $summaryDbRecord->tested_competencies;
$summary->results = array();
// create result instances and add to array of summary instance
foreach($resultsDbArray as $rr) {
$result = new DtaResult();
$result->packageName = $rr->package_name;
$result->className = $rr->class_name;
$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,
DtaResultSummary $summary
): void {
global $DB;
// prepare new database entries
$summaryRecord = new stdClass();
$summaryRecord->assignment_id = $assignmentId;
$summaryRecord->submission_id = $submissionId;
$summaryRecord->successful_competencies = $summary->successfulTestCompetencyProfile;
$summaryRecord->tested_competencies = $summary->overallTestCompetencyProfile;
$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->package_name = $r->packageName;
$record->class_name = $r->className;
$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 is 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, "dtaSubmissionSummary");
}
/**
* 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" => "dtaTableHeaderRow");
$tableRowAttributes = array("class" => "dtaTableRow");
$resultRowAttributes = $tableRowAttributes;
$unknownAttrib = 'dtaResultUnknown';
$successAttrib = 'dtaResultSuccess';
$failureAttrib = 'dtaResultFailure';
$compErrorAttrib = 'dtaResultCompilationError';
// summary table
$tmp = "";
$tmp .= html_writer::tag("th", "Summary", array("class" => "dtaTableHeader"));
$tmp .= html_writer::empty_tag("th", array("class" => "dtaTableHeader"));
$header = html_writer::tag("tr", $tmp, $tableHeaderRowAttributes);
$header = html_writer::tag("thead", $header);
$body = "";
$tmp = "";
$attributes = array("class" => "dtaTableData");
$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" => "dtaTable"));
$html .= $table;
// add empty div for spacing between summary and details table
$html .= html_writer::empty_tag("div", array("class" => "dtaSpacer"));
// details table
$tmp = "";
$tmp .= html_writer::tag("th", "Details", array("class" => "dtaTableHeader"));
$tmp .= html_writer::empty_tag("th", array("class" => "dtaTableHeader"));
$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'] . ' dtaResultUnknown';
} else if ($r->state == 1) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . ' dtaResultSuccess';
} else if ($r->state == 2) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . ' dtaResultFailure';
} else if ($r->state == 3) {
$resultRowAttributes['class'] = $resultRowAttributes['class'] . ' dtaResultCompilationError';
}
$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",
DtaResult::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" => "dtaStacktraceDetails")),
$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" => "dtaTableSpacer"));
}
}
$html .= html_writer::tag("table", $header . $body, array("class" => "dtaTable"));
// wrap generated html into final div
$html = html_writer::div($html, "dtaSubmissionDetails");
return $html;
}
}
<?php
/**
* 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
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2000000;
$plugin->requires = 2019111800; // Moodle 3.8.
$plugin->component = 'assignsubmission_dta';
$plugin->maturity = MATURITY_STABLE;
$plugin->release = "2.0.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