. /** * This file contains the definition for the library class for modocot submission plugin * * This class provides all the functionality for the new assign module. * * @package assignsubmission_modocot * @copyright 2020 hft ip2 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); // File area for modocot submission assignment. define('ASSIGNSUBMISSION_modocot_FILEAREA_SUBMISSION', 'submissions_modocot'); // File area for modocot tests to be uploaded by the teacher. define('ASSIGNSUBMISSION_modocot_FILEAREA_TEST', 'tests_modocot'); /** * library class for modocot submission plugin extending submission plugin base class * * @package assignsubmission_modocot * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assign_submission_modocot extends assign_submission_plugin { /** * Table with assignment/submission id */ const TABLE_ASSIGNSUBMISSION_MODOCOT = "assignsubmission_modocot"; /** * Table with results from tests */ const TABLE_MODOCOT_TESTRESULT = "modocot_testresult"; /** * Table with failed tests */ const TABLE_MODOCOT_TESTFAILURE = "modocot_testfailure"; /** * Table with compilationerrors */ const TABLE_MODOCOT_COMPILATIONERROR = "modocot_compilationerror"; /** * Links to lang/en file */ const COMPONENT_NAME = "assignsubmission_modocot"; /** * Get the name of the modocot submission plugin * @return string */ public function get_name() { return get_string("modocot", self::COMPONENT_NAME); } /** * Get modocot submission information from the database * * @param int $submissionid * @return mixed */ private function get_modocot_submission($submissionid) { global $DB; return $DB->get_record(self::TABLE_ASSIGNSUBMISSION_MODOCOT, array('submission_id' => $submissionid)); } /** * Get the default setting for modocot submission plugin * * @param MoodleQuickForm $mform The form to add elements to * @return void */ public function get_settings(MoodleQuickForm $mform) { $name = get_string("setting_unittests", self::COMPONENT_NAME); $fileoptions = $this->get_file_options(); $mform->addElement("filemanager", "modocottests", $name, null, $fileoptions); $mform->addHelpButton("modocottests", "setting_unittests", "assignsubmission_modocot"); $mform->disabledIf('modocottests', 'assignsubmission_modocot_enabled', 'notchecked'); } /** * Save the settings for modocot submission plugin * * @param stdClass $data * @return bool */ public function save_settings(stdClass $data) { if (isset($data->modocottests)) { file_save_draft_area_files($data->modocottests, $this->assignment->get_context()->id, self::COMPONENT_NAME, ASSIGNSUBMISSION_modocot_FILEAREA_TEST, 0); // TODO Only send file to backend if checkbox in settings is checked. $fs = get_file_storage(); $files = $fs->get_area_files($this->assignment->get_context()->id, self::COMPONENT_NAME, ASSIGNSUBMISSION_modocot_FILEAREA_TEST, 0, 'id', false); if (empty($files)) { \core\notification::warning(get_string("no_testfile_warning", self::COMPONENT_NAME)); return true; } $wsbaseaddress = get_config(self::COMPONENT_NAME, "wsbase"); if (empty($wsbaseaddress)) { \core\notification::error(get_string("wsbase_not_set", self::COMPONENT_NAME)); return true; } $file = reset($files); $url = $wsbaseaddress . "/v1/unittest"; $this->modocot_post_file($file, $url, "unitTestFile"); } return true; } /** * 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) { $draftitemid = file_get_submitted_draft_itemid('modocottests'); file_prepare_draft_area($draftitemid, $this->assignment->get_context()->id, self::COMPONENT_NAME, ASSIGNSUBMISSION_modocot_FILEAREA_TEST, 0, array('subdirs' => 0)); $defaultvalues['modocottests'] = $draftitemid; return; } /** * File format options * * @see https://docs.moodle.org/dev/Using_the_File_API_in_Moodle_forms#filemanager * * @return array */ private function get_file_options() { $fileoptions = array('subdirs' => 1, "maxfiles" => 1, 'accepted_types' => array(".txt",".zip"), 'return_types' => FILE_INTERNAL); return $fileoptions; } /** * 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) { $fileoptions = $this->get_file_options(); $submissionid = $submissionorgrade ? $submissionorgrade->id : 0; $data = file_prepare_standard_filemanager($data, 'tasks', $fileoptions, $this->assignment->get_context(), self::COMPONENT_NAME, ASSIGNSUBMISSION_modocot_FILEAREA_SUBMISSION, $submissionid); $name = get_string("modocot_submission", self::COMPONENT_NAME); $mform->addElement('filemanager', 'tasks_filemanager', $name, null, $fileoptions); $mform->addHelpButton("tasks_filemanager", "modocot_submission", self::COMPONENT_NAME); return true; } /** * Save data to the database * * @param stdClass $submission * @param stdClass $data * @return bool */ public function save(stdClass $submission, stdClass $data) { global $DB; $fileoptions = $this->get_file_options(); $data = file_postupdate_standard_filemanager($data, 'tasks', $fileoptions, $this->assignment->get_context(), self::COMPONENT_NAME, ASSIGNSUBMISSION_modocot_FILEAREA_SUBMISSION, $submission->id); $fs = get_file_storage(); if ($this->is_empty($submission)) { return true; } $files = $fs->get_area_files($this->assignment->get_context()->id, self::COMPONENT_NAME, ASSIGNSUBMISSION_modocot_FILEAREA_SUBMISSION, $submission->id, 'id', false); $modocotsubmission = $this->get_modocot_submission($submission->id); if ($modocotsubmission) { // If there are old results, delete them. $this->delete_test_data($modocotsubmission->id); } else { $modocotsubmission = new stdClass(); $modocotsubmission->submission_id = $submission->id; $modocotsubmission->assignment_id = $this->assignment->get_instance()->id; $modocotsubmission->id = $DB->insert_record(self::TABLE_ASSIGNSUBMISSION_MODOCOT, $modocotsubmission); } $wsbaseaddress = get_config(self::COMPONENT_NAME, "wsbase"); if (empty($wsbaseaddress)) { \core\notification::error(get_string("wsbase_not_set", self::COMPONENT_NAME)); return true; } // Get the file and post it to our backend. $file = reset($files); $url = $wsbaseaddress . "/v1/task"; $response = $this->modocot_post_file($file, $url, "taskFile"); if (empty($response)) { return true; } $results = json_decode($response); $testresults = $results->testResults; foreach ($testresults as $tr) { // Test result. $testresult = new stdClass(); $testresult->testname = $tr->testName; $testresult->testcount = $tr->testCount; $testresult->succtests = implode(",", $tr->successfulTests); $testresult->modocot_id = $modocotsubmission->id; $testresult->id = $DB->insert_record(self::TABLE_MODOCOT_TESTRESULT, $testresult); // Test failure. $testfailures = $tr->testFailures; foreach ($testfailures as $tf) { $testfailure = new stdClass(); $testfailure->testheader = $tf->testHeader; $testfailure->message = $tf->message; $testfailure->trace = $tf->trace; $testfailure->testresult_id = $testresult->id; $testfailure->id = $DB->insert_record(self::TABLE_MODOCOT_TESTFAILURE, $testfailure); } } $compilationerrors = $results->compilationErrors; foreach ($compilationerrors as $ce) { // Compilation error. $compilationerror = new stdClass(); $compilationerror->columnnumber = $ce->columnNumber; $compilationerror->linenumber = $ce->lineNumber; $compilationerror->message = $ce->message; $compilationerror->position = $ce->position; $compilationerror->filename = $ce->javaFileName; $compilationerror->modocot_id = $modocotsubmission->id; $compilationerror->id = $DB->insert_record(self::TABLE_MODOCOT_COMPILATIONERROR, $compilationerror); } return true; } /** * 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, ASSIGNSUBMISSION_modocot_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; } /** * Posts the file to the url under the given param name. * * @param stored_file $file the file to post. * @param string $url the url to post to. * @param string $paramname the param name for the file. * @return mixed */ private function modocot_post_file($file, $url, $paramname) { if (!isset($file) or !isset($url) or !isset($paramname)) { return false; } $params = array( $paramname => $file, "assignmentId" => $this->assignment->get_instance()->id ); $options = array( "CURLOPT_RETURNTRANSFER" => true ); $curl = new curl(); $response = $curl->post($url, $params, $options); $info = $curl->get_info(); if ($info["http_code"] == 200) { return $response; } // Something went wrong. debugging("modocot: Post file to server was not successful: http_code=" . $info["http_code"]); if ($info['http_code'] == 400) { \core\notification::error(get_string("badrequesterror", self::COMPONENT_NAME)); return false; } else { \core\notification::error(get_string("unexpectederror", self::COMPONENT_NAME)); return false; } } /** * Display the test results of the submission. * * @param stdClass $submission * @param bool $showviewlink Set this to true if the list of files is long * @return string */ public function view_summary(stdClass $submission, & $showviewlink) { global $PAGE; if ($PAGE->url->get_param("action") == "grading") { return $this->view_grading_summary($submission, $showviewlink); } else { return $this->view_student_summary($submission); } } /** * Returns the view that should be displayed in the grading table. * * @param stdClass $submission * @param bool $showviewlink * @return string */ private function view_grading_summary(stdClass $submission, & $showviewlink) { global $DB; $showviewlink = true; $modocotsubmission = $DB->get_record(self::TABLE_ASSIGNSUBMISSION_MODOCOT, array("submission_id" => $submission->id)); $testresults = $DB->get_records(self::TABLE_MODOCOT_TESTRESULT, array("modocot_id" => $modocotsubmission->id)); $comperrorcount = $DB->count_records(self::TABLE_MODOCOT_COMPILATIONERROR, array("modocot_id" => $modocotsubmission->id)); $result = $this->get_short_testresult_overview($testresults, $comperrorcount); $result = html_writer::div($result, "submissionmodocotgrading"); return $result; } /** * Returns testresult overview * * @param int $testresults * @param int $comperrorcount * @return string */ private function get_short_testresult_overview($testresults, $comperrorcount) { $testcount = 0; $succcount = 0; foreach ($testresults as $tr) { $testcount += $tr->testcount; $succcount += count($this->split_string(",", $tr->succtests)); } $result = ""; if ($comperrorcount > 0) { $result .= "Comp. Err.: " . $comperrorcount; $result .= "
"; } $result .= "Tests: " . $succcount . "/" . $testcount; if ($testcount > 0) { $percentage = round($succcount / $testcount, 1) * 100; $result .= " (" . $percentage . "%)"; } return $result; } /** * Splits a string by string. * * Behave exactly like {@link explode} apart from returning an * empty array in case string is empty. * * @param string $delimiter the boundary string. * @param string $string the input string. * @return array */ private function split_string($delimiter, $string) { if (empty($string)) { return array(); } else { return explode($delimiter, $string); } } /** * Returns the view that should be displayed to the student. * * @param stdClass $submission * @return string */ private function view_student_summary(stdClass $submission) { return $this->view($submission); } /** * Shows the test results of the submission. * * @param stdClass $submission the submission the results are shown for. * @return string the view of the test results as html. */ public function view(stdClass $submission) { global $DB; $html = ""; $html .= $this->assignment->render_area_files(self::COMPONENT_NAME, ASSIGNSUBMISSION_modocot_FILEAREA_SUBMISSION, $submission->id); $modocotsubmission = $DB->get_record(self::TABLE_ASSIGNSUBMISSION_MODOCOT, array("submission_id" => $submission->id)); $testresults = $DB->get_records(self::TABLE_MODOCOT_TESTRESULT, array("modocot_id" => $modocotsubmission->id)); $compilationerrors = $DB->get_records(self::TABLE_MODOCOT_COMPILATIONERROR, array("modocot_id" => $modocotsubmission->id)); $html .= html_writer::tag("h5", "Overall results"); $html .= $this->get_short_testresult_overview($testresults, count($compilationerrors)); foreach ($testresults as $tr) { $testname = html_writer::tag("h5", $tr->testname); $html .= html_writer::div($testname); if ($tr->succtests) { $html .= html_writer::tag("h6", "Successful Tests"); $html .= html_writer::alist(explode(",", $tr->succtests)); } $testfailures = $DB->get_records(self::TABLE_MODOCOT_TESTFAILURE, array("testresult_id" => $tr->id)); if ($testfailures) { $html .= html_writer::tag("h6", "Failed Tests"); foreach ($testfailures as $tf) { $tmpdiv = html_writer::div("Testheader:", "failedtestsidebar"); $tmpdiv .= html_writer::div($tf->testheader, "failedtestcontent"); $html .= html_writer::div($tmpdiv, "failedTestWrapper"); $tmpdiv = html_writer::div("Message:", "failedtestsidebar"); $tmpdiv .= html_writer::div($tf->message, "failedtestcontent"); $html .= html_writer::div($tmpdiv, "failedTestWrapper"); $tmpdiv = html_writer::div("Trace:", "failedtestsidebar"); if ($tf->trace) { $tmpdiv .= html_writer::start_div("failedtestcontent"); $checkbid = html_writer::random_id(); $tmpdiv .= html_writer::label("show trace", $checkbid, false, array("class" => "collapsible")); $tmpdiv .= html_writer::checkbox(null, null, false, null, array("id" => $checkbid)); $tmpdiv .= html_writer::div($tf->trace); $tmpdiv .= html_writer::end_div(); } else { $tmpdiv .= html_writer::div("no trace", "failedtestcontent"); } $html .= html_writer::div($tmpdiv, "failedTestWrapper"); } } $html = html_writer::div($html); } if ($compilationerrors) { $html .= html_writer::tag("h6", "Compilation errors"); foreach ($compilationerrors as $ce) { $tmpdiv = html_writer::div("Filename:", "failedtestsidebar"); $tmpdiv .= html_writer::div($ce->filename, "failedtestcontent"); $html .= html_writer::div($tmpdiv, "failedTestWrapper"); $tmpdiv = html_writer::div("Message:", "failedtestsidebar"); $tmpdiv .= html_writer::div($ce->message, "failedtestcontent"); $html .= html_writer::div($tmpdiv, "failedTestWrapper"); $tmpdiv = html_writer::div("Column-No.:", "failedtestsidebar"); $tmpdiv .= html_writer::div($ce->columnnumber, "failedtestcontent"); $html .= html_writer::div($tmpdiv, "failedTestWrapper"); $tmpdiv = html_writer::div("Line-No.:", "failedtestsidebar"); $tmpdiv .= html_writer::div($ce->linenumber, "failedtestcontent"); $html .= html_writer::div($tmpdiv, "failedTestWrapper"); $tmpdiv = html_writer::div("Position:", "failedtestsidebar"); $tmpdiv .= html_writer::div($ce->position, "failedtestcontent"); $html .= html_writer::div($tmpdiv, "failedTestWrapper"); } } $html = html_writer::div($html, "modocot_submission_view"); return $html; } /** * The assignment has been deleted - cleanup * * @return bool */ public function delete_instance() { global $DB; $assignmentid = $this->assignment->get_instance()->id; $modocot = $DB->get_record(self::TABLE_ASSIGNSUBMISSION_MODOCOT, array('assignment_id' => $assignmentid), "id"); if ($modocot) { $this->delete_test_data($modocot->id); } // Delete modocot assignment. $DB->delete_records(self::TABLE_ASSIGNSUBMISSION_MODOCOT, array("assignment_id" => $assignmentid)); $wsbaseaddress = get_config(self::COMPONENT_NAME, "wsbase"); if (empty($wsbaseaddress)) { \core\notification::error(get_string("wsbase_not_set", self::COMPONENT_NAME)); return true; } $url = $wsbaseaddress . "/v1/unittest?assignmentId=" . $assignmentid; $curl = new curl(); $curl->delete($url); return true; } /** * The assignment has been deleted - cleanup * * @param int $modocotid * @return bool */ private function delete_test_data($modocotid) { global $DB; $testresult = $DB->get_record(self::TABLE_MODOCOT_TESTRESULT, array("modocot_id" => $modocotid), "id", IGNORE_MISSING); if (!$testresult) { return true; } // Delete compilation errors. $DB->delete_records(self::TABLE_MODOCOT_COMPILATIONERROR, array("modocot_id" => $modocotid)); // Delete test failures. $DB->delete_records(self::TABLE_MODOCOT_TESTFAILURE, array("testresult_id" => $testresult->id)); // Delete test results. $DB->delete_records(self::TABLE_MODOCOT_TESTRESULT, array("modocot_id" => $modocotid)); return true; } /** * Return true if there are no submission files * * @param stdClass $submission * @return bool */ public function is_empty(stdClass $submission) { return $this->count_files($submission->id, ASSIGNSUBMISSION_modocot_FILEAREA_SUBMISSION) == 0; } /** * Count the number of files * * @param int $submissionid * @param string $area * @return int */ private function count_files($submissionid, $area) { $fs = get_file_storage(); $files = $fs->get_area_files($this->assignment->get_context()->id, self::COMPONENT_NAME, $area, $submissionid, 'id', false); return count($files); } /** * 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( ASSIGNSUBMISSION_modocot_FILEAREA_SUBMISSION => get_string("modocot_submissions_fa", self::COMPONENT_NAME), ASSIGNSUBMISSION_modocot_FILEAREA_TEST => get_string("modocot_tests_fa", self::COMPONENT_NAME) ); } }