[moodle] / contrib / plugins / question / type / sojunit / questiontype.php Repository:

View of /contrib/plugins/question/type/sojunit/questiontype.php

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.2 - (download) (annotate)
Wed Feb 4 05:10:01 2009 WST (9 months, 2 weeks ago) by suerec
Branch: MAIN
CVS Tags: HEAD
Changes since 1.1: +7 -4 lines
tested final version with screenshots in example_files folder
<?php
/**
 * The question type class for the SourceCode JUnit question type.
 *
 * @copyright &copy; 2008 Süreç Özcan
 * @author suerec@darkjade.net
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
 * @package so_questiontypes
 */

require_once(dirname(__FILE__) . '/config.php');

/**
 * The SourceCode JUnit question class
 *
 * This question type is to be used in adaptive-mode. 
 * Therefore the print_question_grading_details-method has been
 * overridden so that gradings won't get displayed when using
 * "Submit"-button.
 */
class sojunit_qtype extends default_questiontype {

	var $operating_system = PHP_OS; //Windows: "WINNT" or Linux: "Linux"
	
    function name() {
        return 'sojunit';
    }

    /**
     * @return boolean to indicate success of failure.
     */
    function get_question_options(&$question) {
        // code to retrieve the extra data you stored in the database into $question->options.
        // Get additional information from database and attach it to the question object
        if (!$question->options = get_record('question_sojunit', 'questionid', $question->id)) {
            notify('Error: Missing question options for junit question '. $question->id. '!');
            return false;
        }
        return true;
    }

    /**
     * Save the units and the answers associated with this question.
     * @return boolean to indicate success of failure.
     */
    function save_question_options($question) {
        // code to save the extra data to your database tables from the
        // $question object, which has all the post data from editquestion.html

        // I don't need the answer-table -> so I just don't fill it up with inputs
        // question_sojunit - table
        $update = true;
        $options = get_record("question_sojunit", "questionid", $question->id);
        if (!$options) {
            $update = false;
            $options = new stdClass;
            $options->questionid = $question->id;
        }

        // new fields for this questin type as new options
        $options->givencode = trim($question->givencode);
        $options->testclassname = trim($question->testclassname);
		//$options->useeditor_genfeedb = trim($question->useeditor_genfeedb);  //Tim Hunt
		$options->useeditor_genfeedb = !empty($question->useeditor_genfeedb); //Tim Hunt
		$options->sourcecode = $question->sourcecode;

        if ($update) {
            if (!update_record("question_sojunit", $options)) {
                $result->error = "Could not update quiz sojunit-table options! (id=$options->id)";
                return $result;
            }
        } else {
            if (!insert_record("question_sojunit", $options)) {
                $result->error = "Could not insert quiz sojunit-table options!";
                return $result;
            }
        }
        return true;
    }

    /**
     * Deletes question from the question-type specific tables
     *
     * @param integer $questionid The question being deleted
     * @return boolean to indicate success of failure.
     */
    function delete_question($questionid) {
        delete_records("question_sojunit", "questionid", $questionid);
        return true;
    }


    /**
     * unpack $state->responses[''], which has just been loaded from the
     * database field question_states.answer into the $state->responses array.
     * 
     * We store the reponses by turning the associative array $state->responses
     * into a string as follows. For example, array('f2' => 'No, never - ever', 'f1' => '10')
     * becomes 'f1-10,f2-No\, never - ever'. That is, comma separated pairs, sorted by key,
     * key and value linked with a '-', commas in vales escaped with '\'.
     */
    function restore_session_and_responses(&$question, &$state) {
        // Deal with special case: no responses at all.
        if (empty($state->responses) || empty($state->responses[''])) {
            $state->responses == array();
            return true;
        }

        // Split the responses on non-backslash-escaped commas.
        // JUnit: chanded '\,' into '#,' because MySQL handles '\,' special so that commas won't get seperated correctly
        //$responses = preg_split('/(?<!\\\\)\,/', $state->responses['']);
        $responses = preg_split('/(?<!#),/', $state->responses['']);

        // Now set $state->responses properly.
        $state->responses = array();
		foreach ($responses as $response) {
            list($key, $value) = explode('-', $response, 2);
            $state->responses[$key] = str_replace('#,', ',', $value);
        }
        return true;
    }

    /**
     * See commment at the top of restore_session_and_responses for what we are trying to do.
     * package up the students response from the $state->responses
     * array into a string and save it in the question_states.answer field.
     */
    function save_session_and_responses(&$question, &$state) {
        $responses = array();
        ksort($state->responses);
        foreach ($state->responses as $key => $value) {
            $responses[] = $key . '-' . str_replace(',', '#,', $value);
        }
        $responses = implode(',', $responses);
        return set_field('question_states', 'answer', $responses, 'id', $state->id);
    }
    
    /**
     * in order to use syntax highlighted source code, add corresponding code-tags around the $code
     */
    function format_code($code, $cmoptions) {
        return $this->format_text('[code syntax="java" linenumbers="yes" indentsize="2"]' . $code . '[/code]',
                FORMAT_MOODLE, $cmoptions);
    }

    
    function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
        global $CFG, $COURSE;

        // Print the questiontext
        $questiontext = $this->format_text($question->questiontext, $question->questiontextformat, $cmoptions);

        $image = get_question_image($question, $cmoptions->course);

        // Prepare the students response.
        if (!empty($state->responses[''])) {
            $studentscode = stripslashes_safe($state->responses['']);
        } else {
            $studentscode = $question->options->givencode;
        }

        $inputname = $question->name_prefix;

        $course_files_directory_backslashes = make_upload_directory("$COURSE->id"); // path for course files
        $course_files_directory = str_replace("\\","/",$course_files_directory_backslashes); //replace \\ with /
        $testcode = file_get_contents($course_files_directory . '/' . $question->options->testclassname);
        
        // Question text, image and given code.
        echo '<div class="qtext">', $questiontext, '</div>';

		 if ($image) {
            echo '<img class="qimage" src="', $image, '" alt="" />';
        }

        // Start of students response, feedback, compiler output, execution output etc.
        echo '<div class="ablock clearfix" spellcheck="false">';

            // Response of student
            if (empty($options->readonly)) { //if not readonly - editable
                $answer = print_textarea(false, 30, 100, 630, 400, $inputname, $studentscode, $cmoptions->course, true);
                echo $answer;
            } else { //if readonly - not editable
                echo '<div class="studentscode">', $this->format_code($studentscode, $cmoptions) . '</div>';
            }
            
            // display compiler output allways
            //Note bug: in Moodle 1.8.2+ the compilation output doesn't get displayed after usage of "Submit all and finish"-button?
            if (!empty($state->responses['_compileroutput'])) { //display compilation output when compilation error occured!
               echo '<div class="feedback">';
                    echo '<p>', get_string('compileroutput','qtype_sojunit'), '</p>';
					if (empty($options->readonly)) { //display the studentresponse nicely syntax highlighted so that they can find their comp/exec - error easier
						echo '<div class="studentscode">', $this->format_code($studentscode, $cmoptions) . '</div>';
					}
                    //echo '<pre class="compileroutput">', "some text" . s($state->responses['_compileroutput'], true), '</pre>';
					echo '<pre class="compileroutput">', s($state->responses['_compileroutput'], true), '</pre>';
               echo '</div>';
            } else if((isset($_POST['resp540_submit'])) and (empty($options->readonly))){ 
                //TODO if not readonly and Submit-button pressed -> display compilation success; if readonly -> bug issue
                echo '<div class="feedback">';
                    echo '<pre class="compileroutput">', get_string('compileroutputsuccess','qtype_sojunit'), '</pre>';
               echo '</div>';
            }
            
            // Display any feedback - that is any compiler output or execution output.
            if (($options->feedback) or (!empty($options->readonly))) {
                if (!empty($state->responses['_executionoutput'])) {
                    echo '<div class="feedback">';
                        echo '<p>', get_string('executionoutput','qtype_sojunit'), '</p>';
                        echo '<pre class="executionoutput">', s($state->responses['_executionoutput'], true), '</pre>';
                    echo '</div>';
                }
            }
            
            $this->print_question_submit_buttons($question, $state, $cmoptions, $options);

        // End of <div class="ablock clearfix">
        echo '</div>';
    }

    /**
     * Here happens everything, starting from compiling and executing the proper files
     * till calculating the grade.
     * 
     * see forum entry: http://moodle.org/mod/forum/discuss.php?d=97086
     * $state->responses['compileroutput']=...; // store compileroutput here
     * $state->responses['_executionoutput']=...; // store executionoutput here
     * 
     * $state->raw_grade   //the final grade of the student should be stored in here
     * $state->responses   //should hold the response of a student
     * $question->maxgrade //max. points for this question
     *                     //should hold the grade number the teacher typed in the formular while creating the new question
     *
     * @param $question
     * @param $state
     * @param $cmoptions
     * @return unknown
     */
    function grade_responses(&$question, &$state, $cmoptions) {
        global $CFG;
        global $COURSE;

        $state->raw_grade = 0;

        /* preparation:
         * create a new sub-folder in the course-files-path for each user who takes this question type.
         * Put the related source codes (Test.java and studentid_response.java) of a user into this sub-folder
         */
        //locate the path for the used testclass, and create it.
		$pre = 'a' . $state->attempt . '_' . $state->question . '_'; // the pre-string for unique file naming
        $cfg_dataroot_backslashes = $CFG->dataroot;
        $cfg_dataroot = str_replace("\\", "/", $cfg_dataroot_backslashes);
        //$temp_folder = $cfg_dataroot . '/so_junit_temp/' . $state->attempt . '/' . $state->question;
		$temp_folder = $cfg_dataroot . '/so_junit_temp'; // $state->attempt . '/' . $state->question;
        $this->mkdir_recursive($temp_folder);
		//print_object('temp_folder: ' . $temp_folder); //debug

        // Save the test code to a .java file.
		$testclassname = $question->options->testclassname; //loaded and used testclass by teacher for this question
		//print_object('testclassname: ' . $testclassname);
		
        //locate the path for the used testclass which has been uploaded
        //keep replacing "\" with "/" which should work on linux and windows for compilation and execution
        $course_files_directory_backslashes = make_upload_directory("$COURSE->id"); // path for course files
        $course_files_directory = str_replace("\\","/",$course_files_directory_backslashes); //replace \\ with / NEW
        $testclass_path = $course_files_directory.'/'.$testclassname;
		//print_object('testclass_path: ' . $testclass_path); //debug
        
        //copy the test-file into the temporary-folder directory ($temp_file)
        //e.g. "D:/oezcan/workspace/moodleSue/moodledata/temp/sojunit/85/500/FactorialTest.java"
        $cfg_dataroot_backslashes = $CFG->dataroot;
        $cfg_dataroot = str_replace("\\", "/", $cfg_dataroot_backslashes);
        
        // usually the test-file is directly in the course_files_directory
        if ($pos_of_slash = strpos($testclassname, "/")){ // if test-file is in a subfolder of the course path e.g. "suerec/Test.java"
			$testclassname_without_pre = substr($testclassname,$pos_of_slash + 1); //needed for compilation and execution so that the calls work properly
			$testclassname_without_pre_and_java = substr($testclassname_without_pre, 0, -5);
            $testclassname = $pre . substr($testclassname,$pos_of_slash + 1); // only the string after e.g. "suerec"
        } else {
			$testclassname_without_pre_and_java = substr($testclassname, 0, -5); //needed for compilation and execution so that the calls work properly
			$testclassname = $pre . $testclassname;
		}
		//print_object('testclassname_without_pre: ' . $testclassname_without_pre);
		//print_object('testclassname: ' . $testclassname);
        $temp_file = $temp_folder.'/'.$testclassname;
		//print_object('temp_file: ' . $temp_file);
        if (!copy($testclass_path,$temp_file)){
            echo "Case1: Failed to copy test file \"$testclass_path\" into a temporary userid-file \"$temp_file\".";
        }
		//print_object('testclassname: ' . $testclassname); //debug
        
        // Get the student's code.
        // Deal with special case: no responses at all.
        if (empty($state->responses)) {  //Tim Hunt: changed the next 10 lines
             $state->responses == array();
        }
		
        // Prepare the students response.
        if (!empty($state->responses[''])) {
            $studentscode = stripslashes($state->responses['']);
        } else {
            $studentscode = $question->options->givencode;
        }
		
        // Try to find the class name they used.
        $matches = array();
		// ignore text that is stated above the class X e.g. imports, comments
        preg_match('/^(?:\s*public)?\s*class\s+(\w[a-zA-Z0-9_]+)/m', $studentscode, $matches);
		if (empty( $matches[1])){ //if student deletes accidently the class name return error-message
			preg_match('/^(?:\s*public)?\s*class\s+(\w[a-zA-Z0-9_]+)/', $question->options->givencode, $matches);
			$studentsclassname = $matches[1];
			//filter the class name from givencode
            $brace_pos = strpos($question->options->givencode, '{'); //get the pos in the string where 'Time' starts
            $class_given = substr($question->options->givencode, 0, $brace_pos);
			$state->responses['_compileroutput'] = get_string('error_class', 'qtype_sojunit') . $class_given;
			return true;
		} else {
			$studentsclassname = $matches[1];
		}
        
        // Write their code to a file.
        file_put_contents($temp_folder . '/' . $pre . $studentsclassname . '.java', $studentscode);
		//print_object('studentsclassname: ' . $studentsclassname);
        
        // Compile it.
		$studentclass_path = $temp_folder. '/' . $pre . $studentsclassname . '.java';
		//print_object('studentclass_path' . $studentclass_path);
        list($compiles, $compileroutput) = $this->compile($temp_file, $studentclass_path, $temp_folder, $pre, $testclassname_without_pre_and_java);
		
		//shorten the $compileroutput by the $temp_folder part in order to
		// 1. mainly get same compiler-errors in the Results->Item analysis grouped together for a better analysis
		// 2. hide the compilation-path from the student (it is not important to do so because of the SecurityManager used, but this would be nice as well)
		$compileroutput_without_path = substr_replace($compileroutput, '', 0, strlen($temp_folder)+1);
		//remove the pre-string from the class-name so that the student won't get confused why it is named different
		$compileroutput_without_pre = str_replace($pre, '', $compileroutput_without_path);
		$state->responses['_compileroutput'] = addslashes($compileroutput_without_pre);

        if ($compiles) {
            // Run the tests.
            $testclassname_without_java = substr($testclassname,0,-5);
            $executionoutput = $this->execute($testclassname_without_java, $temp_folder, $pre);
			//filter the $executionoutput by 'Time 0.000' part in order to get same
			//execution-outputs in the Results->Item analysis grouped together for a better analysis
			$cleaned_executionoutput = preg_replace("/Time:\s([0-9]+[.][0-9]+)\s/","",$executionoutput);
			//remove the pre-string from the class-name so that the student won't get confused why it is named different
			$executionoutput_without_pre = str_replace($pre, '', $cleaned_executionoutput);
			$state->responses['_executionoutput'] = addslashes($executionoutput_without_pre);

            //the JUnit-execution-output returns always a String in the first line e.g. "...F.",
            //which means that 1 out of 4 test cases didn't pass the JUnit test
            //In the second line it says "Time ..."

            // Discard the summary.
            $pos = strpos($executionoutput, 'Time') + 1; //get the pos in the string where 'Time' starts
            $executionoutputresult = substr($executionoutput, 0, $pos);

            // Count the failures and errors.
            $numfailures = substr_count($executionoutputresult, '.F');
            $numerrors   = substr_count($executionoutputresult, '.E');

            // Work out the score based on this - we ensure this is not negative below.
            $state->raw_grade = $question->maxgrade - $numfailures - $numerrors;

            // Apply the penalty for this attempt
            $state->penalty = $question->penalty * $question->maxgrade;
        } else {
            // Doesn't compile - zero grade, but don't penalise the student for this submission.
            $state->responses['_executionoutput'] = '';
            $state->raw_grade = 0;
            $state->penalty = 0;
        }

        // Make sure we don't assign negative or too high marks. Note: The grading doesn't work if this line is used!?!
        //$state->raw_grade = min(max((float) $state->raw_grade,0.0), 1.0) * $question->maxgrade;

        // mark the state as graded
        $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
        
        //delete the temporarily created files of a current attempt to its corresponding question (java, log, class)
		$this->rfr($temp_folder,"/*.java");
		$this->rfr($temp_folder,"/*.log");
		$this->rfr($temp_folder,"/*.class");
		
        return true;
    }
	
	
	/**
     * This function deletes all files of the $path-folder that match the ending e.g. '.log'
     * @param string $path 	path to the directory in which the files are to be deleted
     * @param int $match 		files with this ending shall be deleted e.g. '.log'
     */
	function rfr($path,$match){
		static $deld = 0, $dsize = 0;
		$files = glob($path.$match);
		foreach($files as $file){
			if(is_file($file)){
				$dsize += filesize($file);
				unlink($file);
				$deld++;
			}
		}
		return "$deld files deleted with a total size of $dsize bytes";
	}

	
    /**
     * This function exists because mkdir(folder,mode,TRUE) doesn't work on our server.
     * Safe to call even if folder already exists (checks)
     * @param string $folder Folder to create
     * @param int $mode Mode for creation (default 0755)
     * @return boolean True if folder (now) exists, false if there was a failure
     */
    function mkdir_recursive($folder, $mode='') {
        if(is_dir($folder)) {
            return true;
        }
        if ($mode == '') {
            global $CFG;
            $mode = $CFG->directorypermissions;
        }
        if(!$this->mkdir_recursive(dirname($folder),$mode)) {
            return false;
        }
        return mkdir($folder,$mode);
    }
	
	
	/**
     * This function renames all occurances of the $search string with the $replace string for a given file.
     * It will be used to rename the class-name and wherever that string is used in the test- or student-response-file
     * so that it can compile properly. Later we can rename it back to the original strings.
     * Safe to call even if folder already exists (checks)
     * @param string $file		 path to the file in which the renaming shall take place
     * @param string $search	 search for this string
     * @param string $replace	 replace the searched string with this string
     */
	function temp_rename($file, $search, $replace){
		$file_contents = file_get_contents($file);
		$fh = fopen($file, "w");
		$file_contents = str_replace($search, $replace, $file_contents);
		//print_object('testclassname_without_pre_and_java: ' . $search);
		//print_object('testclassname_with_pre_and_java: ' . $replace);
		fwrite($fh, $file_contents);
		fclose($fh);
	}
	
    /**
     * compile the student-response-file and the test-file
     * @param string $temp_file            the complete path of the test-file to be compiled
     * @param string $studentclass_path    the complete path of the student-response-file to be compiled
     * @param string $temp_folder          the complete path to the temp-folder
     * @param string $pre 	          the attempt_questionId of the current attempt of the question compiled 
     * @param string $testclassname_without_pre_and_java	the test-class name without pre and '.java', used to overwrite it temporarily in order to compile properly (needed for proper *.class file naming)
     * @return array $compiles, $compileroutput	$compiles returns true, if the code compiles, else false; $compileroutput is the content of the compiler output
     **/
    function compile($temp_file, $studentclass_path, $temp_folder, $pre, $testclassname_without_pre_and_java) {
        // Work out the compile command line.
        $compileroutputfile = $temp_folder . '/' . $pre . 'compileroutput.log';
		
		//temporarily rename every appearance of $studentsclassname with $pre.$studentsclassname in the .java-files in order to get correct compilation
		$this->temp_rename($temp_file, $testclassname_without_pre_and_java, $pre.$testclassname_without_pre_and_java);
		$this->temp_rename($studentclass_path, $testclassname_without_pre_and_java, $pre.$testclassname_without_pre_and_java);
			
		//the following $command is the same on different operating systems
        $command = PATH_TO_JAVAC . ' -cp ' . PATH_TO_JUNIT . ' ' . $temp_file . ' ' . 
                   $studentclass_path . ' 2> ' . $compileroutputfile;
		//print_object($command);
		
		// Execute it.
        exec($command);
		
		//undo the renaming in the .java-files
		$this->temp_rename($temp_file, $pre.$testclassname_without_pre_and_java, $testclassname_without_pre_and_java);
		$this->temp_rename($studentclass_path, $pre.$testclassname_without_pre_and_java, $testclassname_without_pre_and_java);

        // get the content of the compiler output.
        $compileroutput = file_get_contents($compileroutputfile);
		
		//print_object('compileroutput: '.$compileroutput);	//debug
		
        $compiles = empty($compileroutput);
        return array($compiles, $compileroutput);
    }

    /**
     * execute the file that should have been compiled first
     * @param string $temp_file      complete path to the Junit-test-file to be executed
     * @param string $temp_folder the complete path to the temp-folder
     * @param string $pre 	     the attempt_questionId of the current attempt of the question compiled
     * @return string $executionoutput the content of the junit-execution-output
     **/
    function execute($temp_file, $temp_folder, $pre){
        $executionoutputfile = $temp_folder . '/' . $pre . 'executionoutput.log';

		// first chdir and prepare Security policy so that no 'bad' code can be executed by the student - execute in the next step secure
		if (!chdir($temp_folder)){ // change the directory to the $temp_folder-directory for the policy
			return get_string('chdir', 'qtype_sojunit') . $temp_folder;
		}
		//path for the security policy  for the system for any unsecure entries;
		//to be executed together with the execution file in $command
		$commandWithSecurity = 
			PATH_TO_JAVA . " -Djava.security.manager=default" 		// Run standard security manager
						 . " -Djava.security.policy=". PATH_TO_POLICY . " ";
		
		//in dependence of the operating system use another syntax for the $command
        if ($this->operating_system == "WINNT"){ //Windows
            $command = $commandWithSecurity . ' -cp ' . PATH_TO_JUNIT . ';.' .
                        ' junit.textui.TestRunner ' . $temp_file . ' > ' . $executionoutputfile . ' 2>&1';
        }
        if ($this->operating_system == "Linux"){ //Linux
            $command = $commandWithSecurity . ' -cp ' . PATH_TO_JUNIT . ':. ' . //set junit and $temp_file paths
                        'junit.textui.TestRunner ' . $temp_file . ' > ' . $executionoutputfile . ' 2>&1'; 
                        //put execution error and failure into $executionoutputfile
        }
        
        // Execute it.
        exec($command);        

        // get the content of the execution output.
        $executionoutput = file_get_contents($executionoutputfile); 
		//print_object('executionoutput: '.$executionoutput);	//debug
		return $executionoutput;
    }
    
    /** This method should be overriden if you want to include a special heading or some other
     * html on a question editing page besides the question editing form.
     * 
     * JUnit: This code is to enable the possibility of saving inserted text into the HTML-editor 
     * and switching back to text editor
     *
     * @param question_edit_form $mform a child of question_edit_form
     * @param object $question
     * @param string $wizardnow is '' for first page.
     */
    function display_question_editing_page(&$mform, $question, $wizardnow){
        parent:: display_question_editing_page($mform, $question, $wizardnow);
        $formid = $mform->_form->_attributes['id']; // get form id
        
		echo <<<JS
<script type="text/javascript">
document.forms['$formid'].reloaded.value = 0;

function reload(id) {
	var form = document.forms['$formid'];
	var iframes = form.getElementsByTagName('iframe');
	
	for (var i = 0; i < iframes.length; i++) {
		var ifr = iframes[i];
		var element = ifr.parentNode.nextSibling;
		element.value = ifr.contentWindow.document.body.innerHTML;
	}
	form.reloaded.value='1';
	form.submit();
}
</script>
JS;
    }
    
	
    /** Prints the score obtained and maximum score available plus any penalty
    * information
    *
    * This function prints a summary of the scoring in the most recently
    * graded state (the question may not have been submitted for marking at
    * the current state). The default implementation should be suitable for most
    * question types.
    * 
    * JUnit: Don't print question grading details when pressing the 
    * "Submit"-button in adaptive mode. Only print those details after 
    * the use of "Submit all and finish"-button.
    * 
    * @param object $question The question for which the grading details are
    *                         to be rendered. Question type specific information
    *                         is included. The maximum possible grade is in
    *                         ->maxgrade.
    * @param object $state    The state. In particular the grading information
    *                          is in ->grade, ->raw_grade and ->penalty.
    * @param object $cmoptions
    * @param object $options  An object describing the rendering options.
    */
    function print_question_grading_details(&$question, &$state, $cmoptions, $options) {
        /* The default implementation prints the number of marks if no attempt
        has been made. Otherwise it displays the grade obtained out of the
        maximum grade available and a warning if a penalty was applied for the
        attempt and displays the overall grade obtained counting all previous
        responses (and penalties) */

        if (QUESTION_EVENTDUPLICATE == $state->event) {
            echo ' ';
            print_string('duplicateresponse', 'quiz');
        }
        if (!empty($question->maxgrade) && $options->scores) {
			if (question_state_is_closed($state->last_graded) ){	// Junit: Do the following only if "Submit all and finish"-button ('finishattempt') is pressed
                // Display the grading details from the last graded state
                $grade = new stdClass;
                $grade->cur = round($state->last_graded->grade, $cmoptions->decimalpoints);
                $grade->max = $question->maxgrade;
                $grade->raw = round($state->last_graded->raw_grade, $cmoptions->decimalpoints);

                // let student know wether the answer was correct
                echo '<div class="correctness ';
                if ($state->last_graded->raw_grade >= $question->maxgrade/1.01) { // We divide by 1.01 so that rounding errors dont matter.
                    echo ' correct">';
                    print_string('correct', 'quiz');
                } else if ($state->last_graded->raw_grade > 0) {
                    echo ' partiallycorrect">';
                    print_string('partiallycorrect', 'quiz');
                } else {
                    echo ' incorrect">';
                    print_string('incorrect', 'quiz');
                }
                echo '</div>';

                echo '<div class="gradingdetails">';
                // print grade for this submission
                print_string('gradingdetails', 'quiz', $grade);
                if ($cmoptions->penaltyscheme) {
                    // print details of grade adjustment due to penalties
                    if ($state->last_graded->raw_grade > $state->last_graded->grade){
                        echo ' ';
                        print_string('gradingdetailsadjustment', 'quiz', $grade);
                    }
                    // print info about new penalty
                    // penalty is relevant only if the answer is not correct and further attempts are possible
                    if (($state->last_graded->raw_grade < $question->maxgrade / 1.01)
                                and (QUESTION_EVENTCLOSEANDGRADE !== $state->event)) {

                        if ('' !== $state->last_graded->penalty && ((float)$state->last_graded->penalty) > 0.0) {
                            // A penalty was applied so display it
                            echo ' ';
                            print_string('gradingdetailspenalty', 'quiz', $state->last_graded->penalty);
                        } else {
                            /* No penalty was applied even though the answer was
                            not correct (eg. a syntax error) so tell the student
                            that they were not penalised for the attempt */
                            echo ' ';
                            print_string('gradingdetailszeropenalty', 'quiz');
                        }
                    }
                }
                echo '</div>';
            }
        }
    }


	//the following three functions (get_correct_responses, get_all_responses, get_actual_response) 
	//are needed to get displayed Results->Item analysis
	
    function get_correct_responses(&$question, &$state) {
		// We cannot tell what the correct response is.
        return null;
    }

	function get_all_responses(&$question, &$state) {
        // We cannot tell the range of possible respnoses that the question can respond to,
        // but we need to return something to make things show up in the item analysis report.
        $r = new stdClass();
        $r->answer = get_string('responses','qtype_sojunit');
        $r->credit = 0.0;
        $result = new stdClass;
        $result->id = $question->id;
        $result->responses = array($r);
        return $result;
    }

	//list each attempts' "Answer's text"
    function get_actual_response($question, $state) {
       $responses = array();
       if (!empty($state->responses['__answerLine'])) {
			$responses[] = stripslashes($state->responses['__answerLine']);
       } else if (!empty($state->responses['__actionSummary'])) {
			$responses[] = stripslashes($state->responses['__actionSummary']);
       } else {
			$responses[] = implode(',output:  ', $state->responses);
			//this usually displays the student-response, compiler-output and execution-output
			//we just want to display compiler- and execution-outputs
			//this way the teacher can get a grouped overview by compiler and execution - outputs:
				//this way the teacher can understand which test case being problematic for the students or 
				//how many responses he needs to grade manually since the responses didn't compile or 
				//if the teachers test-file didn't compile at all
			foreach ($responses as $k => $v){ 
				$pos_of_output = strpos($v, ",output:  ");
				$responses[$k] = substr($v, $pos_of_output + 1);
			}
       }
       return $responses;
    }
	
	// need to overwrite this function to let the event-element from the question_states-table know that it shall stay in state 3 as long as the givencode is 
	// included in the $state->responses['']-array. In case the student gives its own response the state shall change to 2.
	function compare_responses($question, $state, $teststate) {
		if (isset($state->responses['']) && isset($teststate->responses[''])) {
			if ($state->responses[''] == $question->options->givencode){
				return true;
			}
			else{
				return strcmp($state->responses[''], $teststate->responses['']) == 0;
			}
		}
        return false;
    }

    /**
     * Backup the data in the question
     *
     * This is used in question/backuplib.php
     */
    function backup($bf,$preferences,$question,$level=6) {
        $status = true;

        // Sue - code to backup an instance of your question type. //adapted from multichoice_questiontype.php
        $sojunits = get_records("question_sojunit", "questionid",$question,"id");
        //if there are junits
        if ($sojunits){
            //iterate over each sojunit
            foreach ($sojunits as $sojunit){
                $status = fwrite($bf,start_tag("SOJUNIT", $level,true));
                //Print sojunit contents
                fwrite ($bf,full_tag("GIVENCODE",$level+1,false,$sojunit->givencode));
                fwrite ($bf,full_tag("TESTCLASSNAME",$level+1,false,$sojunit->testclassname));
				fwrite ($bf,full_tag("USEEDITORGENFEEDB",$level+1,false,$sojunit->useeditor_genfeedb));
				fwrite ($bf,full_tag("SOURCECODE",$level+1,false,$sojunit->sourcecode));
                $status = fwrite ($bf,end_tag("SOJUNIT",$level,true));
            }
			//Now print question_answers
			$status = question_backup_answers($bf,$preferences,$question);
        }
        return $status;
    }

    /**
     * Restores the data in the question: write code to restore an instance of your question type
     *
     * This is used in question/restorelib.php
     */
    function restore($old_question_id,$new_question_id,$info,$restore) {
        $status = true;

        ///////*********************************************** taken from regexp question type
        $sojunit = '';

        //Get the sojunit array
		if (array_key_exists('SOJUNIT', $info['#'])) {
			$sojunits = $info['#']['SOJUNIT'];
		} else {
			$sojunits = array();
		}

		//Iterate over sojunit
        for($i = 0; $i < sizeof($sojunits); $i++) {
            $jun_info = $sojunits[$i];
		
	        //Now, build the question_sojunit record structure
			$sojunit = new stdClass;
	        $sojunit->questionid = $new_question_id;
	        $sojunit->givencode = backup_todb($jun_info['#']['GIVENCODE']['0']['#']);
	        $sojunit->testclassname = backup_todb($jun_info['#']['TESTCLASSNAME']['0']['#']);
			$sojunit->useeditor_genfeedb = backup_todb($jun_info['#']['USEEDITORGENFEEDB']['0']['#']);
			$sojunit->sourcecode = backup_todb($jun_info['#']['SOURCECODE']['0']['#']);

	        //The structure is equal to the db, so insert the question_regexp
	        $newid = insert_record ("question_sojunit",$sojunit);

	        //Do some output
	        if (($i+1) % 50 == 0) {
	            if (!defined('RESTORE_SILENTLY')) {
	                echo ".";
	                if (($i+1) % 1000 == 0) {
	                    echo "<br />";
	                }
	            }
	            backup_flush(300);
	        }
	        if (!$newid) {
	            $status = false;
	        }
        ////****************************************************
		}
        return $status;
    }

}

// Register this question type with the system.
question_register_questiontype(new sojunit_qtype());
?>

Moodle CVS Admin
ViewVC Help
Powered by ViewVC 1.0.7