516cf3eb |
1 | <?php // $Id$ |
2 | /** |
4323d029 |
3 | * The default questiontype class. |
4 | * |
5 | * @author Martin Dougiamas and many others. This has recently been completely |
6 | * rewritten by Alex Smith, Julian Sedding and Gustav Delius as part of |
7 | * the Serving Mathematics project |
8 | * {@link http://maths.york.ac.uk/serving_maths} |
9 | * @license http://www.gnu.org/copyleft/gpl.html GNU Public License |
10 | * @package questionbank |
11 | * @subpackage questiontypes |
12 | *//** */ |
516cf3eb |
13 | |
8d205441 |
14 | require_once($CFG->libdir . '/questionlib.php'); |
516cf3eb |
15 | |
32b0adfa |
16 | /** |
17 | * This is the base class for Moodle question types. |
271ffe3f |
18 | * |
32b0adfa |
19 | * There are detailed comments on each method, explaining what the method is |
20 | * for, and the circumstances under which you might need to override it. |
271ffe3f |
21 | * |
32b0adfa |
22 | * Note: the questiontype API should NOT be considered stable yet. Very few |
23 | * question tyeps have been produced yet, so we do not yet know all the places |
271ffe3f |
24 | * where the current API is insufficient. I would rather learn from the |
32b0adfa |
25 | * experiences of the first few question type implementors, and improve the |
26 | * interface to meet their needs, rather the freeze the API prematurely and |
27 | * condem everyone to working round a clunky interface for ever afterwards. |
4323d029 |
28 | * |
29 | * @package questionbank |
30 | * @subpackage questiontypes |
32b0adfa |
31 | */ |
af3830ee |
32 | class default_questiontype { |
516cf3eb |
33 | |
34 | /** |
a2156789 |
35 | * Name of the question type |
36 | * |
37 | * The name returned should coincide with the name of the directory |
38 | * in which this questiontype is located |
271ffe3f |
39 | * |
32b0adfa |
40 | * @return string the name of this question type. |
a2156789 |
41 | */ |
516cf3eb |
42 | function name() { |
43 | return 'default'; |
44 | } |
45 | |
a2156789 |
46 | /** |
271ffe3f |
47 | * The name this question should appear as in the create new question |
a2156789 |
48 | * dropdown. |
271ffe3f |
49 | * |
a2156789 |
50 | * @return mixed the desired string, or false to hide this question type in the menu. |
51 | */ |
52 | function menu_name() { |
53 | $name = $this->name(); |
54 | $menu_name = get_string($name, 'qtype_' . $name); |
55 | if ($menu_name[0] == '[') { |
271ffe3f |
56 | // Legacy behavior, if the string was not in the proper qtype_name |
a2156789 |
57 | // language file, look it up in the quiz one. |
c2f8c4be |
58 | $menu_name = get_string($name, 'quiz'); |
a2156789 |
59 | } |
60 | return $menu_name; |
61 | } |
271ffe3f |
62 | |
a2156789 |
63 | /** |
64 | * @return boolean true if this question can only be graded manually. |
65 | */ |
66 | function is_manual_graded() { |
67 | return false; |
68 | } |
69 | |
70 | /** |
71 | * @return boolean true if this question type can be used by the random question type. |
72 | */ |
73 | function is_usable_by_random() { |
74 | return true; |
75 | } |
76 | |
c85607f0 |
77 | /** |
78 | * @return whether the question_answers.answer field needs to have |
79 | * restore_decode_content_links_worker called on it. |
80 | */ |
81 | function has_html_answers() { |
82 | return false; |
83 | } |
84 | |
36703ed7 |
85 | /** |
86 | * Return an instance of the question editing form definition. This looks for a |
87 | * class called edit_{$this->name()}_question_form in the file |
88 | * {$CFG->docroot}/question/type/{$this->name()}/edit_{$this->name()}_question_form.php |
89 | * and if it exists returns an instance of it. |
90 | * |
91 | * @param string $submiturl passed on to the constructor call. |
92 | * @return object an instance of the form definition, or null if one could not be found. |
93 | */ |
271ffe3f |
94 | function create_editing_form($submiturl, $question) { |
36703ed7 |
95 | global $CFG; |
96 | require_once("{$CFG->dirroot}/question/type/edit_question_form.php"); |
271ffe3f |
97 | $definition_file = $CFG->dirroot.'/question/type/'.$this->name().'/edit_'.$this->name().'_form.php'; |
36703ed7 |
98 | if (!(is_readable($definition_file) && is_file($definition_file))) { |
99 | return null; |
100 | } |
101 | require_once($definition_file); |
271ffe3f |
102 | $classname = 'question_edit_'.$this->name().'_form'; |
36703ed7 |
103 | if (!class_exists($classname)) { |
104 | return null; |
105 | } |
271ffe3f |
106 | return new $classname($submiturl, $question); |
36703ed7 |
107 | } |
108 | |
99ba746d |
109 | /** |
110 | * @return string the full path of the folder this plugin's files live in. |
111 | */ |
112 | function plugin_dir() { |
113 | global $CFG; |
114 | return $CFG->dirroot . '/question/type/' . $this->name(); |
115 | } |
116 | |
117 | /** |
118 | * @return string the URL of the folder this plugin's files live in. |
119 | */ |
120 | function plugin_baseurl() { |
121 | global $CFG; |
122 | return $CFG->wwwroot . '/question/type/' . $this->name(); |
123 | } |
124 | |
7b41a4a9 |
125 | /** |
126 | * This method should be overriden if you want to include a special heading or some other |
127 | * html on a question editing page besides the question editing form. |
128 | * |
129 | * @param question_edit_form $mform a child of question_edit_form |
130 | * @param object $question |
131 | * @param string $wizardnow is '' for first page. |
132 | */ |
133 | function display_question_editing_page(&$mform, $question, $wizardnow){ |
c2f8c4be |
134 | $name = $this->name(); |
135 | $strheading = get_string('editing' . $name, 'qtype_' . $name); |
136 | if ($strheading[0] == '[') { |
137 | // Legacy behavior, if the string was not in the proper qtype_name |
138 | // language file, look it up in the quiz one. |
139 | $strheading = get_string('editing' . $name, 'quiz'); |
140 | } |
7b41a4a9 |
141 | |
c2f8c4be |
142 | print_heading_with_help($strheading, $name, 'quiz'); |
7b41a4a9 |
143 | $mform->display(); |
7b41a4a9 |
144 | } |
145 | |
36703ed7 |
146 | /** |
147 | * |
148 | * |
149 | * @param $question |
150 | */ |
151 | function set_default_options(&$question) { |
36703ed7 |
152 | } |
153 | |
516cf3eb |
154 | /** |
155 | * Saves or updates a question after editing by a teacher |
156 | * |
157 | * Given some question info and some data about the answers |
158 | * this function parses, organises and saves the question |
159 | * It is used by {@link question.php} when saving new data from |
160 | * a form, and also by {@link import.php} when importing questions |
161 | * This function in turn calls {@link save_question_options} |
162 | * to save question-type specific options |
32b0adfa |
163 | * @param object $question the question object which should be updated |
164 | * @param object $form the form submitted by the teacher |
165 | * @param object $course the course we are in |
695d225c |
166 | * @return object On success, return the new question object. On failure, |
271ffe3f |
167 | * return an object as follows. If the error object has an errors field, |
32b0adfa |
168 | * display that as an error message. Otherwise, the editing form will be |
695d225c |
169 | * redisplayed with validation errors, from validation_errors field, which |
170 | * is itself an object, shown next to the form fields. |
516cf3eb |
171 | */ |
172 | function save_question($question, $form, $course) { |
173 | // This default implementation is suitable for most |
174 | // question types. |
271ffe3f |
175 | |
516cf3eb |
176 | // First, save the basic question itself |
88495aa8 |
177 | if (!record_exists('question_categories', 'id', $form->category)) { |
178 | print_error('categorydoesnotexist', 'question'); |
179 | } |
6000365e |
180 | $question->category = $form->category; |
516cf3eb |
181 | $question->name = trim($form->name); |
182 | $question->questiontext = trim($form->questiontext); |
183 | $question->questiontextformat = $form->questiontextformat; |
184 | $question->parent = isset($form->parent)? $form->parent : 0; |
185 | $question->length = $this->actual_number_of_questions($question); |
186 | $question->penalty = isset($form->penalty) ? $form->penalty : 0; |
187 | |
188 | if (empty($form->image)) { |
189 | $question->image = ""; |
190 | } else { |
191 | $question->image = $form->image; |
192 | } |
193 | |
a4514d91 |
194 | if (empty($form->generalfeedback)) { |
195 | $question->generalfeedback = ''; |
1b8a7434 |
196 | } else { |
a4514d91 |
197 | $question->generalfeedback = trim($form->generalfeedback); |
1b8a7434 |
198 | } |
199 | |
516cf3eb |
200 | if (empty($question->name)) { |
5433ee83 |
201 | $question->name = substr(strip_tags($question->questiontext), 0, 15); |
516cf3eb |
202 | if (empty($question->name)) { |
203 | $question->name = '-'; |
204 | } |
205 | } |
206 | |
207 | if ($question->penalty > 1 or $question->penalty < 0) { |
208 | $question->errors['penalty'] = get_string('invalidpenalty', 'quiz'); |
209 | } |
210 | |
211 | if (isset($form->defaultgrade)) { |
212 | $question->defaultgrade = $form->defaultgrade; |
213 | } |
214 | |
516cf3eb |
215 | if (!empty($question->id)) { // Question already exists |
cbe20043 |
216 | // keep existing unique stamp code |
217 | $question->stamp = get_field('question', 'stamp', 'id', $question->id); |
4f48fb42 |
218 | if (!update_record("question", $question)) { |
516cf3eb |
219 | error("Could not update question!"); |
220 | } |
221 | } else { // Question is a new one |
cbe20043 |
222 | // Set the unique code |
223 | $question->stamp = make_unique_id_code(); |
4f48fb42 |
224 | if (!$question->id = insert_record("question", $question)) { |
516cf3eb |
225 | error("Could not insert new question!"); |
226 | } |
227 | } |
228 | |
229 | // Now to save all the answers and type-specific options |
230 | |
6459b8fc |
231 | $form->id = $question->id; |
232 | $form->qtype = $question->qtype; |
516cf3eb |
233 | $form->category = $question->category; |
6459b8fc |
234 | $form->questiontext = $question->questiontext; |
516cf3eb |
235 | |
236 | $result = $this->save_question_options($form); |
237 | |
238 | if (!empty($result->error)) { |
239 | error($result->error); |
240 | } |
241 | |
242 | if (!empty($result->notice)) { |
243 | notice($result->notice, "question.php?id=$question->id"); |
244 | } |
245 | |
246 | if (!empty($result->noticeyesno)) { |
25ee4157 |
247 | notice_yesno($result->noticeyesno, "question.php?id=$question->id&courseid={$course->id}", |
248 | "edit.php?courseid={$course->id}"); |
516cf3eb |
249 | print_footer($course); |
250 | exit; |
251 | } |
252 | |
cbe20043 |
253 | // Give the question a unique version stamp determined by question_hash() |
254 | if (!set_field('question', 'version', question_hash($question), 'id', $question->id)) { |
255 | error('Could not update question version field'); |
256 | } |
257 | |
516cf3eb |
258 | return $question; |
259 | } |
271ffe3f |
260 | |
516cf3eb |
261 | /** |
262 | * Saves question-type specific options |
263 | * |
264 | * This is called by {@link save_question()} to save the question-type specific data |
265 | * @return object $result->error or $result->noticeyesno or $result->notice |
266 | * @param object $question This holds the information from the editing form, |
267 | * it is not a standard question object. |
268 | */ |
269 | function save_question_options($question) { |
4eda4eec |
270 | return null; |
516cf3eb |
271 | } |
272 | |
273 | /** |
274 | * Changes all states for the given attempts over to a new question |
275 | * |
276 | * This is used by the versioning code if the teacher requests that a question |
277 | * gets replaced by the new version. In order for the attempts to be regraded |
278 | * properly all data in the states referring to the old question need to be |
279 | * changed to refer to the new version instead. In particular for question types |
280 | * that use the answers table the answers belonging to the old question have to |
281 | * be changed to those belonging to the new version. |
282 | * |
283 | * @param integer $oldquestionid The id of the old question |
284 | * @param object $newquestion The new question |
285 | * @param array $attempts An array of all attempt objects in whose states |
286 | * replacement should take place |
287 | */ |
288 | function replace_question_in_attempts($oldquestionid, $newquestion, $attemtps) { |
289 | echo 'Not yet implemented'; |
290 | return; |
291 | } |
292 | |
293 | /** |
294 | * Loads the question type specific options for the question. |
295 | * |
296 | * This function loads any question type specific options for the |
297 | * question from the database into the question object. This information |
298 | * is placed in the $question->options field. A question type is |
299 | * free, however, to decide on a internal structure of the options field. |
300 | * @return bool Indicates success or failure. |
301 | * @param object $question The question object for the question. This object |
302 | * should be updated to include the question type |
303 | * specific information (it is passed by reference). |
304 | */ |
305 | function get_question_options(&$question) { |
306 | if (!isset($question->options)) { |
307 | $question->options = new object; |
308 | } |
309 | // The default implementation attaches all answers for this question |
f07d1d31 |
310 | $question->options->answers = get_records('question_answers', 'question', $question->id, 'id ASC'); |
516cf3eb |
311 | return true; |
312 | } |
313 | |
314 | /** |
0429cd86 |
315 | * Deletes states from the question-type specific tables |
316 | * |
317 | * @param string $stateslist Comma separated list of state ids to be deleted |
318 | */ |
319 | function delete_states($stateslist) { |
320 | /// The default question type does not have any tables of its own |
321 | // therefore there is nothing to delete |
322 | |
323 | return true; |
324 | } |
325 | |
326 | /** |
327 | * Deletes a question from the question-type specific tables |
516cf3eb |
328 | * |
329 | * @return boolean Success/Failure |
330 | * @param object $question The question being deleted |
331 | */ |
90c3f310 |
332 | function delete_question($questionid) { |
516cf3eb |
333 | /// The default question type does not have any tables of its own |
334 | // therefore there is nothing to delete |
335 | |
336 | return true; |
337 | } |
338 | |
339 | /** |
340 | * Returns the number of question numbers which are used by the question |
341 | * |
342 | * This function returns the number of question numbers to be assigned |
343 | * to the question. Most question types will have length one; they will be |
dfa47f96 |
344 | * assigned one number. The 'description' type, however does not use up a |
516cf3eb |
345 | * number and so has a length of zero. Other question types may wish to |
346 | * handle a bundle of questions and hence return a number greater than one. |
347 | * @return integer The number of question numbers which should be |
348 | * assigned to the question. |
349 | * @param object $question The question whose length is to be determined. |
350 | * Question type specific information is included. |
351 | */ |
352 | function actual_number_of_questions($question) { |
353 | // By default, each question is given one number |
354 | return 1; |
355 | } |
356 | |
357 | /** |
358 | * Creates empty session and response information for the question |
359 | * |
360 | * This function is called to start a question session. Empty question type |
361 | * specific session data (if any) and empty response data will be added to the |
362 | * state object. Session data is any data which must persist throughout the |
363 | * attempt possibly with updates as the user interacts with the |
364 | * question. This function does NOT create new entries in the database for |
365 | * the session; a call to the {@link save_session_and_responses} member will |
366 | * occur to do this. |
367 | * @return bool Indicates success or failure. |
368 | * @param object $question The question for which the session is to be |
369 | * created. Question type specific information is |
370 | * included. |
371 | * @param object $state The state to create the session for. Note that |
372 | * this will not have been saved in the database so |
373 | * there will be no id. This object will be updated |
374 | * to include the question type specific information |
375 | * (it is passed by reference). In particular, empty |
376 | * responses will be created in the ->responses |
377 | * field. |
378 | * @param object $cmoptions |
379 | * @param object $attempt The attempt for which the session is to be |
380 | * started. Questions may wish to initialize the |
381 | * session in different ways depending on the user id |
382 | * or time available for the attempt. |
383 | */ |
384 | function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) { |
385 | // The default implementation should work for the legacy question types. |
386 | // Most question types with only a single form field for the student's response |
387 | // will use the empty string '' as the index for that one response. This will |
388 | // automatically be stored in and restored from the answer field in the |
4f48fb42 |
389 | // question_states table. |
5a14d563 |
390 | $state->responses = array( |
391 | '' => '', |
392 | ); |
516cf3eb |
393 | return true; |
394 | } |
395 | |
396 | /** |
397 | * Restores the session data and most recent responses for the given state |
398 | * |
399 | * This function loads any session data associated with the question |
400 | * session in the given state from the database into the state object. |
401 | * In particular it loads the responses that have been saved for the given |
402 | * state into the ->responses member of the state object. |
403 | * |
404 | * Question types with only a single form field for the student's response |
405 | * will not need not restore the responses; the value of the answer |
4f48fb42 |
406 | * field in the question_states table is restored to ->responses[''] |
516cf3eb |
407 | * before this function is called. Question types with more response fields |
408 | * should override this method and set the ->responses field to an |
409 | * associative array of responses. |
410 | * @return bool Indicates success or failure. |
411 | * @param object $question The question object for the question including any |
412 | * question type specific information. |
413 | * @param object $state The saved state to load the session for. This |
414 | * object should be updated to include the question |
415 | * type specific session information and responses |
416 | * (it is passed by reference). |
417 | */ |
418 | function restore_session_and_responses(&$question, &$state) { |
419 | // The default implementation does nothing (successfully) |
420 | return true; |
421 | } |
422 | |
423 | /** |
424 | * Saves the session data and responses for the given question and state |
425 | * |
426 | * This function saves the question type specific session data from the |
427 | * state object to the database. In particular for most question types it saves the |
428 | * responses from the ->responses member of the state object. The question type |
4f48fb42 |
429 | * non-specific data for the state has already been saved in the question_states |
516cf3eb |
430 | * table and the state object contains the corresponding id and |
431 | * sequence number which may be used to index a question type specific table. |
432 | * |
433 | * Question types with only a single form field for the student's response |
434 | * which is contained in ->responses[''] will not have to save this response, |
4f48fb42 |
435 | * it will already have been saved to the answer field of the question_states table. |
516cf3eb |
436 | * Question types with more response fields should override this method and save |
437 | * the responses in their own database tables. |
438 | * @return bool Indicates success or failure. |
439 | * @param object $question The question object for the question including |
440 | * the question type specific information. |
441 | * @param object $state The state for which the question type specific |
442 | * data and responses should be saved. |
443 | */ |
444 | function save_session_and_responses(&$question, &$state) { |
445 | // The default implementation does nothing (successfully) |
446 | return true; |
447 | } |
448 | |
449 | /** |
450 | * Returns an array of values which will give full marks if graded as |
451 | * the $state->responses field |
452 | * |
453 | * The correct answer to the question in the given state, or an example of |
454 | * a correct answer if there are many, is returned. This is used by some question |
455 | * types in the {@link grade_responses()} function but it is also used by the |
456 | * question preview screen to fill in correct responses. |
457 | * @return mixed A response array giving the responses corresponding |
458 | * to the (or a) correct answer to the question. If there is |
459 | * no correct answer that scores 100% then null is returned. |
460 | * @param object $question The question for which the correct answer is to |
461 | * be retrieved. Question type specific information is |
462 | * available. |
463 | * @param object $state The state of the question, for which a correct answer is |
464 | * needed. Question type specific information is included. |
465 | */ |
466 | function get_correct_responses(&$question, &$state) { |
467 | /* The default implementation returns the response for the first answer |
468 | that gives full marks. */ |
4eda4eec |
469 | if ($question->options->answers) { |
470 | foreach ($question->options->answers as $answer) { |
471 | if (((int) $answer->fraction) === 1) { |
1f8db780 |
472 | return array('' => addslashes($answer->answer)); |
4eda4eec |
473 | } |
516cf3eb |
474 | } |
475 | } |
476 | return null; |
477 | } |
478 | |
479 | /** |
480 | * Return an array of values with the texts for all possible responses stored |
481 | * for the question |
482 | * |
483 | * All answers are found and their text values isolated |
484 | * @return object A mixed object |
485 | * ->id question id. Needed to manage random questions: |
486 | * it's the id of the actual question presented to user in a given attempt |
487 | * ->responses An array of values giving the responses corresponding |
488 | * to all answers to the question. Answer ids are used as keys. |
489 | * The text and partial credit are the object components |
490 | * @param object $question The question for which the answers are to |
491 | * be retrieved. Question type specific information is |
492 | * available. |
493 | */ |
494 | // ULPGC ecastro |
495 | function get_all_responses(&$question, &$state) { |
7baff8aa |
496 | if (isset($question->options->answers) && is_array($question->options->answers)) { |
497 | $answers = array(); |
516cf3eb |
498 | foreach ($question->options->answers as $aid=>$answer) { |
7baff8aa |
499 | $r = new stdClass; |
516cf3eb |
500 | $r->answer = $answer->answer; |
501 | $r->credit = $answer->fraction; |
502 | $answers[$aid] = $r; |
503 | } |
7baff8aa |
504 | $result = new stdClass; |
505 | $result->id = $question->id; |
506 | $result->responses = $answers; |
507 | return $result; |
516cf3eb |
508 | } else { |
7baff8aa |
509 | return null; |
516cf3eb |
510 | } |
516cf3eb |
511 | } |
512 | |
513 | /** |
514 | * Return the actual response to the question in a given state |
515 | * for the question |
516 | * |
517 | * @return mixed An array containing the response or reponses (multiple answer, match) |
518 | * given by the user in a particular attempt. |
519 | * @param object $question The question for which the correct answer is to |
520 | * be retrieved. Question type specific information is |
521 | * available. |
522 | * @param object $state The state object that corresponds to the question, |
523 | * for which a correct answer is needed. Question |
524 | * type specific information is included. |
525 | */ |
526 | // ULPGC ecastro |
8cc274de |
527 | function get_actual_response($question, $state) { |
528 | // change length to truncate responses here if you want |
529 | $lmax = 40; |
530 | if (!empty($state->responses)) { |
531 | $responses[] = (strlen($state->responses['']) > $lmax) ? |
532 | substr($state->responses[''], 0, $lmax).'...' : $state->responses['']; |
533 | } else { |
534 | $responses[] = ''; |
535 | } |
536 | return $responses; |
516cf3eb |
537 | } |
538 | |
539 | // ULPGC ecastro |
540 | function get_fractional_grade(&$question, &$state) { |
541 | $maxgrade = $question->maxgrade; |
542 | $grade = $state->grade; |
543 | if ($maxgrade) { |
544 | return (float)($grade/$maxgrade); |
545 | } else { |
546 | return (float)$grade; |
547 | } |
548 | } |
549 | |
550 | |
551 | /** |
552 | * Checks if the response given is correct and returns the id |
553 | * |
554 | * @return int The ide number for the stored answer that matches the response |
555 | * given by the user in a particular attempt. |
556 | * @param object $question The question for which the correct answer is to |
557 | * be retrieved. Question type specific information is |
558 | * available. |
559 | * @param object $state The state object that corresponds to the question, |
560 | * for which a correct answer is needed. Question |
561 | * type specific information is included. |
562 | */ |
563 | // ULPGC ecastro |
564 | function check_response(&$question, &$state){ |
565 | return false; |
566 | } |
567 | |
568 | /** |
99ba746d |
569 | * If this question type requires extra CSS or JavaScript to function, |
570 | * then this method will return an array of <link ...> tags that reference |
571 | * those stylesheets. This function will also call require_js() |
572 | * from ajaxlib.php, to get any necessary JavaScript linked in too. |
573 | * |
574 | * The two parameters match the first two parameters of print_question. |
575 | * |
576 | * @param object $question The question object. |
577 | * @param object $state The state object. |
578 | * |
579 | * @return an array of bits of HTML to add to the head of pages where |
580 | * this question is print_question-ed in the body. The array should use |
581 | * integer array keys, which have no significance. |
582 | */ |
583 | function get_html_head_contributions(&$question, &$state) { |
584 | // By default, we link to any of the files styles.css, styles.php, |
585 | // script.js or script.php that exist in the plugin folder. |
586 | // Core question types should not use this mechanism. Their styles |
587 | // should be included in the standard theme. |
588 | |
589 | // We only do this once |
590 | // for this question type, no matter how often this method is called. |
591 | static $already_done = false; |
592 | if ($already_done) { |
593 | return array(); |
594 | } |
595 | $already_done = true; |
596 | |
597 | $plugindir = $this->plugin_dir(); |
598 | $baseurl = $this->plugin_baseurl(); |
599 | $stylesheets = array(); |
600 | if (file_exists($plugindir . '/styles.css')) { |
601 | $stylesheets[] = 'styles.css'; |
602 | } |
603 | if (file_exists($plugindir . '/styles.php')) { |
604 | $stylesheets[] = 'styles.php'; |
605 | } |
606 | if (file_exists($plugindir . '/script.js')) { |
607 | require_js($baseurl . '/script.js'); |
608 | } |
609 | if (file_exists($plugindir . '/script.php')) { |
610 | require_js($baseurl . '/script.php'); |
611 | } |
612 | $contributions = array(); |
613 | foreach ($stylesheets as $stylesheet) { |
614 | $contributions[] = '<link rel="stylesheet" type="text/css" href="' . |
615 | $baseurl . '/' . $stylesheet . '" />"'; |
616 | } |
617 | return $contributions; |
618 | } |
619 | |
620 | /** |
621 | * Prints the question including the number, grading details, content, |
622 | * feedback and interactions |
623 | * |
624 | * This function prints the question including the question number, |
625 | * grading details, content for the question, any feedback for the previously |
626 | * submitted responses and the interactions. The default implementation calls |
627 | * various other methods to print each of these parts and most question types |
628 | * will just override those methods. |
629 | * @param object $question The question to be rendered. Question type |
630 | * specific information is included. The |
631 | * maximum possible grade is in ->maxgrade. The name |
632 | * prefix for any named elements is in ->name_prefix. |
633 | * @param object $state The state to render the question in. The grading |
634 | * information is in ->grade, ->raw_grade and |
635 | * ->penalty. The current responses are in |
636 | * ->responses. This is an associative array (or the |
637 | * empty string or null in the case of no responses |
638 | * submitted). The last graded state is in |
639 | * ->last_graded (hence the most recently graded |
640 | * responses are in ->last_graded->responses). The |
641 | * question type specific information is also |
642 | * included. |
643 | * @param integer $number The number for this question. |
644 | * @param object $cmoptions |
645 | * @param object $options An object describing the rendering options. |
646 | */ |
516cf3eb |
647 | function print_question(&$question, &$state, $number, $cmoptions, $options) { |
648 | /* The default implementation should work for most question types |
649 | provided the member functions it calls are overridden where required. |
37a12367 |
650 | The layout is determined by the template question.html */ |
271ffe3f |
651 | |
516cf3eb |
652 | global $CFG; |
1b8a7434 |
653 | $isgraded = question_state_is_graded($state->last_graded); |
516cf3eb |
654 | |
647bcd41 |
655 | // If this question is being shown in the context of a quiz |
656 | // get the context so we can determine whether some extra links |
271ffe3f |
657 | // should be shown. (Don't show these links during question preview.) |
d758f1e2 |
658 | $cm = get_coursemodule_from_instance('quiz', $cmoptions->id); |
647bcd41 |
659 | if (!empty($cm->id)) { |
660 | $context = get_context_instance(CONTEXT_MODULE, $cm->id); |
ec6adca0 |
661 | } else if (!empty($cm->course)) { |
662 | $context = get_context_instance(CONTEXT_COURSE, $cm->course); |
647bcd41 |
663 | } else { |
ec6adca0 |
664 | $context = get_context_instance(CONTEXT_SYSTEM, SITEID); |
647bcd41 |
665 | } |
271ffe3f |
666 | |
516cf3eb |
667 | // For editing teachers print a link to an editing popup window |
668 | $editlink = ''; |
ec6adca0 |
669 | if ($context && has_capability('moodle/question:manage', $context)) { |
516cf3eb |
670 | $stredit = get_string('edit'); |
0fd5feef |
671 | $linktext = '<img src="'.$CFG->pixpath.'/t/edit.gif" alt="'.$stredit.'" />'; |
ee6c9355 |
672 | $editlink = link_to_popup_window('/question/question.php?inpopup=1&id='.$question->id, 'editquestion', $linktext, 450, 550, $stredit, '', true); |
516cf3eb |
673 | } |
674 | |
a4514d91 |
675 | $generalfeedback = ''; |
54074fb8 |
676 | if ($options->generalfeedback) { |
a4514d91 |
677 | $generalfeedback = $this->format_text($question->generalfeedback, |
1b8a7434 |
678 | $question->questiontextformat, $cmoptions); |
679 | } |
680 | |
516cf3eb |
681 | $grade = ''; |
682 | if ($question->maxgrade and $options->scores) { |
6b11a0e8 |
683 | if ($cmoptions->optionflags & QUESTION_ADAPTIVE) { |
1b8a7434 |
684 | $grade = !$isgraded ? '--/' : round($state->last_graded->grade, $cmoptions->decimalpoints).'/'; |
516cf3eb |
685 | } |
686 | $grade .= $question->maxgrade; |
687 | } |
271ffe3f |
688 | |
3e3e5a40 |
689 | $comment = stripslashes($state->manualcomment); |
b6e907a2 |
690 | $commentlink = ''; |
271ffe3f |
691 | |
647bcd41 |
692 | if (isset($options->questioncommentlink) && $context && has_capability('mod/quiz:grade', $context)) { |
b6e907a2 |
693 | $strcomment = get_string('commentorgrade', 'quiz'); |
097bd3f5 |
694 | $commentlink = '<div class="commentlink">'.link_to_popup_window ($options->questioncommentlink.'?attempt='.$state->attempt.'&question='.$question->id, |
695 | 'commentquestion', $strcomment, 450, 650, $strcomment, 'none', true).'</div>'; |
b6e907a2 |
696 | } |
516cf3eb |
697 | |
fe9b5cfd |
698 | $history = $this->history($question, $state, $number, $cmoptions, $options); |
699 | |
aaae75b0 |
700 | include "$CFG->dirroot/question/type/question.html"; |
fe9b5cfd |
701 | } |
702 | |
703 | /* |
704 | * Print history of responses |
705 | * |
706 | * Used by print_question() |
707 | */ |
708 | function history($question, $state, $number, $cmoptions, $options) { |
516cf3eb |
709 | $history = ''; |
710 | if(isset($options->history) and $options->history) { |
711 | if ($options->history == 'all') { |
712 | // show all states |
755bddf1 |
713 | $states = get_records_select('question_states', "attempt = '$state->attempt' AND question = '$question->id' AND event > '0'", 'seq_number ASC'); |
516cf3eb |
714 | } else { |
715 | // show only graded states |
755bddf1 |
716 | $states = get_records_select('question_states', "attempt = '$state->attempt' AND question = '$question->id' AND event IN (".QUESTION_EVENTGRADE.','.QUESTION_EVENTCLOSEANDGRADE.")", 'seq_number ASC'); |
516cf3eb |
717 | } |
718 | if (count($states) > 1) { |
719 | $strreviewquestion = get_string('reviewresponse', 'quiz'); |
1b8a7434 |
720 | $table = new stdClass; |
516cf3eb |
721 | $table->width = '100%'; |
d57cf4c1 |
722 | if ($options->scores) { |
723 | $table->head = array ( |
724 | get_string('numberabbr', 'quiz'), |
725 | get_string('action', 'quiz'), |
726 | get_string('response', 'quiz'), |
727 | get_string('time'), |
728 | get_string('score', 'quiz'), |
729 | //get_string('penalty', 'quiz'), |
730 | get_string('grade', 'quiz'), |
731 | ); |
732 | } else { |
733 | $table->head = array ( |
734 | get_string('numberabbr', 'quiz'), |
735 | get_string('action', 'quiz'), |
736 | get_string('response', 'quiz'), |
737 | get_string('time'), |
738 | ); |
3925a20a |
739 | } |
271ffe3f |
740 | |
516cf3eb |
741 | foreach ($states as $st) { |
0a5b58af |
742 | $st->responses[''] = $st->answer; |
743 | $this->restore_session_and_responses($question, $st); |
516cf3eb |
744 | $b = ($state->id == $st->id) ? '<b>' : ''; |
745 | $be = ($state->id == $st->id) ? '</b>' : ''; |
fe9b5cfd |
746 | if ($state->id == $st->id) { |
747 | $link = '<b>'.$st->seq_number.'</b>'; |
748 | } else { |
749 | if(isset($options->questionreviewlink)) { |
750 | $link = link_to_popup_window ($options->questionreviewlink.'?state='.$st->id.'&number='.$number, |
751 | 'reviewquestion', $st->seq_number, 450, 650, $strreviewquestion, 'none', true); |
752 | } else { |
753 | $link = $st->seq_number; |
754 | } |
755 | } |
d57cf4c1 |
756 | if ($options->scores) { |
757 | $table->data[] = array ( |
758 | $link, |
759 | $b.get_string('event'.$st->event, 'quiz').$be, |
760 | $b.$this->response_summary($question, $st).$be, |
761 | $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be, |
762 | $b.round($st->raw_grade, $cmoptions->decimalpoints).$be, |
763 | //$b.round($st->penalty, $cmoptions->decimalpoints).$be, |
764 | $b.round($st->grade, $cmoptions->decimalpoints).$be |
765 | ); |
766 | } else { |
767 | $table->data[] = array ( |
768 | $link, |
769 | $b.get_string('event'.$st->event, 'quiz').$be, |
770 | $b.$this->response_summary($question, $st).$be, |
771 | $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be, |
772 | ); |
773 | } |
516cf3eb |
774 | } |
775 | $history = make_table($table); |
776 | } |
777 | } |
fe9b5cfd |
778 | return $history; |
516cf3eb |
779 | } |
780 | |
781 | |
782 | /** |
783 | * Prints the score obtained and maximum score available plus any penalty |
784 | * information |
785 | * |
786 | * This function prints a summary of the scoring in the most recently |
787 | * graded state (the question may not have been submitted for marking at |
788 | * the current state). The default implementation should be suitable for most |
789 | * question types. |
790 | * @param object $question The question for which the grading details are |
791 | * to be rendered. Question type specific information |
792 | * is included. The maximum possible grade is in |
793 | * ->maxgrade. |
794 | * @param object $state The state. In particular the grading information |
795 | * is in ->grade, ->raw_grade and ->penalty. |
796 | * @param object $cmoptions |
797 | * @param object $options An object describing the rendering options. |
798 | */ |
799 | function print_question_grading_details(&$question, &$state, $cmoptions, $options) { |
800 | /* The default implementation prints the number of marks if no attempt |
801 | has been made. Otherwise it displays the grade obtained out of the |
802 | maximum grade available and a warning if a penalty was applied for the |
803 | attempt and displays the overall grade obtained counting all previous |
804 | responses (and penalties) */ |
805 | |
f30bbcaf |
806 | if (QUESTION_EVENTDUPLICATE == $state->event) { |
516cf3eb |
807 | echo ' '; |
808 | print_string('duplicateresponse', 'quiz'); |
809 | } |
810 | if (!empty($question->maxgrade) && $options->scores) { |
f30bbcaf |
811 | if (question_state_is_graded($state->last_graded)) { |
516cf3eb |
812 | // Display the grading details from the last graded state |
1b8a7434 |
813 | $grade = new stdClass; |
516cf3eb |
814 | $grade->cur = round($state->last_graded->grade, $cmoptions->decimalpoints); |
815 | $grade->max = $question->maxgrade; |
816 | $grade->raw = round($state->last_graded->raw_grade, $cmoptions->decimalpoints); |
817 | |
818 | // let student know wether the answer was correct |
819 | echo '<div class="correctness '; |
820 | if ($state->last_graded->raw_grade >= $question->maxgrade/1.01) { // We divide by 1.01 so that rounding errors dont matter. |
821 | echo ' correct">'; |
822 | print_string('correct', 'quiz'); |
823 | } else if ($state->last_graded->raw_grade > 0) { |
824 | echo ' partiallycorrect">'; |
825 | print_string('partiallycorrect', 'quiz'); |
826 | } else { |
827 | echo ' incorrect">'; |
828 | print_string('incorrect', 'quiz'); |
829 | } |
830 | echo '</div>'; |
831 | |
832 | echo '<div class="gradingdetails">'; |
833 | // print grade for this submission |
834 | print_string('gradingdetails', 'quiz', $grade); |
835 | if ($cmoptions->penaltyscheme) { |
836 | // print details of grade adjustment due to penalties |
837 | if ($state->last_graded->raw_grade > $state->last_graded->grade){ |
2962ad61 |
838 | echo ' '; |
516cf3eb |
839 | print_string('gradingdetailsadjustment', 'quiz', $grade); |
840 | } |
841 | // print info about new penalty |
842 | // penalty is relevant only if the answer is not correct and further attempts are possible |
c7a99084 |
843 | if (($state->last_graded->raw_grade < $question->maxgrade / 1.01) |
844 | and (QUESTION_EVENTCLOSEANDGRADE !== $state->event)) { |
845 | |
516cf3eb |
846 | if ('' !== $state->last_graded->penalty && ((float)$state->last_graded->penalty) > 0.0) { |
847 | // A penalty was applied so display it |
2962ad61 |
848 | echo ' '; |
516cf3eb |
849 | print_string('gradingdetailspenalty', 'quiz', $state->last_graded->penalty); |
850 | } else { |
851 | /* No penalty was applied even though the answer was |
852 | not correct (eg. a syntax error) so tell the student |
853 | that they were not penalised for the attempt */ |
2962ad61 |
854 | echo ' '; |
516cf3eb |
855 | print_string('gradingdetailszeropenalty', 'quiz'); |
856 | } |
857 | } |
858 | } |
859 | echo '</div>'; |
860 | } |
861 | } |
862 | } |
863 | |
864 | /** |
865 | * Prints the main content of the question including any interactions |
866 | * |
867 | * This function prints the main content of the question including the |
868 | * interactions for the question in the state given. The last graded responses |
869 | * are printed or indicated and the current responses are selected or filled in. |
870 | * Any names (eg. for any form elements) are prefixed with $question->name_prefix. |
871 | * This method is called from the print_question method. |
872 | * @param object $question The question to be rendered. Question type |
873 | * specific information is included. The name |
874 | * prefix for any named elements is in ->name_prefix. |
875 | * @param object $state The state to render the question in. The grading |
876 | * information is in ->grade, ->raw_grade and |
877 | * ->penalty. The current responses are in |
878 | * ->responses. This is an associative array (or the |
879 | * empty string or null in the case of no responses |
880 | * submitted). The last graded state is in |
881 | * ->last_graded (hence the most recently graded |
882 | * responses are in ->last_graded->responses). The |
883 | * question type specific information is also |
884 | * included. |
885 | * The state is passed by reference because some adaptive |
886 | * questions may want to update it during rendering |
887 | * @param object $cmoptions |
888 | * @param object $options An object describing the rendering options. |
889 | */ |
890 | function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) { |
891 | /* This default implementation prints an error and must be overridden |
892 | by all question type implementations, unless the default implementation |
893 | of print_question has been overridden. */ |
894 | |
895 | notify('Error: Question formulation and input controls has not' |
896 | .' been implemented for question type '.$this->name()); |
897 | } |
898 | |
899 | /** |
900 | * Prints the submit button(s) for the question in the given state |
901 | * |
902 | * This function prints the submit button(s) for the question in the |
903 | * given state. The name of any button created will be prefixed with the |
904 | * unique prefix for the question in $question->name_prefix. The suffix |
4dca7e51 |
905 | * 'submit' is reserved for the single question submit button and the suffix |
516cf3eb |
906 | * 'validate' is reserved for the single question validate button (for |
907 | * question types which support it). Other suffixes will result in a response |
908 | * of that name in $state->responses which the printing and grading methods |
909 | * can then use. |
910 | * @param object $question The question for which the submit button(s) are to |
911 | * be rendered. Question type specific information is |
912 | * included. The name prefix for any |
913 | * named elements is in ->name_prefix. |
914 | * @param object $state The state to render the buttons for. The |
915 | * question type specific information is also |
916 | * included. |
917 | * @param object $cmoptions |
918 | * @param object $options An object describing the rendering options. |
919 | */ |
920 | function print_question_submit_buttons(&$question, &$state, $cmoptions, $options) { |
921 | /* The default implementation should be suitable for most question |
922 | types. It prints a mark button in the case where individual marking is |
923 | allowed. */ |
924 | |
6b11a0e8 |
925 | if (($cmoptions->optionflags & QUESTION_ADAPTIVE) and !$options->readonly) { |
516cf3eb |
926 | echo '<input type="submit" name="'; |
927 | echo $question->name_prefix; |
4dca7e51 |
928 | echo 'submit" value="'; |
516cf3eb |
929 | print_string('mark', 'quiz'); |
5bc57211 |
930 | echo '" class="submit btn"'; |
931 | echo ' />'; |
516cf3eb |
932 | } |
933 | } |
934 | |
935 | |
936 | /** |
937 | * Return a summary of the student response |
938 | * |
939 | * This function returns a short string of no more than a given length that |
940 | * summarizes the student's response in the given $state. This is used for |
941 | * example in the response history table |
942 | * @return string The summary of the student response |
271ffe3f |
943 | * @param object $question |
516cf3eb |
944 | * @param object $state The state whose responses are to be summarized |
945 | * @param int $length The maximum length of the returned string |
946 | */ |
0a5b58af |
947 | function response_summary($question, $state, $length=80) { |
516cf3eb |
948 | // This should almost certainly be overridden |
879caa51 |
949 | $responses = $this->get_actual_response($question, $state); |
950 | if (empty($responses) || !is_array($responses)) { |
951 | $responses = array(); |
952 | } |
953 | if (is_array($responses)) { |
954 | $responses = implode(',', $responses); |
955 | } |
956 | return substr($responses, 0, $length); |
516cf3eb |
957 | } |
958 | |
959 | /** |
960 | * Renders the question for printing and returns the LaTeX source produced |
961 | * |
962 | * This function should render the question suitable for a printed problem |
963 | * or solution sheet in LaTeX and return the rendered output. |
964 | * @return string The LaTeX output. |
965 | * @param object $question The question to be rendered. Question type |
966 | * specific information is included. |
967 | * @param object $state The state to render the question in. The |
968 | * question type specific information is also |
969 | * included. |
970 | * @param object $cmoptions |
971 | * @param string $type Indicates if the question or the solution is to be |
972 | * rendered with the values 'question' and |
973 | * 'solution'. |
974 | */ |
975 | function get_texsource(&$question, &$state, $cmoptions, $type) { |
976 | // The default implementation simply returns a string stating that |
977 | // the question is only available online. |
978 | |
979 | return get_string('onlineonly', 'texsheet'); |
980 | } |
981 | |
982 | /** |
983 | * Compares two question states for equivalence of the student's responses |
984 | * |
985 | * The responses for the two states must be examined to see if they represent |
986 | * equivalent answers to the question by the student. This method will be |
987 | * invoked for each of the previous states of the question before grading |
988 | * occurs. If the student is found to have already attempted the question |
989 | * with equivalent responses then the attempt at the question is ignored; |
990 | * grading does not occur and the state does not change. Thus they are not |
991 | * penalized for this case. |
992 | * @return boolean |
993 | * @param object $question The question for which the states are to be |
994 | * compared. Question type specific information is |
995 | * included. |
996 | * @param object $state The state of the question. The responses are in |
ac249da5 |
997 | * ->responses. This is the only field of $state |
998 | * that it is safe to use. |
516cf3eb |
999 | * @param object $teststate The state whose responses are to be |
1000 | * compared. The state will be of the same age or |
271ffe3f |
1001 | * older than $state. If possible, the method should |
bb080d20 |
1002 | * only use the field $teststate->responses, however |
1003 | * any field that is set up by restore_session_and_responses |
1004 | * can be used. |
516cf3eb |
1005 | */ |
1006 | function compare_responses(&$question, $state, $teststate) { |
1007 | // The default implementation performs a comparison of the response |
1008 | // arrays. The ordering of the arrays does not matter. |
1009 | // Question types may wish to override this (eg. to ignore trailing |
1010 | // white space or to make "7.0" and "7" compare equal). |
271ffe3f |
1011 | |
c82f76d0 |
1012 | return $state->responses === $teststate->responses; |
516cf3eb |
1013 | } |
1014 | |
37a12367 |
1015 | /** |
1016 | * Checks whether a response matches a given answer |
1017 | * |
1018 | * This method only applies to questions that use teacher-defined answers |
1019 | * |
1020 | * @return boolean |
1021 | */ |
1022 | function test_response(&$question, &$state, $answer) { |
1023 | $response = isset($state->responses['']) ? $state->responses[''] : ''; |
1024 | return ($response == $answer->answer); |
1025 | } |
1026 | |
516cf3eb |
1027 | /** |
1028 | * Performs response processing and grading |
1029 | * |
1030 | * This function performs response processing and grading and updates |
1031 | * the state accordingly. |
1032 | * @return boolean Indicates success or failure. |
1033 | * @param object $question The question to be graded. Question type |
1034 | * specific information is included. |
1035 | * @param object $state The state of the question to grade. The current |
1036 | * responses are in ->responses. The last graded state |
1037 | * is in ->last_graded (hence the most recently graded |
1038 | * responses are in ->last_graded->responses). The |
1039 | * question type specific information is also |
1040 | * included. The ->raw_grade and ->penalty fields |
1041 | * must be updated. The method is able to |
1042 | * close the question session (preventing any further |
1043 | * attempts at this question) by setting |
f30bbcaf |
1044 | * $state->event to QUESTION_EVENTCLOSEANDGRADE |
516cf3eb |
1045 | * @param object $cmoptions |
1046 | */ |
1047 | function grade_responses(&$question, &$state, $cmoptions) { |
5a14d563 |
1048 | // The default implementation uses the test_response method to |
1049 | // compare what the student entered against each of the possible |
271ffe3f |
1050 | // answers stored in the question, and uses the grade from the |
5a14d563 |
1051 | // first one that matches. It also sets the marks and penalty. |
1052 | // This should be good enought for most simple question types. |
516cf3eb |
1053 | |
5a14d563 |
1054 | $state->raw_grade = 0; |
516cf3eb |
1055 | foreach($question->options->answers as $answer) { |
5a14d563 |
1056 | if($this->test_response($question, $state, $answer)) { |
1057 | $state->raw_grade = $answer->fraction; |
516cf3eb |
1058 | break; |
1059 | } |
1060 | } |
5a14d563 |
1061 | |
1062 | // Make sure we don't assign negative or too high marks. |
1063 | $state->raw_grade = min(max((float) $state->raw_grade, |
1064 | 0.0), 1.0) * $question->maxgrade; |
271ffe3f |
1065 | |
5a14d563 |
1066 | // Update the penalty. |
1067 | $state->penalty = $question->penalty * $question->maxgrade; |
516cf3eb |
1068 | |
f30bbcaf |
1069 | // mark the state as graded |
1070 | $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE; |
1071 | |
516cf3eb |
1072 | return true; |
1073 | } |
1074 | |
1075 | |
1076 | /** |
1077 | * Includes configuration settings for the question type on the quiz admin |
1078 | * page |
1079 | * |
7518b645 |
1080 | * TODO: It makes no sense any longer to do the admin for question types |
1081 | * from the quiz admin page. This should be changed. |
516cf3eb |
1082 | * Returns an array of objects describing the options for the question type |
1083 | * to be included on the quiz module admin page. |
1084 | * Configuration options can be included by setting the following fields in |
1085 | * the object: |
1086 | * ->name The name of the option within this question type. |
1087 | * The full option name will be constructed as |
1088 | * "quiz_{$this->name()}_$name", the human readable name |
1089 | * will be displayed with get_string($name, 'quiz'). |
1090 | * ->code The code to display the form element, help button, etc. |
1091 | * i.e. the content for the central table cell. Be sure |
1092 | * to name the element "quiz_{$this->name()}_$name" and |
1093 | * set the value to $CFG->{"quiz_{$this->name()}_$name"}. |
1094 | * ->help Name of the string from the quiz module language file |
1095 | * to be used for the help message in the third column of |
1096 | * the table. An empty string (or the field not set) |
1097 | * means to leave the box empty. |
1098 | * Links to custom settings pages can be included by setting the following |
1099 | * fields in the object: |
1100 | * ->name The name of the link text string. |
1101 | * get_string($name, 'quiz') will be called. |
1102 | * ->link The filename part of the URL for the link. The full URL |
1103 | * is contructed as |
aaae75b0 |
1104 | * "$CFG->wwwroot/question/type/{$this->name()}/$link?sesskey=$sesskey" |
516cf3eb |
1105 | * [but with the relavant calls to the s and rawurlencode |
1106 | * functions] where $sesskey is the sesskey for the user. |
1107 | * @return array Array of objects describing the configuration options to |
1108 | * be included on the quiz module admin page. |
1109 | */ |
1110 | function get_config_options() { |
1111 | // No options by default |
1112 | |
1113 | return false; |
1114 | } |
1115 | |
1116 | /** |
dc1f00de |
1117 | * Returns true if the editing wizard is finished, false otherwise. |
1118 | * |
1119 | * The default implementation returns true, which is suitable for all question- |
516cf3eb |
1120 | * types that only use one editing form. This function is used in |
1121 | * question.php to decide whether we can regrade any states of the edited |
1122 | * question and redirect to edit.php. |
1123 | * |
1124 | * The dataset dependent question-type, which is extended by the calculated |
1125 | * question-type, overwrites this method because it uses multiple pages (i.e. |
1126 | * a wizard) to set up the question and associated datasets. |
1127 | * |
1128 | * @param object $form The data submitted by the previous page. |
1129 | * |
1130 | * @return boolean Whether the wizard's last page was submitted or not. |
1131 | */ |
1132 | function finished_edit_wizard(&$form) { |
1133 | //In the default case there is only one edit page. |
1134 | return true; |
1135 | } |
1136 | |
dc1f00de |
1137 | /** |
1138 | * Prints a table of course modules in which the question is used |
1139 | * |
7518b645 |
1140 | * TODO: This should be made quiz-independent |
1141 | * |
dc1f00de |
1142 | * This function is used near the end of the question edit forms in all question types |
1143 | * It prints the table of quizzes in which the question is used |
1144 | * containing checkboxes to allow the teacher to replace the old question version |
1145 | * |
1146 | * @param object $question |
1147 | * @param object $course |
1148 | * @param integer $cmid optional The id of the course module currently being edited |
1149 | */ |
1150 | function print_replacement_options($question, $course, $cmid='0') { |
516cf3eb |
1151 | |
1152 | // Disable until the versioning code has been fixed |
20808266 |
1153 | if (true) { |
1154 | return; |
1155 | } |
271ffe3f |
1156 | |
516cf3eb |
1157 | // no need to display replacement options if the question is new |
1158 | if(empty($question->id)) { |
1159 | return true; |
1160 | } |
1161 | |
1162 | // get quizzes using the question (using the question_instances table) |
1163 | $quizlist = array(); |
1164 | if(!$instances = get_records('quiz_question_instances', 'question', $question->id)) { |
1165 | $instances = array(); |
1166 | } |
1167 | foreach($instances as $instance) { |
1168 | $quizlist[$instance->quiz] = $instance->quiz; |
1169 | } |
1170 | $quizlist = implode(',', $quizlist); |
1171 | if(empty($quizlist) or !$quizzes = get_records_list('quiz', 'id', $quizlist)) { |
1172 | $quizzes = array(); |
1173 | } |
1174 | |
1175 | // do the printing |
1176 | if(count($quizzes) > 0) { |
1177 | // print the table |
1178 | $strquizname = get_string('modulename', 'quiz'); |
1179 | $strdoreplace = get_string('replace', 'quiz'); |
1180 | $straffectedstudents = get_string('affectedstudents', 'quiz', $course->students); |
1181 | echo "<tr valign=\"top\">\n"; |
1182 | echo "<td align=\"right\"><b>".get_string("replacementoptions", "quiz").":</b></td>\n"; |
1183 | echo "<td align=\"left\">\n"; |
1184 | echo "<table cellpadding=\"5\" align=\"left\" class=\"generalbox\" width=\"100%\">\n"; |
1185 | echo "<tr>\n"; |
077f3814 |
1186 | echo "<th align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\" scope=\"col\">$strquizname</th>\n"; |
1187 | echo "<th align=\"center\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\" scope=\"col\">$strdoreplace</th>\n"; |
1188 | echo "<th align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\" scope=\"col\">$straffectedstudents</th>\n"; |
516cf3eb |
1189 | echo "</tr>\n"; |
1190 | foreach($quizzes as $quiz) { |
1191 | // work out whethere it should be checked by default |
1192 | $checked = ''; |
dc1f00de |
1193 | if((int)$cmid === (int)$quiz->id |
516cf3eb |
1194 | or empty($quiz->usercount)) { |
1195 | $checked = "checked=\"checked\""; |
1196 | } |
1197 | |
1198 | // find how many different students have already attempted this quiz |
1199 | $students = array(); |
1200 | if($attempts = get_records_select('quiz_attempts', "quiz = '$quiz->id' AND preview = '0'")) { |
1201 | foreach($attempts as $attempt) { |
4f48fb42 |
1202 | if (record_exists('question_states', 'attempt', $attempt->uniqueid, 'question', $question->id, 'originalquestion', 0)) { |
516cf3eb |
1203 | $students[$attempt->userid] = 1; |
1204 | } |
1205 | } |
1206 | } |
1207 | $studentcount = count($students); |
1208 | |
1209 | $strstudents = $studentcount === 1 ? $course->student : $course->students; |
1210 | echo "<tr>\n"; |
1211 | echo "<td align=\"left\" class=\"generaltablecell c0\">".format_string($quiz->name)."</td>\n"; |
1212 | echo "<td align=\"center\" class=\"generaltablecell c0\"><input name=\"q{$quiz->id}replace\" type=\"checkbox\" ".$checked." /></td>\n"; |
1213 | echo "<td align=\"left\" class=\"generaltablecell c0\">".(($studentcount) ? $studentcount.' '.$strstudents : '-')."</td>\n"; |
1214 | echo "</tr>\n"; |
1215 | } |
1216 | echo "</table>\n"; |
1217 | } |
1218 | echo "</td></tr>\n"; |
1219 | } |
1220 | |
1b8a7434 |
1221 | /** |
1222 | * Print the start of the question editing form, including the question category, |
a4514d91 |
1223 | * questionname, questiontext, image, defaultgrade, penalty and generalfeedback fields. |
271ffe3f |
1224 | * |
1b8a7434 |
1225 | * Three of the fields, image, defaultgrade, penalty, are optional, and |
271ffe3f |
1226 | * can be removed from the from using the $hidefields argument. |
1227 | * |
1b8a7434 |
1228 | * @param object $question The question object that the form we are printing is for. |
1229 | * @param array $err Array of optional error messages to display by each field. |
1230 | * Used when the form is being redisplayed after validation failed. |
1231 | * @param object $course The course object for the course this question belongs to. |
1232 | * @param boolean $usehtmleditor Whether the html editor should be used. |
1233 | * @param array $hidefields An array which may contain the strings, |
1234 | * 'image', 'defaultgrade' or 'penalty' to remove the corresponding field. |
1235 | */ |
1236 | function print_question_form_start($question, $err, $course, $usehtmleditor, $hidefields = array()) { |
1237 | global $CFG; |
1238 | |
1239 | // If you edit this function, you also need to edit random/editquestion.html. |
271ffe3f |
1240 | |
1b8a7434 |
1241 | if (!in_array('image', $hidefields)) { |
1242 | make_upload_directory("$course->id"); // Just in case |
1243 | $coursefiles = get_directory_list("$CFG->dataroot/$course->id", $CFG->moddata); |
1244 | foreach ($coursefiles as $filename) { |
1245 | if (mimeinfo("icon", $filename) == "image.gif") { |
1246 | $images["$filename"] = $filename; |
1247 | } |
1248 | } |
516cf3eb |
1249 | } |
271ffe3f |
1250 | |
1b8a7434 |
1251 | include('editquestionstart.html'); |
1252 | } |
271ffe3f |
1253 | |
1b8a7434 |
1254 | /** |
1255 | * Print the end of the question editing form, including the submit, copy, |
1256 | * and cancel button, and the standard hidden fields like the sesskey and |
1257 | * the question type. |
271ffe3f |
1258 | * |
1b8a7434 |
1259 | * @param object $question The question object that the form we are printing is for. |
1260 | * @param string $submitscript Extra attributes, for example 'onsubmit="myfunction"', |
1261 | * that is added to the HTML of the submit button. |
1262 | * @param string $hiddenfields Extra hidden fields (actually any HTML) |
1263 | * to be added at the end of the form. |
1264 | */ |
1265 | function print_question_form_end($question, $submitscript = '', $hiddenfields = '') { |
1266 | global $USER; |
271ffe3f |
1267 | |
1b8a7434 |
1268 | // If you edit this function, you also need to edit random/editquestion.html. |
1269 | |
1270 | include('editquestionend.html'); |
1271 | } |
271ffe3f |
1272 | |
1b8a7434 |
1273 | /** |
1274 | * Call format_text from weblib.php with the options appropriate to question types. |
271ffe3f |
1275 | * |
1b8a7434 |
1276 | * @param string $text the text to format. |
1277 | * @param integer $text the type of text. Normally $question->questiontextformat. |
1278 | * @param object $cmoptions the context the string is being displayed in. Only $cmoptions->course is used. |
1279 | * @return string the formatted text. |
1280 | */ |
08eef20d |
1281 | function format_text($text, $textformat, $cmoptions = NULL) { |
1b8a7434 |
1282 | $formatoptions = new stdClass; |
1283 | $formatoptions->noclean = true; |
1284 | $formatoptions->para = false; |
08eef20d |
1285 | return format_text($text, $textformat, $formatoptions, $cmoptions === NULL ? NULL : $cmoptions->course); |
516cf3eb |
1286 | } |
271ffe3f |
1287 | |
c5d94c41 |
1288 | /// BACKUP FUNCTIONS //////////////////////////// |
1289 | |
1290 | /* |
1291 | * Backup the data in the question |
1292 | * |
1293 | * This is used in question/backuplib.php |
1294 | */ |
1295 | function backup($bf,$preferences,$question,$level=6) { |
1296 | // The default type has nothing to back up |
1297 | return true; |
1298 | } |
1299 | |
315559d3 |
1300 | /// RESTORE FUNCTIONS ///////////////// |
1301 | |
1302 | /* |
1303 | * Restores the data in the question |
1304 | * |
1305 | * This is used in question/restorelib.php |
1306 | */ |
1307 | function restore($old_question_id,$new_question_id,$info,$restore) { |
1308 | // The default question type has nothing to restore |
1309 | return true; |
1310 | } |
1311 | |
1312 | function restore_map($old_question_id,$new_question_id,$info,$restore) { |
1313 | // There is nothing to decode |
1314 | return true; |
1315 | } |
1316 | |
1317 | function restore_recode_answer($state, $restore) { |
1318 | // There is nothing to decode |
ba8f7ff0 |
1319 | return $state->answer; |
271ffe3f |
1320 | } |
315559d3 |
1321 | |
1322 | //This function restores the question_rqp_states |
1323 | function restore_state($state_id,$info,$restore) { |
1324 | // The default question type does not keep its own state information |
1325 | return true; |
1326 | } |
516cf3eb |
1327 | |
1328 | } |
1329 | |
96ed722b |
1330 | ?> |