/[drupal]/contributions/modules/quiz/quiz.module
ViewVC logotype

Contents of /contributions/modules/quiz/quiz.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.143 - (show annotations) (download) (as text)
Fri Aug 14 18:46:46 2009 UTC (3 months, 1 week ago) by sivaji
Branch: MAIN
CVS Tags: HEAD
Changes since 1.142: +3 -3 lines
File MIME type: text/x-php
Appending destination = $_GET['q'] to url in quiz node form page for automatic redirection after enabling/disabling quiz addon.
1 <?php
2
3 // $Id: quiz.module,v 1.142 2009/08/13 16:48:17 sivaji Exp $
4
5 /**
6 * @file
7 * Quiz Module
8 *
9 * This module allows the creation of interactive quizzes for site visitors.
10 */
11
12 // This module is structured as follows:
13 //
14 // The main module file:
15 // * Defines and general includes are at the top.
16 // * Hook implementations come immediately after.
17 // * Public functions come next.
18 // * Private functions are at the bottom.
19 //
20 // Where possible, user pages are located in quiz.pages.inc, and admin pages
21 // are in quiz.admin.inc. Most utility functions have been left here, even if they
22 // are only used by a function in one of the other files. quiz_datetime.inc holds
23 // some additional date/time functions.
24 //
25 // Themes are in quiz.pages.inc unless they clearly only apply to admin screens.
26 // Then they are in quiz.admin.inc.
27 //
28 // Views support is included in includes/views/quiz.views.inc
29 define('QUIZ_VIEWS_DIR', drupal_get_path('module', 'quiz') . '/includes/views');
30
31 include(drupal_get_path('module', 'quiz') .'/quiz_datetime.inc');
32
33 /*
34 * Define question statuses...
35 */
36 define('QUESTION_RANDOM', 0);
37 define('QUESTION_ALWAYS', 1);
38 define('QUESTION_NEVER', 2);
39
40 /**
41 * Quiz name.
42 */
43 define('QUIZ_NAME', _quiz_get_quiz_name());
44
45 /**
46 * Define feedback statuses.
47 */
48 define('QUIZ_FEEDBACK_END', 0);
49 define('QUIZ_FEEDBACK_QUESTION', 1);
50 define('QUIZ_FEEDBACK_NEVER', 2);
51
52 /**
53 * Quiz perms.
54 *
55 * TODO: Simply adding the new quiz config perm for now - refactor other perms
56 * to constants in the future.
57 */
58 define('QUIZ_PERM_ADMIN_CONFIG', 'administer quiz configuration');
59
60 /**
61 * Implementation of hook_help().
62 */
63 function quiz_help($path, $arg) {
64 // This is moved on an experimental basis.
65 include_once drupal_get_path('module', 'quiz') . '/quiz.help.inc';
66 return _quiz_help($path, $arg);
67 }
68
69 /**
70 * This module is Views 2.0 enabled.
71 * Implementation of hook_views_api().
72 */
73 function quiz_views_api() {
74 return array(
75 'api' => 2,
76 'path' => QUIZ_VIEWS_DIR,
77 );
78 }
79
80 /**
81 * Implementation of hook_perm().
82 */
83 function quiz_perm() {
84 return array(QUIZ_PERM_ADMIN_CONFIG,
85 // Administrating quizzes:
86 'administer quiz',
87 // Managing quizzes:
88 'access quiz', 'create quiz', 'edit own quiz', 'edit any quiz', 'delete any quiz', 'delete own quiz',
89 // Managing results:
90 'view user results', 'view own results',
91 // Allow a quiz question to be viewed outside of a test.
92 'view quiz question outside of a quiz',
93 // Allow someone to see the answers to a quiz question
94 'view quiz question solutions'
95 );
96 }
97
98 /**
99 * Implementation of hook_access().
100 */
101 function quiz_access($op, $node, $account) {
102
103 // Admin can do all of this.
104 if (user_access('administer quiz', $account)) {
105 return TRUE;
106 }
107
108 if (!user_access('access quiz')) {
109 // If you can't access, you get NOTHING!
110 // Otherwise, we allow further permission checking.
111 return FALSE;
112 }
113
114 switch ($op) {
115 case 'create':
116 return user_access('create quiz', $account);
117 case 'update':
118 return (user_access('edit any quiz', $account) || (user_access('edit own quiz', $account) && $account->uid == $node->uid));
119 case 'delete':
120 return (user_access('delete any quiz', $account) || (user_access('delete own quiz', $account) && $account->uid == $node->uid));
121 }
122 }
123
124 /**
125 * Implementation of hook_node_info().
126 */
127 function quiz_node_info() {
128 return array(
129 'quiz' => array(
130 'name' => t('@quiz', array("@quiz" => QUIZ_NAME)),
131 'module' => 'quiz',
132 'description' => 'Create interactive quizzes for site visitors',
133 )
134 );
135 }
136
137 /**
138 * Implementation of hook_init().
139 *
140 * Add quiz-specific styling.
141 */
142 function quiz_init() {
143 // MPB FIXME: Probably don't want to add this to _every_ page.
144 drupal_add_css(drupal_get_path('module', 'quiz') .'/quiz.css', 'module', 'all');
145 }
146
147 /**
148 * Implementation of hook_menu().
149 */
150 function quiz_menu() {
151
152 // ADMIN //
153 $items['admin/quiz'] = array(
154 'title' => t('@quiz management', array('@quiz' => QUIZ_NAME)),
155 'description' => t('View @quiz results, score tests, run reports.', array('@quiz' => QUIZ_NAME)),
156 'page callback' => 'system_admin_menu_block_page',
157 'access arguments' => array(QUIZ_PERM_ADMIN_CONFIG),
158 'type' => MENU_NORMAL_ITEM, // MENU_CALLBACK, MENU_SUGGESTED_ITEM, MENU_LOCAL_TASK, MENU_DEFAULT_LOCAL_TASK
159 'file' => 'system.admin.inc',
160 'file path' => drupal_get_path('module', 'system'),
161 //'weight' => -1,
162 //'position' => 'right',
163 //'menu_name' => 'name of menu',
164 );
165
166 $items['admin/quiz/settings'] = array(
167 'title' => t('@quiz configuration', array('@quiz' => QUIZ_NAME)),
168 'description' => t('Configure @quiz options.', array('@quiz' => QUIZ_NAME)),
169 'page callback' => 'drupal_get_form',
170 'page arguments' => array('quiz_admin_settings'),
171 'access arguments' => array(QUIZ_PERM_ADMIN_CONFIG),
172 'type' => MENU_NORMAL_ITEM, // optional
173 'file' => 'quiz.admin.inc',
174 );
175
176 $items['admin/quiz/reports'] = array(
177 'title' => t('@quiz reports', array('@quiz' => QUIZ_NAME)),
178 'description' => t('View @quiz reports.', array('@quiz' => QUIZ_NAME)),
179 'page callback' => 'system_admin_menu_block_page',
180 'access arguments' => array(QUIZ_PERM_ADMIN_CONFIG),
181 'type' => MENU_NORMAL_ITEM,
182 'file' => 'system.admin.inc',
183 'file path' => drupal_get_path('module', 'system'),
184 );
185
186 $items['admin/quiz/reports/results'] = array(
187 'title' => t('@quiz results', array('@quiz' => QUIZ_NAME)),
188 'description' => 'View results.',
189 'page callback' => 'quiz_admin_quizzes',
190 'access arguments' => array(QUIZ_PERM_ADMIN_CONFIG),
191 'type' => MENU_NORMAL_ITEM,
192 'file' => 'quiz.admin.inc',
193 );
194
195 $items['admin/quiz/reports/%/results'] = array(
196 'title' => t('View @quiz', array('@quiz' => QUIZ_NAME)),
197 'description' => t('View results for the given quiz.'),
198 'page callback' => 'quiz_admin_results',
199 'page arguments' => array(3),
200 'type' => MENU_NORMAL_ITEM, // MENU_CALLBACK, MENU_SUGGESTED_ITEM, MENU_LOCAL_TASK, MENU_DEFAULT_LOCAL_TASK
201 'file' => 'quiz.admin.inc',
202 'access arguments' => array(QUIZ_PERM_ADMIN_CONFIG),
203 );
204
205 $items['admin/quiz/%/view'] = array(
206 'title' => t('View @quiz', array('@quiz' => QUIZ_NAME)),
207 'page callback' => 'quiz_admin',
208 'page arguments' => array(2),
209 'access arguments' => array('administer quiz'),
210 'type' => MENU_CALLBACK,
211 'file' => 'quiz.admin.inc',
212 );
213
214 $items['admin/quiz/%/delete'] = array(
215 'title' => t('Delete @quiz', array('@quiz' => QUIZ_NAME)),
216 'page callback' => 'quiz_admin_result_delete',
217 //'page arguments' => array(2),
218 'access arguments' => array('administer quiz'),
219 'type' => MENU_CALLBACK,
220 'file' => 'quiz.admin.inc',
221 );
222
223 // JSON callback
224 $items['admin/quiz/listquestions'] = array(
225 'title' => t('List Quiz Questions'),
226 'description' => t('Auto-completion question listing.'),
227 'page callback' => 'quiz_admin_list_questions_ac',
228 'page arguments' => array(''),
229 'type' => MENU_CALLBACK,
230 //'access callback' => 'user_access',
231 'access arguments' => array('create quiz'),
232 'file' => 'quiz.admin.inc'
233 );
234 // AHAH callback
235 $items['admin/quiz/newquestion'] = array(
236 'title' => t('Add a Question to a Quiz'),
237 'description' => t('AHAH Callback for adding a quiz question'),
238 'page callback' => 'quiz_admin_add_question_ahah',
239 'type' => MENU_CALLBACK,
240 'access arguments' => array('create quiz'),
241 'file' => 'quiz.admin.inc',
242 );
243
244 // Menu item for adding questions to quiz.
245 $items['node/%quiz_type_access/questions'] = array(
246 'title' => t('Manage questions'),
247 'page callback' => 'quiz_questions',
248 'page arguments' => array(1),
249 'access callback' => 'node_access',
250 'access arguments' => array('update', 1),
251 'type' => MENU_LOCAL_TASK,
252 'file' => 'quiz.admin.inc',
253 );
254
255 $items['node/%quiz_type_access/admin'] = array(
256 'title' => t('Quiz admin', array('@quiz' => QUIZ_NAME)),
257 'page callback' => 'theme',
258 'page arguments' => array('quiz_view', 1),
259 'access arguments' => array('administer quiz'),
260 'type' => MENU_LOCAL_TASK,
261 'file' => 'quiz.admin.inc',
262
263 );
264
265 // USER //
266 $items['user/%/myresults'] = array(
267 'title' => t('My results'),
268 'page callback' => 'quiz_get_user_results',
269 'page arguments' => array(1),
270 'access arguments' => array('view own results'),
271 'type' => MENU_LOCAL_TASK,
272 'file' => 'quiz.pages.inc',
273 );
274
275 $items['user/quiz/%/userresults'] = array(
276 'title' => t('User results'),
277 'page callback' => 'quiz_user_results',
278 'page arguments' => array(2),
279 'access arguments' => array('view own results'),
280 'type' => MENU_CALLBACK,
281 'file' => 'quiz.pages.inc',
282 );
283
284 return $items;
285 }
286
287 /**
288 * Implementation of hook_theme().
289 */
290 function quiz_theme() {
291 return array(
292 'quiz_availability' => array(
293 'arguments' => array('node' => NULL),
294 'file' => 'quiz.pages.inc',
295 ),
296 'quiz_view' => array(
297 'arguments' => array('node' => NULL, 'teaser' => FALSE, 'page' => FALSE),
298 'file' => 'quiz.pages.inc',
299 ),
300 'quiz_get_user_results' => array(
301 'arguments' => array('results' => NULL),
302 'file' => 'quiz.pages.inc',
303 ),
304 'quiz_question_table' => array(
305 'arguments' => array('questions' => NULL, 'quiz_id' => NULL),
306 'file' => 'quiz.pages.inc',
307 ),
308 'quiz_filtered_questions' => array(
309 'arguments' => array('form' => NULL),
310 'file' => 'quiz.pages.inc',
311 ),
312 'quiz_take_question' => array(
313 'arguments' => array('quiz' => NULL, 'question_node' => NULL),
314 'file' => 'quiz.pages.inc',
315 ),
316 'quiz_take_summary' => array(
317 'arguments' => array('quiz' => NULL, 'questions' => NULL, 'score' => 0, 'summary' => ''),
318 'file' => 'quiz.pages.inc',
319 ),
320 'quiz_admin' => array(
321 'arguments' => array('results' => NULL),
322 'file' => 'quiz.admin.inc',
323 ),
324 'quiz_admin_summary' => array(
325 'arguments' => array('quiz' => NULL, 'questions' => NULL, 'score' => NULL, 'summary' => NULL),
326 'file' => 'quiz.admin.inc',
327 ),
328 'quiz_user_summary' => array(
329 'arguments' => array('quiz' => NULL, 'questions' => NULL, 'score' => NULL, 'summary' => NULL),
330 'file' => 'quiz.pages.inc',
331 ),
332 'quiz_feedback' => array(
333 'arguments' => array('questions' => NULL, 'showpoints' => TRUE, 'showfeedback' => FALSE),
334 'file' => 'quiz.pages.inc',
335 ),
336 'quiz_single_question_feedback' => array(
337 'arguments' => array('quiz' => NULL, 'report' => NULL),
338 'file' => 'quiz.pages.inc',
339 ),
340 'quiz_questions' => array(
341 'arguments' => array('form' => NULL),
342 'file' => 'quiz.pages.inc',
343 ),
344 'quiz_progress' => array(
345 'arguments' => array('question_number' => NULL, 'num_of_question' => NULL),
346 'file' => 'quiz.pages.inc',
347 ),
348 'quiz_question_table' => array(
349 'arguments' => array('questions' => NULL, 'quiz_id' => NULL),
350 'file' => 'quiz.pages.inc',
351 ),
352 'quiz_no_feedback' => array(
353 'file' => 'quiz.pages.inc',
354 'arguments' => array(),
355 ),
356 'quiz_admin_quizzes' => array(
357 'file' => 'quiz.admin.inc',
358 'arguments' => array('results' => NULL),
359 ),
360 'quiz_single_question_node' => array(
361 'file' => 'quiz.pages.inc',
362 'arguments' => array('question_node' => NULL),
363 ),
364 'question_selection_table' => array(
365 'file' => 'quiz.admin.inc',
366 'arguments' => array('form' => array()),
367 ),
368 'quiz_score_correct' => array(
369 'file' => 'quiz.pages.inc',
370 'arguments' => array(),
371 ),
372 'quiz_score_incorrect' => array(
373 'file' => 'quiz.pages.inc',
374 'arguments' => array(),
375 ),
376 );
377 }
378
379 /**
380 * Implementation of hook_form_alter().
381 *
382 * Override settings in some existing forms. For example, we remove the
383 * preview button on a quiz.
384 */
385 function quiz_form_alter(&$form, $form_state, $form_id) {
386 if ($form_id == 'quiz_node_form') {
387 // Remove preview button:
388 unset($form['buttons']['preview']);
389 }
390 }
391
392 /**
393 * Implementation of hook_insert().
394 */
395 function quiz_insert($node) {
396 quiz_translate_form_date($node, 'quiz_open');
397 quiz_translate_form_date($node, 'quiz_close');
398
399 $tid = (isset($node->tid) ? $node->tid : 0);
400
401 if (!isset($node->has_userpoints)) {
402 $node->has_userporints = 0;
403 }
404
405 $sql = "INSERT INTO {quiz_node_properties}
406 (vid, nid, aid, number_of_random_questions, shuffle,
407 backwards_navigation, quiz_open, quiz_close, takes, time_limit, pass_rate, summary_pass, summary_default, quiz_always, feedback_time, tid, has_userpoints)
408 VALUES(%d, %d, '%s', %d, %d, %d, %d, %d, %d, %d, %d, '%s', '%s', %d, %d, %d, %d)";
409 db_query($sql, $node->vid, $node->nid, $node->aid, $node->number_of_random_questions, $node->shuffle, $node->backwards_navigation, $node->quiz_open, $node->quiz_close, $node->takes, $node->time_limit, $node->pass_rate, $node->summary_pass, $node->summary_default, $node->quiz_always, $node->feedback_time, $node->has_userpoints, $tid);
410 _quiz_insert_resultoptions($node);
411 }
412
413 /**
414 * Implementation of hook_update().
415 */
416 function quiz_update($node) {
417 // Quiz node vid (revision) was updated.
418 if ($node->revision) {
419
420 // Insert a new row in the quiz_node_properties table.
421 quiz_insert($node);
422
423 // Create new quiz-question relation entries in the quiz_node_relationship table.
424 quiz_update_quiz_question_relationship($node->old_vid, $node->vid, $node->nid);
425 }
426
427 // Quiz node vid (revision) was not updated.
428 else {
429 // Update an existing row in the quiz_node_properties table.
430 quiz_translate_form_date($node, 'quiz_open');
431 quiz_translate_form_date($node, 'quiz_close');
432 $sql = "UPDATE {quiz_node_properties}
433 SET vid = %d,
434 aid='%s',
435 shuffle = %d,
436 backwards_navigation = %d,
437 quiz_open = %d,
438 quiz_close = %d,
439 takes = %d,
440 time_limit = '%d',
441 pass_rate = %d,
442 summary_pass = '%s',
443 summary_default = '%s',
444 quiz_always = %d,
445 feedback_time = %d,
446 number_of_random_questions = %d,
447 has_userpoints = %d
448 WHERE vid = %d
449 AND nid = %d";
450 $resource = db_query($sql, $node->vid, $node->aid, $node->shuffle, $node->backwards_navigation, $node->quiz_open, $node->quiz_close,
451 $node->takes, $node->time_limit, $node->pass_rate, $node->summary_pass, $node->summary_default, $node->quiz_always,
452 $node->feedback_time, $node->number_of_random_questions, $node->has_userpoints,
453 $node->vid, $node->nid);
454
455 // if (db_affected_rows($resource) == 0) {
456 // drupal_set_message('No quiz was found that could be modified.', 'status');
457 // }
458 }
459 _quiz_update_resultoptions($node);
460 }
461
462 /**
463 * Implementation of hook_delete().
464 */
465 function quiz_delete($node) {
466 // This first line should load all the vid's for the nid.
467 db_query('DELETE FROM {quiz_node_properties} WHERE vid = %d AND nid = %d', $node->vid, $node->nid);
468 db_query('DELETE FROM {quiz_node_relationship} WHERE parent_nid = %d', $node->nid);
469 db_query('DELETE FROM {quiz_node_results} WHERE vid = %d AND nid = %d', $node->vid, $node->nid);
470 db_query('DELETE FROM {quiz_node_result_options} WHERE vid = %d AND nid = %d', $node->vid, $node->nid);
471 }
472
473 function _quiz_get_node_defaults() {
474 return array(
475 'property_id' => NULL,
476 'aid' => NULL,
477 'number_of_random_questions' => 0,
478 'pass_rate' => 75,
479 'summary_pass' => '',
480 'summary_default' => '',
481 'shuffle' => 0,
482 'backwards_navigation' => 0,
483 'feedback_time' => 0,
484 'quiz_open' => 0,
485 'quiz_close' => 0,
486 'takes' => 0,
487 'time_limit' => 0,
488 'quiz_always' => 0,
489 'tid' => 0,
490 'has_userpoints' => 0,
491 );
492 }
493
494 /**
495 * Implementation of hook_load().
496 */
497 function quiz_load($node) {
498
499 $default_additions = _quiz_get_node_defaults();
500 $fields = implode(', ', array_keys($default_additions));
501
502 $quiz_vid = $node->vid;
503 $sql = 'SELECT %s FROM {quiz_node_properties} WHERE vid = %d AND nid = %d ORDER BY property_id DESC';
504 $fetched_additions = db_fetch_array(db_query($sql, $fields, $quiz_vid, $node->nid));
505
506 $additions = ($fetched_additions) ? (object)($fetched_additions += $default_additions) : NULL;
507
508 /*
509 * This doesn't appear to have ever worked.... It just adds an empty item to $additions->status.
510 * Also, I can't find where this information is ever used, so there's probably no point in fixing it.
511 $results = db_query('SELECT nr.nid, qnr.question_status, qnr.child_nid
512 FROM {quiz_node_relationship} qnr
513 INNER JOIN {node_revisions} nr ON (qnr.parent_vid = nr.vid AND qnr.parent_nid = nr.nid)
514 WHERE qnr.parent_vid = %d AND qnr.parent_nid = %d', $quiz_vid, $node->nid);
515
516 while ($question = db_fetch_object($results)) {
517 $additions->question_status[$question->child_nid] = $question->status;
518 $additions->status[$question->child_nid] = $question->status;
519
520 }*/
521
522 $result_options = db_query('SELECT * FROM {quiz_node_result_options} WHERE nid = %d AND vid= %d', $node->nid, $node->vid);
523 while ($option = db_fetch_array($result_options)) {
524 $additions->resultoptions[$option['option_id']] = $option;
525 }
526
527 return $additions;
528 }
529
530 /**
531 * Implementation of hook_view().
532 */
533 function quiz_view($node, $teaser = FALSE, $page = FALSE) {
534 drupal_alter('quiz_view', $node, $teaser, $page);
535 if (!$teaser && $page) {
536 //load the questions in the view page
537 //$node->content['body']['#value'] = quiz_take_quiz($node);
538 $node->content = quiz_take_quiz($node);
539 }
540 else {
541 $node = node_prepare($node, $teaser);
542 }
543 return $node;
544 }
545
546 // QUIZ FORM
547
548 /**
549 * Implementation of hook_form().
550 *
551 * This is an admin form used to build a new quiz. It is called as part of the node edit form.
552 */
553 function quiz_form(&$node) {
554 $form = array();
555 $form['title'] = array(
556 '#type' => 'textfield',
557 '#title' => t('Title'),
558 '#default_value' => $node->title,
559 '#description' => t('The name of the @quiz.', array('@quiz' => QUIZ_NAME)),
560 '#required' => TRUE,
561 );
562
563 $form['body_field']['body'] = array(
564 '#type' => 'textarea',
565 '#title' => t('Description'),
566 '#default_value' => $node->body,
567 '#description' => t('A description of what the @quiz entails', array('@quiz' => QUIZ_NAME)),
568 '#required' => FALSE,
569 );
570 $form['body_field']['format'] = filter_form($node->format);
571
572 $form['shuffle'] = array(
573 '#type' => 'checkbox',
574 '#title' => t('Shuffle questions'),
575 '#default_value' => (isset($node->shuffle) ? $node->shuffle : 1),
576 '#description' => t('Whether to shuffle/randomize the questions on the @quiz', array('@quiz' => QUIZ_NAME)),
577 );
578
579 /*
580 * Added the action as a dropdown for selection with specific quizzes
581 * This allows you to choose a defined action from the actions module for use when
582 * a user completes the quiz.
583 */
584 $form['aid'] = array(
585 '#title' => t('Assign Action'),
586 '#description' => t('Select an action to be preformed after a user has completed this @quiz.', array('@quiz' => QUIZ_NAME)),
587 '#type' => 'select',
588 /*
589 * An idea here would be to add a system conf variable into the quiz_action_options() function that
590 * could filter the type of actions you could display on your quizzes. For Example: you create
591 * a custom module that defines some actions that you only want a user to choose when creating
592 * a quiz and selecting an action from the dropdown. You setup your actions with type 'quiz' and
593 * then add in that variable into the function and it will automatically filter and show only
594 * those specific actions. @note: In doing this you loose your default "Choose an Action"
595 * option. Review actions and the quiz_action_options() function for further explaination.
596 */
597 '#options' => quiz_action_options(variable_get('quiz_action_type', 'all')),
598 '#default_value' => MD5($node->aid),
599 );
600
601 $form['backwards_navigation'] = array(
602 '#type' => 'checkbox',
603 '#title' => t('Backwards navigation'),
604 '#default_value' => $node->backwards_navigation,
605 '#description' => t('Whether to allow user to go back and revisit their answers'),
606 );
607
608 $form['feedback_time'] = array(
609 '#title' => t('Feedback Time'),
610 '#type' => 'radios',
611 '#default_value' => (isset($node->feedback_time) ? $node->feedback_time : QUIZ_FEEDBACK_END),
612 '#options' => _quiz_get_feedback_options(),
613 '#description' => t('Indicates at what point feedback for each question will be given to the user'),
614 );
615
616 // Set up the availability options.
617 $form['quiz_availability'] = array(
618 '#type' => 'fieldset',
619 '#title' => t('Availability options'),
620 '#collapsed' => FALSE,
621 '#collapsible' => TRUE,
622 );
623 $form['quiz_availability']['quiz_always'] = array(
624 '#type' => 'checkbox',
625 '#title' => t('Always Available'),
626 '#default_value' => $node->quiz_always,
627 '#description' => t('Click this option to ignore the open and close dates.'),
628 );
629 $form['quiz_availability']['quiz_open'] = array(
630 '#type' => 'date',
631 '#title' => t('Open Date'),
632 '#default_value' => _quiz_form_prepare_date($node->quiz_open),
633 '#description' => t('The date this @quiz will become available.', array('@quiz' => QUIZ_NAME)),
634 );
635 $form['quiz_availability']['quiz_close'] = array(
636 '#type' => 'date',
637 '#title' => t('Close Date'),
638 '#default_value' => _quiz_form_prepare_date($node->quiz_close, variable_get('quiz_default_close', 30)),
639 '#description' => t('The date this @quiz will cease to be available.', array('@quiz' => QUIZ_NAME)),
640 );
641
642 $options = array(t('Unlimited'));
643 for ($i = 1; $i < 10; $i++) {
644 $options[$i] = $i;
645 }
646 $form['takes'] = array(
647 '#type' => 'select',
648 '#title' => t('Number of takes'),
649 '#default_value' => $node->takes,
650 '#options' => $options,
651 '#description' => t('The number of times a user is allowed to take the @quiz', array('@quiz' => QUIZ_NAME)),
652 );
653
654 $form['addons'] = array(
655 '#type' => 'fieldset',
656 '#title' => t('Quiz Addons Properties'),
657 '#description' => t('Configure Quiz !url and their Properties', array('!url' => l(t('Addons'), 'admin/quiz/settings', array('query' => array('destination' => $_GET['q']))))),
658 '#collapsible' => TRUE,
659 '#collapsed' => FALSE,
660 );
661
662
663 if (function_exists('jquery_countdown_add') && variable_get('quiz_has_timer', 0)) {
664 $form['addons']['time_limit'] = array(
665 '#type' => 'textfield',
666 '#title' => t(' Time Limit'),
667 '#default_value' => isset($node->time_limit) ? $node->time_limit : 0,
668 '#description' => t('Set the maximum allowed time in seconds for this @quiz. Use 0 for no limit.', array('@quiz' => QUIZ_NAME)),
669 );
670 }
671 else {
672 $form['addons']['time_limit'] = array(
673 '#type' => 'value',
674 '#value' => 0,
675 );
676 }
677
678
679 if (module_exists('userpoints') && variable_get('quiz_has_userpoints', 0)) {
680 $form['addons']['has_userpoints'] = array(
681 '#type' => 'checkbox',
682 '#default_value' => (isset($node->has_userpoints) ? $node->has_userpoints : 1),
683 '#title' => t('Enable UserPoints Module Integration'),
684 '#description' => t('If checked, marks scored in this @quiz will be credited to userpoints. For each correct answer 1 point will be added to user\'s point.', array('@quiz' => QUIZ_NAME)),
685 );
686 }
687
688 // Quiz summary options.
689 $form['summaryoptions'] = array(
690 '#type' => 'fieldset',
691 '#title' => t('@quiz Summary Options', array('@quiz' => QUIZ_NAME)),
692 '#collapsible' => TRUE,
693 '#collapsed' => FALSE,
694 );
695 // If pass/fail option is checked, present the form elements.
696 if (variable_get('quiz_use_passfail', 1)) {
697 // New nodes get the default.
698 if (empty($node->nid)) {
699 $node->pass_rate = variable_get('quiz_default_pass_rate', 75);
700 }
701 $form['summaryoptions']['pass_rate'] = array(
702 '#type' => 'textfield',
703 '#title' => t('Pass rate for @quiz (%)', array('@quiz' => QUIZ_NAME)),
704 '#default_value' => $node->pass_rate,
705 '#description' => t('Pass rate for the @quiz as a percentage score. (For personality quiz enter 0, and use result options.)', array('@quiz' => QUIZ_NAME)),
706 '#required' => FALSE,
707 );
708 $form['summaryoptions']['summary_pass'] = array(
709 '#type' => 'textarea',
710 '#title' => t('Summary text if passed'),
711 '#default_value' => $node->summary_pass,
712 '#cols' => 60,
713 '#description' => t("Summary for when the user gets enough correct answers to pass the @quiz. Leave blank if you don't want to give different summary text if they passed or if you are not using the 'percent to pass' option above. If you don't use the 'Percentage needed to pass' field above, this text will not be used.", array('@quiz' => QUIZ_NAME)),
714 );
715 }
716 // If the pass/fail option is unchecked, use the default and hide it.
717 else {
718 $form['summaryoptions']['pass_rate'] = array(
719 '#type' => 'hidden',
720 '#value' => variable_get('quiz_default_pass_rate', 75),
721 '#required' => FALSE,
722 );
723 }
724 $form['summaryoptions']['summary_default'] = array(
725 '#type' => 'textarea',
726 '#title' => t('Default summary text'),
727 '#default_value' => $node->summary_default,
728 '#cols' => 60,
729 '#description' => t("Default summary. Leave blank if you don't want to give a summary."),
730 );
731
732 $num_rand = (isset($node->number_of_random_questions)) ? $node->number_of_random_questions : 0;
733 $form['number_of_random_questions'] = array(
734 '#type' => 'value',
735 '#value' => $num_rand,
736 );
737
738 $form['resultoptions'] = array(
739 '#type' => 'fieldset',
740 '#title' => t('!quiz Results', array('!quiz' => QUIZ_NAME)),
741 '#collapsible' => TRUE,
742 '#collapsed' => TRUE,
743 '#tree' => TRUE,
744 );
745
746 $options = !empty($node->resultoptions) ? $node->resultoptions : array();
747 $num_options = max(3, (!empty($options)) ? count($options) : variable_get('quiz_max_result_options', 5));
748
749 for ($i=0; $i < $num_options; $i++) {
750 $option = (count($options) > 0) ? array_shift($options) : NULL; // grab each option in the array
751 $form['resultoptions'][$i] = array(
752 '#type' => 'fieldset',
753 '#title' => t('Result Option ') . ($i + 1),
754 '#collapsible' => TRUE,
755 '#collapsed' => FALSE,
756 );
757 $form['resultoptions'][$i]['option_name'] = array(
758 '#type' => 'textfield',
759 '#title' => t('The name of the result'),
760 '#description' => t('Not displayed on personality !quiz.', array('!quiz' => QUIZ_NAME)),
761 '#default_value' => $option['option_name'],
762 '#maxlength' => 40,
763 '#size' => 40,
764 );
765 $form['resultoptions'][$i]['option_start'] = array(
766 '#type' => 'textfield',
767 '#title' => t('Percentage Start Range'),
768 '#description' => t('Show this result for scored quizzes in this range (0-100). Leave blank for personality quizzes.'),
769 '#default_value' => $option['option_start'],
770 '#size' => 5,
771 );
772 $form['resultoptions'][$i]['option_end'] = array(
773 '#type' => 'textfield',
774 '#title' => t('Percentage End Range'),
775 '#description' => t('Show this result for scored quizzes in this range (0-100). Leave blank for personality quizzes.'),
776 '#default_value' => $option['option_end'],
777 '#size' => 5,
778 );
779 $form['resultoptions'][$i]['option_summary'] = array(
780 '#type' => 'textarea',
781 '#title' => t('Display text for the result'),
782 '#default_value' => $option['option_summary'],
783 '#description' => t('Result summary. This is the summary that is displayed when the user falls in this result set determined by his/her responses.'),
784 );
785
786 if ($option['option_id']) {
787 $form['resultoptions'][$i]['option_id'] = array(
788 '#type' => 'hidden',
789 '#value' => $option['option_id'],
790 );
791 }
792 }
793 return $form;
794 }
795
796 /**
797 * Implementation of hook_validate().
798 */
799 function quiz_validate($node) {
800 if (!$node->nid && empty($_POST)) return;
801
802 if (mktime(0, 0, 0, $node->quiz_open['month'], $node->quiz_open['day'], $node->quiz_open['year']) > mktime(0, 0, 0, $node->quiz_close['month'], $node->quiz_close['day'], $node->quiz_close['year'])) {
803 form_set_error('quiz_close', t('Please make sure the close date is after the open date.'));
804 }
805 if (!is_numeric($node->pass_rate)) {
806 form_set_error('pass_rate', t('The pass rate value must be a number between 0% and 100%.'));
807 }
808 if ($node->pass_rate > 100) {
809 form_set_error('pass_rate', t('The pass rate value must not be more than 100%.'));
810 }
811 if ($node->pass_rate < 0) {
812 form_set_error('pass_rate', t('The pass rate value must not be less than 0%.'));
813 }
814
815 if (isset($node->time_limit)) {
816 if ($node->time_limit < 0 || !is_numeric($node->time_limit)) {
817 form_set_error('time_limit', t('Time limit must be a non negative interger '));
818 }
819 }
820
821 $taken_values = array();
822 $num_options =0;
823 foreach ($node->resultoptions as $option) {
824 if (!empty($option['option_name'])) {
825 $num_options++;
826 if (empty($option['option_summary'])) {
827 form_set_error('option_summary', t('Option has no summary text.'));
828 }
829 if ($node->pass_rate && (isset($option['option_start']) || isset($option['option_end']))) {
830
831 // Check for a number between 0-100.
832 foreach (array('option_start' => 'start', 'option_end' => 'end') as $bound => $bound_text) {
833 if (!is_numeric($option[$bound])) {
834 form_set_error($bound, t('The range %start value must be a number between 0% and 100%.', array('%start' => $bound_text)));
835 }
836 if ($option[$bound] < 0) {
837 form_set_error($bound, t('The range %start value must not be less than 0%.', array('%start' => $bound_text)));
838 }
839 if ($option[$bound] > 100) {
840 form_set_error($bound, t('The range %start value must not be more than 100%.', array('%start' => $bound_text)));
841 }
842 }
843
844 // Check that range end >= start.
845 if ($option['option_start'] > $option['option_end']) {
846 form_set_error('option_start', t('The start must be less than the end of the range.'));
847 }
848
849 // Check that range doesn't collide with any other range.
850 $option_range = range($option['option_start'], $option['option_end']);
851 if ($intersect = array_intersect($taken_values, $option_range)) {
852 form_set_error('option_start', t('The ranges must not overlap each other. (%intersect)', array('%intersect' => implode(',', $intersect))));
853 }
854 else {
855 $taken_values = array_merge($taken_values, $option_range);
856 }
857 }
858 }
859 elseif (!empty($option['option_summary'])) {
860 form_set_error('option_summary', t('Option has a summary, but no name.'));
861 }
862 }
863 if ($node->pass_rate == 0 && !$num_options) {
864 form_set_error('pass_rate', t('Unscored quiz, but no result options defined.'));
865 }
866 }
867 /**
868 * Implementation of hook_nodeapi()
869 */
870 function quiz_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
871 // We need to filter on node type to prevent this from overriding any other node
872 if ($node->type == 'quiz') {
873 switch ($op) {
874 case 'presave':
875 /*
876 * convert the action id to the actual id from the MD5 hash
877 * Why the actions module does this I do not know? Maybe to prevent invalid values put
878 * into the options value="" field.
879 */
880 $node->aid = actions_function_lookup($node->aid);
881 break;
882 case 'prepare':
883 // Meet E_STRICT on new nodes.
884 $defaults = _quiz_get_node_defaults();
885 if (!isset($node->nid)) {
886 //drupal_set_message('Building defaults');
887 foreach ($defaults as $key => $value) {
888 if (!isset($node->$key)) {
889 $node->$key = $value;
890 }
891 }
892 }
893 }
894 }
895 /* If we want to pre-process question nodes before they get rendered, here's how to do it:
896 if ($op == 'view' && in_array($node->type, array_keys(_quiz_get_question_types())) && $a3) {
897 drupal_set_message("Do global munging of node content here.");
898 //var_dump($node->content);
899 }
900 if ($op == 'alter' && in_array($node->type, array_keys(_quiz_get_question_types())) && $a3) {
901 drupal_set_message("Do alter of node content here.");
902 //var_dump($node->content);
903 }
904 */
905 }
906
907 // END HOOKS
908
909 /**
910 * Load a quiz node and validate it.
911 *
912 * @param $arg
913 * The Node ID
914 * @return
915 * A quiz node object or FALSE if a load failed.
916 */
917 function quiz_type_access_load($arg) {
918 // Simple verification/load of the node.
919 return (($node = node_load($arg)) && $node->type == 'quiz') ? $node : FALSE;
920 }
921
922 /**
923 * Finds out the number of questions for the quiz.
924 *
925 * Good example of usage could be to calculate the % of score.
926 *
927 * @param $nid
928 * Quiz ID
929 * @return integer
930 * Returns the number of quiz questions.
931 */
932 function quiz_get_number_of_questions($vid, $nid) {
933 // PostgreSQL cannot handle addition of count() columns plus int columns, so we have to do this as two queries:
934 // $sql = 'SELECT COUNT(*) + (SELECT number_of_random_questions FROM {quiz_node_properties} WHERE vid = %d AND nid = %d)
935 // FROM {quiz_node_relationship} qnr
936 // WHERE qnr.parent_vid = %d
937 // AND qnr.parent_nid = %d
938 // AND question_status = %d';
939 // return db_result(db_query($sql, $vid, $nid, $vid, $nid, QUESTION_ALWAYS));
940 $sql = 'SELECT COUNT(*) FROM {quiz_node_relationship} qnr WHERE qnr.parent_vid = %d AND question_status = %d';
941 $always_count = db_result(db_query($sql, $vid, QUESTION_ALWAYS));
942 $rand_count = db_result(db_query('SELECT number_of_random_questions FROM {quiz_node_properties} WHERE vid = %d', $vid));
943 return $always_count + (int)$rand_count;
944 }
945
946 /**
947 * Finds out the pass rate for the quiz.
948 *
949 * @param $nid
950 * The quiz ID.
951 * @return integer
952 * Returns the passing percentage of the quiz.
953 */
954 function quiz_get_pass_rate($nid, $vid) {
955 return db_result(db_query('SELECT pass_rate FROM {quiz_node_properties} WHERE nid = %d AND vid = %d', $nid, $vid));
956 }
957
958 /**
959 * Updates quiz-question relation entries in the quiz_node_relationship table.
960 *
961 * @access public
962 * @param integer $old_quiz_vid
963 * The quiz vid prior to a new revision.
964 * @param integer $new_quiz_vid
965 * The quiz vid of the latest revision.
966 */
967 function quiz_update_quiz_question_relationship($old_quiz_vid, $new_quiz_vid, $quiz_nid) {
968 $sql = "INSERT INTO {quiz_node_relationship} (parent_nid, parent_vid, child_nid, child_vid, question_status)
969 SELECT src.parent_nid, %d, src.child_nid, src.child_vid, src.question_status
970 FROM {quiz_node_relationship} AS src
971 WHERE src.parent_vid = %d AND src.parent_nid = %d AND src.question_status != %d";
972 db_query($sql, $new_quiz_vid, $old_quiz_vid, $quiz_nid, QUESTION_NEVER);
973 }
974
975
976 /**
977 * Handles quiz taking.
978 *
979 * This gets executed when the main quiz node is first loaded.
980 *
981 * @param $quiz
982 * The quiz node.
983 *
984 * @return
985 * HTML output for page.
986 */
987 function quiz_take_quiz($quiz) {
988 global $user;
989 $allow_skipping = TRUE;
990
991 // If no access, fail.
992 if (!user_access('access quiz')) {
993 drupal_access_denied();
994 return;
995 }
996 if (!isset($quiz)) {
997 drupal_not_found();
998 return;
999 }
1000
1001 // If anonymous user and no unique hash, refresh with a unique string to prevent caching.
1002 if (!$quiz->name && arg(4) == NULL) {
1003 drupal_goto('node/'. $quiz->nid .'/quiz/start/'. md5(mt_rand() . time()));
1004 }
1005
1006 if (!isset($_SESSION['quiz_'. $quiz->nid]['quiz_questions'])) {
1007 $rid = _quiz_active_result_id($user->uid, $quiz->nid, $quiz->vid);
1008
1009 // Are we resuming an in-progress quiz?
1010 if ($rid > 0) {
1011 _quiz_resume_existing_quiz($quiz, $user->uid, $rid);
1012 }
1013 // First time running through quiz.
1014 elseif ($rid = quiz_start_actions($quiz)) {
1015 // Create question list.
1016 $questions = quiz_build_question_list($quiz);
1017
1018 if ($questions === FALSE) {
1019 drupal_set_message(t('Not enough random questions were found. Please !add_more_questions before trying to take this @quiz.',
1020 array('@quiz' => QUIZ_NAME, '!add_more_questions' => l(t('add more questions'), 'node/'. arg(1) .'/questions'))), 'error');
1021 return array('body' => array('#value' => ''));
1022 }
1023
1024 if (count($questions) == 0) {
1025 drupal_set_message(t('No questions were found. Please !assign_questions before trying to take this @quiz.',
1026 array('@quiz' => QUIZ_NAME, '!assign_questions' => l(t('assign questions'), 'node/'. arg(1) .'/questions'))), 'error');
1027 return array('body' => array('#value' => ''));
1028 }
1029
1030 // Initialize session variables.
1031 $_SESSION['quiz_'. $quiz->nid]['quiz_questions'] = $questions;
1032 $_SESSION['quiz_'. $quiz->nid]['result_id'] = $rid;
1033 $_SESSION['quiz_'. $quiz->nid]['question_number'] = 0;
1034 $_SESSION['quiz_'. $quiz->nid]['question_start_time'] = time();
1035 $_SESSION['quiz_'. $quiz->nid]['question_duration'] = $quiz->time_limit;
1036 }
1037
1038 else {
1039 return array('body' => array('#value' => ''));
1040 }
1041 }
1042
1043 if (!isset($_POST['op'])) {
1044 // Starting new quiz... Do we need to show instructions here?
1045 }
1046 // Navigate backwards
1047 elseif ($_POST['op'] == t('Back')) {
1048 unset($_POST['tries']);
1049
1050 // We maintain two lists -- previous questions and upcomming questions.
1051 // When we go backward, we pop one from the previous and prepend it to
1052 // the upcomming.
1053 // TODO: This can be maintained more efficiently with a single array of
1054 // all questions and then a pointer to the current question. That makes
1055 // rewinding much easier.
1056 $quiz_id = 'quiz_' . $quiz->nid;
1057 $last_q = array_pop($_SESSION[$quiz_id]['previous_quiz_questions']);
1058 array_unshift($_SESSION[$quiz_id]['quiz_questions'], $last_q);
1059 }
1060 // Check for answer submission.
1061 elseif ($_POST['op'] == t('Submit') || $_POST['op'] == t('Next')) {
1062 if (!isset($_POST['tries'])) {
1063 // Moving skip logic here...
1064
1065 if ($allow_skipping) {
1066 // Advance the question.
1067 $_SESSION['quiz_'. $quiz->nid]['previous_quiz_questions'][] = $_SESSION['quiz_'. $quiz->nid]['quiz_questions'][0];
1068
1069 // Load the last asked question.
1070 $former_question_array = array_shift($_SESSION['quiz_'. $quiz->nid]['quiz_questions']);
1071 $former_question = node_load(array('nid' => $former_question_array['nid']));
1072
1073 // Call hook_skip_question().
1074 $module = quiz_module_for_type($former_question->type);
1075 $result = module_invoke($module, 'skip_question', $former_question, $_SESSION['quiz_'. $quiz->nid]['result_id']);
1076
1077 // Report that the question was skipped:
1078 //quiz_store_question_result($former_question_array['nid'], $former_question_array['vid'], $_SESSION['quiz_'. $quiz->nid]['result_id'], $result);
1079 quiz_store_question_result($result, array('set_msg' => TRUE));
1080 }
1081 else {
1082 drupal_set_message(t('You must select an answer before you can progress to the next question!'), 'error');
1083 }
1084 }
1085 else {
1086 //unset($_SESSION['quiz_'. $quiz->nid]['previous_quiz_questions']);
1087
1088 // Previous quiz questions: Questions that have been asked already. We save a record of all of them
1089 // so that a user can navigate backward all the way to the beginning of the quiz.
1090 $_SESSION['quiz_'. $quiz->nid]['previous_quiz_questions'][] = $_SESSION['quiz_'. $quiz->nid]['quiz_questions'][0];
1091
1092 $former_question_array = array_shift($_SESSION['quiz_'. $quiz->nid]['quiz_questions']);
1093 $former_question = node_load(array('nid' => $former_question_array['nid']));
1094
1095 // Call hook_evaluate_question().
1096 $types = _quiz_get_question_types();
1097 $module = $types[$former_question->type]['module'];
1098 //drupal_set_message($module . ' evaluate_question called');
1099 $result = module_invoke($module, 'evaluate_question', $former_question, $_SESSION['quiz_'. $quiz->nid]['result_id']);
1100
1101 //$result stdClass Object ( [score] => 0 [nid] => 3 [vid] => 3 [rid] => 27 [is_correct] => [is_evaluated] => 1 [is_skipped] => )
1102 quiz_store_question_result($result, array('set_msg' => TRUE));
1103
1104 // Stash feedback in the session, since the $_POST gets cleared.
1105 if ($quiz->feedback_time == QUIZ_FEEDBACK_QUESTION) {
1106 // Invoke hook_get_report().
1107 //$report = module_invoke($former_question->type, 'get_report', $former_question_array['nid'], $former_question_array['vid'], $_SESSION['quiz_'. $quiz->nid]['result_id']);
1108 $report = module_invoke($module, 'get_report', $former_question_array['nid'], $former_question_array['vid'], $_SESSION['quiz_'. $quiz->nid]['result_id']);
1109 $_SESSION['quiz_'. $quiz->nid]['feedback'] = rawurlencode(quiz_get_feedback($quiz, $report));
1110 }
1111 // If anonymous user, refresh url with unique hash to prevent caching.
1112 if (!$user->uid) {
1113 //drupal_goto('node/'. $quiz->nid .'/quiz/start/'. md5(mt_rand() . time())); #460550
1114 drupal_goto('node/' . $quiz->nid, array('quizkey' => md5(mt_rand() . time())));
1115 }
1116 }
1117 }
1118 // Check for a skip.
1119 // XXX: Deprecated. Causing too many UI issues.
1120 elseif ($_POST['op'] == t('Skip') && $allow_skipping) {
1121
1122 // Advance the question.
1123 $_SESSION['quiz_'. $quiz->nid]['previous_quiz_questions'][] = $_SESSION['quiz_'. $quiz->nid]['quiz_questions'][0];
1124
1125 // Load the last asked question.
1126 $former_question_array = array_shift($_SESSION['quiz_'. $quiz->nid]['quiz_questions']);
1127 $former_question = node_load(array('nid' => $former_question_array['nid']));
1128
1129 // Call hook_skip_question().
1130 $module<