book.module 36.3 KB
Newer Older
Dries's avatar
 
Dries committed
1
<?php
2
// $Id$
Dries's avatar
 
Dries committed
3

Dries's avatar
   
Dries committed
4
5
6
7
8
/**
 * @file
 * Allows users to collaboratively author a book.
 */

9
10
11
/**
 * Implementation of hook_node_name().
 */
Dries's avatar
   
Dries committed
12
function book_node_name($node) {
13
  return t('book page');
Dries's avatar
   
Dries committed
14
15
}

16
17
18
/**
 * Implementation of hook_perm().
 */
Dries's avatar
   
Dries committed
19
function book_perm() {
20
  return array('create book pages', 'maintain books', 'edit own book pages');
Dries's avatar
   
Dries committed
21
22
}

23
24
25
/**
 * Implementation of hook_access().
 */
Dries's avatar
   
Dries committed
26
function book_access($op, $node) {
Dries's avatar
   
Dries committed
27
  global $user;
Dries's avatar
   
Dries committed
28

29
  if ($op == 'create') {
Dries's avatar
   
Dries committed
30
31
    // Only registered users can create book pages.  Given the nature
    // of the book module this is considered to be a good/safe idea.
32
    return user_access('create book pages');
Dries's avatar
   
Dries committed
33
34
  }

35
  if ($op == 'update') {
Dries's avatar
   
Dries committed
36
37
38
    // Only registered users can update book pages.  Given the nature
    // of the book module this is considered to be a good/safe idea.
    // One can only update a book page if there are no suggested updates
39
40
    // of that page waiting for approval.  That is, only updates that
    // don't overwrite the current or pending information are allowed.
41

42
43
44
45
46
47
    if ((user_access('maintain books') && !$node->moderate) || ($node->uid == $user->uid && user_access('edit own book pages'))) {
      return TRUE;
    }
    else {
       // do nothing. node-access() will determine further access
    }
Dries's avatar
   
Dries committed
48
  }
Dries's avatar
   
Dries committed
49
50
}

Dries's avatar
   
Dries committed
51
52
53
/**
 * Implementation of hook_link().
 */
Dries's avatar
   
Dries committed
54
function book_link($type, $node = 0, $main = 0) {
Dries's avatar
   
Dries committed
55
56
57

  $links = array();

58
  if ($type == 'node' && isset($node->parent)) {
Dries's avatar
   
Dries committed
59
    if (!$main) {
Dries's avatar
   
Dries committed
60
61
62
      if (book_access('create', $node)) {
        $links[] = l(t('add child page'), "node/add/book/parent/$node->nid");
      }
63
64
      $links[] = l(t('printer-friendly version'), 'book/export/html/'. $node->nid, array('title' => t('Show a printer-friendly version of this book page and its sub-pages.')));
      $links[] = l(t('export as XML'), 'book/export/docbook/'. $node->nid, array('title' => t('Export this book page and its sub-pages as Docbook-like XML.')));
Dries's avatar
   
Dries committed
65
    }
Dries's avatar
   
Dries committed
66
67
  }

Dries's avatar
   
Dries committed
68
  return $links;
Dries's avatar
   
Dries committed
69
70
}

Dries's avatar
   
Dries committed
71
72
73
/**
 * Implementation of hook_menu().
 */
Dries's avatar
   
Dries committed
74
function book_menu($may_cache) {
Dries's avatar
   
Dries committed
75
76
  $items = array();

Dries's avatar
   
Dries committed
77
  if ($may_cache) {
Dries's avatar
   
Dries committed
78
79
    $items[] = array('path' => 'book', 'title' => t('books'),
      'access' => user_access('access content'), 'type' => MENU_NORMAL_ITEM, 'weight' => 5);
Dries's avatar
   
Dries committed
80
    $items[] = array('path' => 'node/add/book', 'title' => t('book page'),
81
      'access' => user_access('create book pages'));
Dries's avatar
   
Dries committed
82
83
84
85
86
87
88
89
90
91
92
93
    $items[] = array('path' => 'admin/node/book', 'title' => t('books'),
      'callback' => 'book_admin',
      'access' => user_access('administer nodes'),
      'weight' => 4);
    $items[] = array('path' => 'admin/node/book/orphan', 'title' => t('orphan pages'),
      'callback' => 'book_admin_orphan',
      'access' => user_access('administer nodes'),
      'weight' => 8);
    $items[] = array('path' => 'book', 'title' => t('books'),
      'callback' => 'book_render',
      'access' => user_access('access content'),
      'type' => MENU_SUGGESTED_ITEM);
94
95
96
97
98
99
100
101
    $items[] = array(
      'path' => 'book/export/docbook',
      'title' => t('export XML'),
      'callback' => 'book_export_docbook',
      'access' => user_access('access content'),
      'type' => MENU_CALLBACK);
    $items[] = array('path' => 'book/export/printer', 'title' => t('printer-friendly version'),
      'callback' => 'book_export_html',
Dries's avatar
   
Dries committed
102
103
      'access' => user_access('access content'),
      'type' => MENU_CALLBACK);
Dries's avatar
   
Dries committed
104
  }
Dries's avatar
   
Dries committed
105
106
107
108
109
  else {
    // To avoid SQL overhead, check whether we are on a node page and whether the
    // user is allowed to maintain books.
    if (arg(0) == 'node' && is_numeric(arg(1)) && user_access('maintain books')) {
      // Only add the outline-tab for non-book pages:
110
      $result = db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.nid = %d AND n.type != 'book'"), arg(1));
Dries's avatar
   
Dries committed
111
112
113
114
115
116
117
      if (db_num_rows($result) > 0) {
        $items[] = array('path' => 'node/'. arg(1) .'/outline', 'title' => t('outline'),
          'callback' => 'book_outline', 'access' => user_access('maintain books'),
          'type' => MENU_LOCAL_TASK, 'weight' => 2);
      }
    }
  }
Dries's avatar
   
Dries committed
118
119
120
121

  return $items;
}

122
123
124
/**
 * Implementation of hook_block().
 *
Dries's avatar
   
Dries committed
125
126
 * Displays the book table of contents in a block when the current page is a
 * single-node view of a book node.
127
 */
Dries's avatar
   
Dries committed
128
function book_block($op = 'list', $delta = 0) {
Dries's avatar
   
Dries committed
129
  $block = array();
Dries's avatar
   
Dries committed
130
131
  if ($op == 'list') {
    $block[0]['info'] = t('Book navigation');
132
    return $block;
Dries's avatar
   
Dries committed
133
  }
134
  else if ($op == 'view') {
Dries's avatar
   
Dries committed
135
136
    // Only display this block when the user is browsing a book:
    if (arg(0) == 'node' && is_numeric(arg(1))) {
137
      $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.nid = %d'), arg(1));
Dries's avatar
   
Dries committed
138
139
140
141
142
143
144
145
146
147
148
      if (db_num_rows($result) > 0) {
        $node = db_fetch_object($result);

        $path = book_location($node);
        $path[] = $node;

        $expand = array();
        foreach ($path as $key => $node) {
          $expand[] = $node->nid;
        }

149
        $block['subject'] = check_plain($path[0]->title);
Dries's avatar
   
Dries committed
150
151
152
        $block['content'] = book_tree($expand[0], 5, $expand);
      }
    }
Dries's avatar
   
Dries committed
153

154
155
    return $block;
  }
Dries's avatar
   
Dries committed
156
157
}

158
159
160
/**
 * Implementation of hook_load().
 */
Dries's avatar
   
Dries committed
161
function book_load($node) {
Dries's avatar
   
Dries committed
162
  global $user;
Dries's avatar
   
Dries committed
163

164
  $book = db_fetch_object(db_query('SELECT parent, weight, log FROM {book} WHERE nid = %d', $node->nid));
Dries's avatar
   
Dries committed
165

Dries's avatar
   
Dries committed
166
  if (arg(2) == 'edit' && !user_access('administer nodes')) {
Dries's avatar
   
Dries committed
167
168
    // If a user is about to update a book page, we overload some
    // fields to reflect the changes.
Dries's avatar
   
Dries committed
169
170
171
172
173
174
    if ($user->uid) {
      $book->uid = $user->uid;
      $book->name = $user->name;
    }
    else {
      $book->uid = 0;
175
      $book->name = '';
Dries's avatar
   
Dries committed
176
    }
Dries's avatar
   
Dries committed
177
  }
Dries's avatar
   
Dries committed
178

Dries's avatar
   
Dries committed
179
  return $book;
Dries's avatar
   
Dries committed
180
181
}

182
183
184
/**
 * Implementation of hook_insert().
 */
Dries's avatar
   
Dries committed
185
function book_insert($node) {
186
  db_query("INSERT INTO {book} (nid, parent, weight, log) VALUES (%d, %d, %d, '%s')", $node->nid, $node->parent, $node->weight, $node->log);
Dries's avatar
   
Dries committed
187
}
Dries's avatar
   
Dries committed
188

189
190
191
/**
 * Implementation of hook_update().
 */
Dries's avatar
   
Dries committed
192
function book_update($node) {
193
  db_query("UPDATE {book} SET parent = %d, weight = %d, log = '%s' WHERE nid = %d", $node->parent, $node->weight, $node->log, $node->nid);
Dries's avatar
   
Dries committed
194
}
Dries's avatar
   
Dries committed
195

196
197
198
/**
 * Implementation of hook_delete().
 */
199
function book_delete(&$node) {
200
  db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
Dries's avatar
   
Dries committed
201
202
}

203
204
205
/**
 * Implementation of hook_validate().
 */
206
function book_validate(&$node) {
207
208
  // Set default values for non-administrators.
  if (!user_access('administer nodes')) {
209
210
211
    $node->weight = 0;
    $node->revision = 1;
  }
Dries's avatar
   
Dries committed
212
213
}

214
215
216
/**
 * Implementation of hook_form().
 */
Dries's avatar
   
Dries committed
217
function book_form(&$node) {
Dries's avatar
   
Dries committed
218
  global $user;
Dries's avatar
   
Dries committed
219

220
  $op = $_POST['op'];
Dries's avatar
   
Dries committed
221

Dries's avatar
   
Dries committed
222
  $output = form_select(t('Parent'), 'parent', ($node->parent ? $node->parent : arg(4)), book_toc($node->nid), t('The parent that this page belongs in. Note that pages whose parent is &lt;top-level&gt; are regarded as independent, top-level books.'));
Dries's avatar
   
Dries committed
223

224
225
  if (function_exists('taxonomy_node_form')) {
    $output .= implode('', taxonomy_node_form('book', $node));
226
227
  }

228
  $output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, '', NULL, TRUE);
229
  $output .= filter_form('format', $node->format);
Dries's avatar
   
Dries committed
230
  $output .= form_textarea(t('Log message'), 'log', $node->log, 60, 5, t('An explanation of the additions or updates being made to help other authors understand your motivations.'));
Dries's avatar
   
Dries committed
231

232
  if (user_access('administer nodes')) {
Dries's avatar
   
Dries committed
233
    $output .= form_weight(t('Weight'), 'weight', $node->weight, 15, t('Pages at a given level are ordered first by weight and then by title.'));
Dries's avatar
   
Dries committed
234
235
  }
  else {
Dries's avatar
   
Dries committed
236
237
    // If a regular user updates a book page, we create a new revision
    // authored by that user:
238
    $output .= form_hidden('revision', 1);
Dries's avatar
   
Dries committed
239
240
241
242
243
  }

  return $output;
}

244
/**
Dries's avatar
   
Dries committed
245
246
 * Implementation of function book_outline()
 * Handles all book outline operations.
247
 */
Dries's avatar
   
Dries committed
248
function book_outline() {
Dries's avatar
   
Dries committed
249

250
251
  $op = $_POST['op'];
  $edit = $_POST['edit'];
Dries's avatar
   
Dries committed
252
  $node = node_load(array('nid' => arg(1)));
Dries's avatar
   
Dries committed
253

Dries's avatar
   
Dries committed
254
255
256
257
  if ($node->nid) {
    switch ($op) {
      case t('Add to book outline'):
        db_query('INSERT INTO {book} (nid, parent, weight) VALUES (%d, %d, %d)', $node->nid, $edit['parent'], $edit['weight']);
258
        drupal_set_message(t('The post has been added to the book.'));
Dries's avatar
   
Dries committed
259
260
261
262
263
        drupal_goto("node/$node->nid");
        break;

      case t('Update book outline'):
        db_query('UPDATE {book} SET parent = %d, weight = %d WHERE nid = %d', $edit['parent'], $edit['weight'], $node->nid);
264
        drupal_set_message(t('The book outline has been updated.'));
Dries's avatar
   
Dries committed
265
266
267
268
269
        drupal_goto("node/$node->nid");
        break;

      case t('Remove from book outline'):
        db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
270
        drupal_set_message(t('The post has been removed from the book.'));
Dries's avatar
   
Dries committed
271
272
273
274
275
276
277
        drupal_goto("node/$node->nid");
        break;

      default:
        $page = db_fetch_object(db_query('SELECT * FROM {book} WHERE nid = %d', $node->nid));

        $output  = form_select(t('Parent'), 'parent', $page->parent, book_toc($node->nid), t('The parent page in the book.'));
Dries's avatar
   
Dries committed
278
        $output .= form_weight(t('Weight'), 'weight', $node->weight, 15, t('Pages at a given level are ordered first by weight and then by title.'));
Dries's avatar
   
Dries committed
279
280
281
282
283
284
285
286

        if ($page->nid) {
          $output .= form_submit(t('Update book outline'));
          $output .= form_submit(t('Remove from book outline'));
        }
        else {
          $output .= form_submit(t('Add to book outline'));
        }
Dries's avatar
   
Dries committed
287

288
        drupal_set_title(check_plain($node->title));
Dries's avatar
   
Dries committed
289
        return form($output);
Dries's avatar
   
Dries committed
290
291
292
293
    }
  }
}

Dries's avatar
   
Dries committed
294

295
296
297
/**
 * Return the the most recent revision that matches the specified conditions.
 */
Dries's avatar
   
Dries committed
298
299
300
301
302
303
function book_revision_load($page, $conditions = array()) {

  $revisions = array_reverse(node_revision_list($page));

  foreach ($revisions as $revision) {

Dries's avatar
   
Dries committed
304
    // Extract the specified revision:
Dries's avatar
   
Dries committed
305
306
    $node = node_revision_load($page, $revision);

Dries's avatar
   
Dries committed
307
308
    // Check to see if the conditions are met:
    $status = TRUE;
Dries's avatar
   
Dries committed
309
310
311

    foreach ($conditions as $key => $value) {
      if ($node->$key != $value) {
Dries's avatar
   
Dries committed
312
        $status = FALSE;
Dries's avatar
   
Dries committed
313
314
315
316
317
318
319
320
321
      }
    }

    if ($status) {
      return $node;
    }
  }
}

322
323
324
/**
 * Return the path (call stack) to a certain book page.
 */
Dries's avatar
   
Dries committed
325
function book_location($node, $nodes = array()) {
326
  $parent = db_fetch_object(db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.nid = %d'), $node->parent));
Dries's avatar
   
Dries committed
327
328
329
330
331
332
333
  if ($parent->title) {
    $nodes = book_location($parent, $nodes);
    array_push($nodes, $parent);
  }
  return $nodes;
}

334
335
336
/**
 * Accumulates the nodes up to the root of the book from the given node in the $nodes array.
 */
Dries's avatar
   
Dries committed
337
function book_location_down($node, $nodes = array()) {
338
  $last_direct_child = db_fetch_object(db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d ORDER BY b.weight DESC, n.title DESC'), $node->nid));
Dries's avatar
   
Dries committed
339
340
341
342
343
344
345
  if ($last_direct_child) {
    array_push($nodes, $last_direct_child);
    $nodes = book_location_down($last_direct_child, $nodes);
  }
  return $nodes;
}

346
/**
347
 * Fetches the node object of the previous page of the book.
348
 */
Dries's avatar
   
Dries committed
349
function book_prev($node) {
Dries's avatar
   
Dries committed
350
  // If the parent is zero, we are at the start of a book so there is no previous.
Dries's avatar
   
Dries committed
351
352
353
354
  if ($node->parent == 0) {
    return NULL;
  }

Dries's avatar
   
Dries committed
355
  // Previous on the same level:
356
  $direct_above = db_fetch_object(db_query(db_rewrite_sql("SELECT n.nid, n.title FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d AND n.status = 1 AND n.moderate = 0 AND (b.weight < %d OR (b.weight = %d AND n.title < '%s')) ORDER BY b.weight DESC, n.title DESC"), $node->parent, $node->weight, $node->weight, $node->title));
Dries's avatar
   
Dries committed
357
  if ($direct_above) {
Dries's avatar
   
Dries committed
358
    // Get last leaf of $above.
Dries's avatar
   
Dries committed
359
    $path = book_location_down($direct_above);
Dries's avatar
   
Dries committed
360
361

    return $path ? (count($path) > 0 ? array_pop($path) : NULL) : $direct_above;
Dries's avatar
   
Dries committed
362
363
  }
  else {
Dries's avatar
   
Dries committed
364
    // Direct parent:
365
    $prev = db_fetch_object(db_query(db_rewrite_sql('SELECT n.nid, n.title FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.nid = %d AND n.status = 1 AND n.moderate = 0'), $node->parent));
Dries's avatar
   
Dries committed
366
367
368
369
    return $prev;
  }
}

370
/**
371
 * Fetches the node object of the next page of the book.
372
 */
Dries's avatar
   
Dries committed
373
374
function book_next($node) {
  // get first direct child
375
  $child = db_fetch_object(db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d AND n.status = 1 AND n.moderate = 0 ORDER BY b.weight ASC, n.title ASC'), $node->nid));
Dries's avatar
   
Dries committed
376
377
378
379
  if ($child) {
    return $child;
  }

Dries's avatar
   
Dries committed
380
381
  // No direct child: get next for this level or any parent in this book.
  array_push($path = book_location($node), $node); // Path to top-level node including this one.
382

Dries's avatar
   
Dries committed
383
  while (($leaf = array_pop($path)) && count($path)) {
384
    $next = db_fetch_object(db_query(db_rewrite_sql("SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d AND n.status = 1 AND n.moderate = 0 AND (b.weight > %d OR (b.weight = %d AND n.title > '%s')) ORDER BY b.weight ASC, n.title ASC"), $leaf->parent, $leaf->weight, $leaf->weight, $leaf->title));
Dries's avatar
   
Dries committed
385
386
387
388
389
390
    if ($next) {
      return $next;
    }
  }
}

391
392
393
394
395
396
/**
 * Returns the content of a given node.  If $teaser if true, returns
 * the teaser rather than full content.  Displays the most recently
 * approved revision of a node (if any) unless we have to display this
 * page in the context of the moderation queue.
 */
Dries's avatar
   
Dries committed
397
function book_content($node, $teaser = FALSE) {
398
  $op = $_POST['op'];
Dries's avatar
   
Dries committed
399

Dries's avatar
   
Dries committed
400
401
402
  // Always display the most recently approved revision of a node
  // (if any) unless we have to display this page in the context of
  // the moderation queue.
403
404
  if ($op != t('Preview') && $node->moderate && arg(0) != 'queue') {
    $revision = book_revision_load($node, array('moderate' => 0, 'status' => 1));
Dries's avatar
   
Dries committed
405
406
407
408

    if ($revision) {
      $node = $revision;
    }
Dries's avatar
   
Dries committed
409
410
  }

411
412
  // Extract the page body.
  $node = node_prepare($node, $teaser);
Dries's avatar
   
Dries committed
413

Dries's avatar
   
Dries committed
414
415
416
  return $node;
}

417
418
419
420
421
422
/**
 * Implementation of hook_view().
 *
 * If not displayed on the main page, we render the node as a page in the
 * book with extra links to the previous and next pages.
 */
Dries's avatar
   
Dries committed
423
function book_view(&$node, $teaser = FALSE, $page = FALSE) {
Dries's avatar
   
Dries committed
424
  $node = book_content($node, $teaser);
Dries's avatar
 
Dries committed
425

Dries's avatar
   
Dries committed
426
427
  if (!$teaser && $node->moderate) {
    $node->body .= '<div class="log"><div class="title">'. t('Log') .':</div>'. $node->log .'</div>';
Dries's avatar
   
Dries committed
428
429
430
  }
}

431
/**
Dries's avatar
   
Dries committed
432
433
434
 * Implementation of hook_nodeapi().
 *
 * Appends book navigation to all nodes in the book.
435
 */
Dries's avatar
   
Dries committed
436
437
438
439
function book_nodeapi(&$node, $op, $teaser, $page) {
  switch ($op) {
    case 'view':
      if (!$teaser) {
Dries's avatar
   
Dries committed
440
        $book = db_fetch_array(db_query('SELECT * FROM {book} WHERE nid = %d', $node->nid));
Dries's avatar
   
Dries committed
441
        if ($book) {
442
          if ($node->moderate && user_access('administer nodes')) {
443
            drupal_set_message(t("The post has been submitted for moderation and won't be accessible until it has been approved."));
444
445
          }

Dries's avatar
   
Dries committed
446
447
448
          foreach ($book as $key => $value) {
            $node->$key = $value;
          }
449
          $node = theme('book_navigation', $node);
450
451
452
          if ($page) {
            menu_set_location($node->breadcrumb);
          }
Dries's avatar
   
Dries committed
453
        }
Dries's avatar
   
Dries committed
454
      }
Dries's avatar
   
Dries committed
455
      break;
Dries's avatar
   
Dries committed
456
  }
Dries's avatar
   
Dries committed
457
}
Dries's avatar
   
Dries committed
458

459
460
461
/**
 * Prepares both the custom breadcrumb trail and the forward/backward
 * navigation for a node presented as a book page.
462
463
 *
 * @ingroup themeable
464
 */
465
function theme_book_navigation($node) {
Dries's avatar
   
Dries committed
466
  $path = book_location($node);
Dries's avatar
   
Dries committed
467

Dries's avatar
   
Dries committed
468
  // Construct the breadcrumb:
Dries's avatar
   
Dries committed
469

Dries's avatar
   
Dries committed
470
  $node->breadcrumb = array(); // Overwrite the trail with a book trail.
Dries's avatar
   
Dries committed
471
  foreach ($path as $level) {
Dries's avatar
   
Dries committed
472
    $node->breadcrumb[] = array('path' => 'node/'. $level->nid, 'title' =>  $level->title);
Dries's avatar
   
Dries committed
473
  }
Dries's avatar
   
Dries committed
474
  $node->breadcrumb[] = array('path' => 'node/'. $node->nid);
Dries's avatar
   
Dries committed
475

Dries's avatar
   
Dries committed
476
  if ($node->nid) {
477
    $output .= '<div class="book">';
Dries's avatar
   
Dries committed
478
479

    if ($tree = book_tree($node->nid)) {
Dries's avatar
   
Dries committed
480
      $output .= '<div class="tree">'. $tree .'</div>';
Dries's avatar
   
Dries committed
481
    }
Dries's avatar
   
Dries committed
482

Dries's avatar
   
Dries committed
483
    if ($prev = book_prev($node)) {
484
      $links .= '<div class="prev">';
Dries's avatar
   
Dries committed
485
      $links .= l(t('previous'), 'node/'. $prev->nid, array('title' => t('View the previous page.')));
486
      $links .= '</div>';
487
      $titles .= '<div class="prev">'. check_plain($prev->title) .'</div>';
Dries's avatar
   
Dries committed
488
489
    }
    else {
Dries's avatar
   
Dries committed
490
      $links .= '<div class="prev">&nbsp;</div>'; // Make an empty div to fill the space.
Dries's avatar
   
Dries committed
491
492
    }
    if ($next = book_next($node)) {
493
      $links .= '<div class="next">';
Dries's avatar
   
Dries committed
494
      $links .= l(t('next'), 'node/'. $next->nid, array('title' => t('View the next page.')));
495
      $links .= '</div>';
496
      $titles .= '<div class="next">'. check_plain($next->title) .'</div>';
Dries's avatar
   
Dries committed
497
498
    }
    else {
Dries's avatar
   
Dries committed
499
      $links .= '<div class="next">&nbsp;</div>'; // Make an empty div to fill the space.
Dries's avatar
   
Dries committed
500
501
    }
    if ($node->parent) {
502
      $links .= '<div class="up">';
Dries's avatar
   
Dries committed
503
      $links .= l(t('up'), 'node/'. $node->parent, array('title' => t('View this page\'s parent section.')));
504
      $links .= '</div>';
Dries's avatar
   
Dries committed
505
    }
Dries's avatar
   
Dries committed
506

507
    $output .= '<div class="nav">';
Dries's avatar
   
Dries committed
508
509
    $output .= ' <div class="links">'. $links .'</div>';
    $output .= ' <div class="titles">'. $titles .'</div>';
510
511
    $output .= '</div>';
    $output .= '</div>';
Dries's avatar
   
Dries committed
512
  }
Dries's avatar
   
Dries committed
513

Dries's avatar
   
Dries committed
514
  $node->body = $node->body.$output;
Dries's avatar
   
Dries committed
515

Dries's avatar
   
Dries committed
516
  return $node;
Dries's avatar
   
Dries committed
517
}
Dries's avatar
 
Dries committed
518

519
520
521
/**
 * This is a helper function for book_toc().
 */
522
function book_toc_recurse($nid, $indent, $toc, $children, $exclude) {
Dries's avatar
   
Dries committed
523
524
  if ($children[$nid]) {
    foreach ($children[$nid] as $foo => $node) {
525
526
527
528
      if (!$exclude || $exclude != $node->nid) {
        $toc[$node->nid] = $indent .' '. $node->title;
        $toc = book_toc_recurse($node->nid, $indent .'--', $toc, $children, $exclude);
      }
Dries's avatar
   
Dries committed
529
530
531
532
533
534
    }
  }

  return $toc;
}

535
536
537
/**
 * Returns an array of titles and nid entries of book pages in table of contents order.
 */
538
function book_toc($exclude = 0) {
539
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.status = 1 ORDER BY b.weight, n.title'));
Dries's avatar
   
Dries committed
540

Dries's avatar
   
Dries committed
541
  while ($node = db_fetch_object($result)) {
Dries's avatar
   
Dries committed
542
543
544
545
    if (!$children[$node->parent]) {
      $children[$node->parent] = array();
    }
    array_push($children[$node->parent], $node);
Dries's avatar
   
Dries committed
546
  }
Dries's avatar
   
Dries committed
547

548
549
  $toc = array();

Dries's avatar
   
Dries committed
550
551
  // If the user is an administrator, add the top-level book page;
  // only administrators can start new books.
552
553
  if (user_access('administer nodes')) {
    $toc[0] = '<'. t('top-level') .'>';
Dries's avatar
   
Dries committed
554
555
  }

556
  $toc = book_toc_recurse(0, '', $toc, $children, $exclude);
Dries's avatar
   
Dries committed
557

Dries's avatar
   
Dries committed
558
559
560
  return $toc;
}

561
562
563
/**
 * This is a helper function for book_tree()
 */
Dries's avatar
   
Dries committed
564
function book_tree_recurse($nid, $depth, $children, $unfold = array()) {
Dries's avatar
   
Dries committed
565
  if ($depth > 0) {
Dries's avatar
   
Dries committed
566
567
    if ($children[$nid]) {
      foreach ($children[$nid] as $foo => $node) {
Dries's avatar
   
Dries committed
568
569
570
        if (in_array($node->nid, $unfold)) {
          if ($tree = book_tree_recurse($node->nid, $depth - 1, $children, $unfold)) {
            $output .= '<li class="expanded">';
Dries's avatar
   
Dries committed
571
572
            $output .= l($node->title, 'node/'. $node->nid);
            $output .= '<ul>'. $tree .'</ul>';
Dries's avatar
   
Dries committed
573
574
575
            $output .= '</li>';
          }
          else {
Dries's avatar
   
Dries committed
576
            $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
   
Dries committed
577
578
579
580
          }
        }
        else {
          if ($tree = book_tree_recurse($node->nid, 1, $children)) {
Dries's avatar
   
Dries committed
581
            $output .= '<li class="collapsed">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
   
Dries committed
582
583
          }
          else {
Dries's avatar
   
Dries committed
584
            $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
   
Dries committed
585
          }
Dries's avatar
   
Dries committed
586
        }
Dries's avatar
   
Dries committed
587
588
      }
    }
Dries's avatar
   
Dries committed
589
590
591
592
593
  }

  return $output;
}

594
595
596
597
/**
 * Returns an HTML nested list (wrapped in a menu-class div) representing the book nodes
 * as a tree.
 */
Dries's avatar
   
Dries committed
598
function book_tree($parent = 0, $depth = 3, $unfold = array()) {
599
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.status = 1 AND n.moderate = 0 ORDER BY b.weight, n.title'));
Dries's avatar
   
Dries committed
600

Dries's avatar
   
Dries committed
601
602
603
604
  while ($node = db_fetch_object($result)) {
    $list = $children[$node->parent] ? $children[$node->parent] : array();
    array_push($list, $node);
    $children[$node->parent] = $list;
Dries's avatar
   
Dries committed
605
  }
Dries's avatar
   
Dries committed
606

Dries's avatar
   
Dries committed
607
  if ($tree = book_tree_recurse($parent, $depth, $children, $unfold)) {
608
    return '<div class="menu"><ul>'. $tree .'</ul></div>';
Dries's avatar
   
Dries committed
609
  }
Dries's avatar
   
Dries committed
610
611
}

612
/**
Dries's avatar
Dries committed
613
 * Menu callback; prints a listing of all books.
614
 */
Dries's avatar
   
Dries committed
615
function book_render() {
616
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = 0 AND n.status = 1 AND n.moderate = 0 ORDER BY b.weight, n.title'));
Dries's avatar
   
Dries committed
617

Dries's avatar
   
Dries committed
618
  return $output;
Dries's avatar
   
Dries committed
619
620
}

621
/**
622
 * Menu callback; generates a printer-friendly book page with all descendants.
623
 */
624
function book_export_html($nid = 0, $depth = 1) {
Dries's avatar
   
Dries committed
625
  global $base_url;
Dries's avatar
   
Dries committed
626

627
  $output .= book_recurse($nid, $depth, 'book_node_visitor_print_pre', 'book_node_visitor_print_post');
Dries's avatar
   
Dries committed
628

629
630
  $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
  $html .= '<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">';
Dries's avatar
   
Dries committed
631

632
633
634
635
636
  $html .= "<head>\n<title>". check_plain($node->title) ."</title>\n";
  $html .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
  $html .= '<base href="'. $base_url .'/" />' . "\n";
  $html .= "<style type=\"text/css\">\n@import url(misc/print.css);\n</style>\n";
  $html .= "</head>\n<body>\n". $output . "\n</body>\n</html>\n";
Dries's avatar
   
Dries committed
637

638
639
  print $html;
}
Dries's avatar
   
Dries committed
640

641
642
643
644
645
646
647
648
649
650
/**
 * Menu callback; generates XML output of entire book hierarchy beneath
 * the given node.
 */
function book_export_docbook($nid = 0, $depth = 1) {
  $xml = "<?xml version='1.0'?>\n";
  $xml .= "<book>\n";
  $xml .= book_recurse($nid, $depth, 'book_node_visitor_xml_pre', 'book_node_visitor_xml_post');
  $xml .= "</book>\n";
  print $xml;
Dries's avatar
   
Dries committed
651

Dries's avatar
   
Dries committed
652
653
}

654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
/**
 * Traverses the book tree.  Applies the $visit_pre() callback to each
 * node, is called recursively for each child of the node (in weight,
 * title order).  Finally appends the output of the $visit_post()
 * callback to the output before returning the generated output.
 *
 * @param nid
 *  - the node id (nid) of the root node of the book hierarchy.
 * @param depth
 *  - the depth of the given node in the book hierarchy.
 * @param visit_pre
 *  - a function callback to be called upon visiting a node in the tree
 * @param visit_post
 *  - a function callback to be called after visiting a node in the tree,
 *    but before recursively visiting children.
 * @return
 *  - the output generated in visiting each node
 */
function book_recurse($nid = 0, $depth = 1, $visit_pre, $visit_post) {
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.status = 1 AND n.nid = %d AND n.moderate = 0 ORDER BY b.weight, n.title'), $nid);
Dries's avatar
   
Dries committed
674

Dries's avatar
   
Dries committed
675
  while ($page = db_fetch_object($result)) {
Dries's avatar
   
Dries committed
676
    // Load the node:
677
    $node = node_load(array('nid' => $page->nid));
Dries's avatar
   
Dries committed
678

Dries's avatar
   
Dries committed
679
    // Take the most recent approved revision:
Dries's avatar
   
Dries committed
680
    if ($node->moderate) {
681
      $node = book_revision_load($node, array('moderate' => 0, 'status' => 1));
Dries's avatar
   
Dries committed
682
683
    }

Dries's avatar
   
Dries committed
684
    if ($node) {
685
686
      if (function_exists($visit_pre)) {
        $output .= call_user_func($visit_pre, $node, $depth, $nid);
Dries's avatar
   
Dries committed
687
      }
688
689
      else { # default
        $output .= book_node_visitor_print_pre($node, $depth, $nid);
Dries's avatar
   
Dries committed
690
      }
Dries's avatar
   
Dries committed
691

692
693
694
695
696
697
698
699
700
701
702
703
704
      $children = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.status = 1 AND b.parent = %d AND n.moderate = 0 ORDER BY b.weight, n.title'), $node->nid);
      while ($childpage = db_fetch_object($children)) {
          $childnode = node_load(array('nid' => $childpage->nid));
          if ($childnode->nid != $node->nid) {
              $output .= book_recurse($childnode->nid, $depth+1, $visit_pre, $visit_post);
          }
      }
      if (function_exists($visit_post)) {
        $output .= call_user_func($visit_post, $node);
      }
      else { # default
        $output .= book_node_visitor_print_post();
      }
Dries's avatar
   
Dries committed
705
    }
Dries's avatar
   
Dries committed
706
  }
Dries's avatar
   
Dries committed
707

Dries's avatar
   
Dries committed
708
709
  return $output;
}
Dries's avatar
   
Dries committed
710

711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
/**
 * Generates printer-friendly HTML for a node.  This function
 * is a 'pre-node' visitor function for book_recurse().
 *
 * @param $node
 *   - the node to generate output for.
 * @param $depth
 *   - the depth of the given node in the hierarchy. This
 *   is used only for generating output.
 * @param $nid
 *   - the node id (nid) of the given node. This
 *   is used only for generating output.
 * @return
 *   - the HTML generated for the given node.
 */
function book_node_visitor_print_pre($node, $depth, $nid) {
  // Output the content:
  if (node_hook($node, 'content')) {
    $node = node_invoke($node, 'content');
  }
  // Allow modules to change $node->body before viewing.
  node_invoke_nodeapi($node, 'view', $node->body, false);

  $output .= '<div id="node-'.$node->nid. '" class="section-'.$depth.'">'."\n";
  $output .= '<h1 class="book-heading">'. check_plain($node->title) ."</h1>\n";

  if ($node->body) {
    $output .= $node->body;
  }
  return $output;
}

/**
 * Finishes up generation of printer-friendly HTML after visiting a
 * node. This function is a 'post-node' visitor function for
 * book_recurse().
 */
function book_node_visitor_print_post() {
  return "</div>\n";
}

/**
 * Generates XML for a given node. This function is a 'pre-node'
 * visitor function for book_recurse().  The generated XML is
 * DocBook-like - the node's HTML content wrapped in a CDATA
 * processing instruction, and put inside a <literallayout> tag.  The
 * node body has an md5-hash applied; the value of this is stored as
 * node metadata to allow importing code to determine if contents have
 * changed.
 *
 * @param $node
 *   - the node to generate output for.
 * @param $depth
 *   - the depth of the given node in the hierarchy. This
 *   is currently not used.
 * @param $nid
 *   - the node id (nid) of the given node. This
 *   is used only for generating output (e.g., ID attribute)
 * @return
 *   - the generated XML for the given node.
 */
function book_node_visitor_xml_pre($node, $depth, $nid) {
  // Output the content:
  if (node_hook($node, 'content')) {
    $node = node_invoke($node, 'content');
  }
  // Allow modules to change $node->body before viewing.
  node_invoke_nodeapi($node, 'view', $node->body, false);

  $output .= '<section id="node-'.$node->nid .'">'."\n";
  $output .= "<sectioninfo>\n";
  $output .= "<releaseinfo>\n";
  $output .= "md5-hash:" . md5($node->body) . "\n";
  $output .= "weight:". $node->weight . "\n";
  $output .= "</releaseinfo>\n";
  $output .= "</sectioninfo>\n";
  $output .= '<title>'. check_plain($node->title) ."</title>\n";
  // wrap the node body in a CDATA declaration
  $output .= "<literallayout>";
  $output .= "<![CDATA[";
  if ($node->body) {
    $output .= $node->body;
  }
  $output .= "]]>";
  $output .= "</literallayout>\n";
  return $output;
}

/**
 * Completes the XML generated for the node. This
 * function is a 'post-node' visitor function for
 * book_recurse().
 */
function book_node_visitor_xml_post() {
  return "</section>\n";
}

/**
 * Creates a row for the 'admin' view of a book.  Each row represents a page in the book, in the tree representing the book
 */
811
function book_admin_edit_line($node, $depth = 0) {
812
  return array('<div style="padding-left: '. (25 * $depth) .'px;">'. form_textfield(NULL, $node->nid .'][title', $node->title, 64, 255) .'</div>', form_weight(NULL, $node->nid .'][weight', $node->weight, 15), l(t('view'), 'node/'. $node->nid), l(t('edit'), 'node/'. $node->nid .'/edit'), l(t('delete'), 'node/'.$node->nid.'/delete'));
Dries's avatar
   
Dries committed
813
814
}

815
function book_admin_edit_book($nid, $depth = 1) {
816
  $result = db_query(db_rewrite_sql('SELECT n.nid FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d ORDER BY b.weight, n.title'), $nid);
Dries's avatar
   
Dries committed
817

818
819
  $rows = array();

Dries's avatar
   
Dries committed
820
  while ($node = db_fetch_object($result)) {
Dries's avatar
   
Dries committed
821
    $node = node_load(array('nid' => $node->nid));
822
823
    $rows[] = book_admin_edit_line($node, $depth);
    $rows = array_merge($rows, book_admin_edit_book($node->nid, $depth + 1));
Dries's avatar
   
Dries committed
824
825
  }

Dries's avatar
   
Dries committed
826
  return $rows;
Dries's avatar
   
Dries committed
827
828
}

829
830
831
/**
 * Display an administrative view of the hierarchy of a book.
 */
832
833
834
function book_admin_edit($nid, $depth = 0) {
  $node = node_load(array('nid' => $nid));
  if ($node->nid) {
Dries's avatar
   
Dries committed
835
    $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3'));
836
837
    $rows[] = book_admin_edit_line($node);
    $rows = array_merge($rows, book_admin_edit_book($nid));
Dries's avatar
   
Dries committed
838

Dries's avatar
   
Dries committed
839
840
    $output .= theme('table', $header, $rows);
    $output .= form_submit(t('Save book pages'));
Dries's avatar
   
Dries committed
841

842
    drupal_set_title(check_plain($node->title));
Dries's avatar
   
Dries committed
843
844
    return form($output);
  }
845
846
847
  else {
    drupal_not_found();
  }
Dries's avatar
   
Dries committed
848
849
}

850
851
852
/**
 * Saves the changes to a book made by an administrator in the book admin view.
 */
Dries's avatar
   
Dries committed
853
function book_admin_save($nid, $edit = array()) {
Dries's avatar
   
Dries committed
854
  if ($nid) {
Dries's avatar
   
Dries committed
855
    $book = node_load(array('nid' => $nid));
Dries's avatar
   
Dries committed
856

Dries's avatar
   
Dries committed
857
    foreach ($edit as $nid => $value) {
Dries's avatar
   
Dries committed
858
859
860
861
      // Check to see whether the title needs updating:
      $title = db_result(db_query('SELECT title FROM {node} WHERE nid = %d', $nid));
      if ($title != $value['title']) {
        db_query("UPDATE {node} SET title = '%s' WHERE nid = %d", $value['title'], $nid);
Dries's avatar
   
Dries committed
862
      }
Dries's avatar
   
Dries committed
863

Dries's avatar
   
Dries committed
864
865
866
867
      // Check to see whether the weight needs updating:
      $weight = db_result(db_query('SELECT weight FROM {book} WHERE nid = %d', $nid));
      if ($weight != $value['weight']) {
        db_query('UPDATE {book} SET weight = %d WHERE nid = %d', $value['weight'], $nid);
Dries's avatar
   
Dries committed
868
      }
Dries's avatar
   
Dries committed
869
870
    }

871
    $message = t('The book %title has been updated.', array('%title' => theme('placeholder', $book->title)));
872
    watchdog('content', $message);
Dries's avatar
   
Dries committed
873

Dries's avatar
   
Dries committed
874
875
    return $message;
  }
Dries's avatar
   
Dries committed
876
877
}

878
/**
Dries's avatar
Dries committed
879
 * Menu callback; displays a listing of all orphaned book pages.
880
 */
Dries's avatar
   
Dries committed
881
function book_admin_orphan() {
882
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, n.status, b.parent FROM {node} n INNER JOIN {book} b ON n.nid = b.nid'));
Dries's avatar
   
Dries committed
883
884
885
886
887

  while ($page = db_fetch_object($result)) {
    $pages[$page->nid] = $page;
  }

Dries's avatar
   
Dries committed
888
  if ($pages) {
889
    $output .= '<h3>'. t('Orphan pages') .'</h3>';
Dries's avatar
   
Dries committed
890
    $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3'));
Dries's avatar
   
Dries committed
891
892
    foreach ($pages as $nid => $node) {
      if ($node->parent && empty($pages[$node->parent])) {
893
894
        $rows[] = book_admin_edit_line($node, $depth);
        $rows = array_merge($rows, book_admin_edit_book($node->nid, $depth + 1));
Dries's avatar
   
Dries committed
895
      }
Dries's avatar
   
Dries committed
896
    }
Dries's avatar
   
Dries committed
897
    $output .= theme('table', $header, $rows);
Dries's avatar
   
Dries committed
898
899
  }

Dries's avatar
   
Dries committed
900
  return $output;
Dries's avatar
   
Dries committed
901
902
}

903
/**
Dries's avatar
Dries committed
904
 * Menu callback; displays the book administration page.
905
 */
Dries's avatar
Dries committed
906
function book_admin($nid = 0) {
Dries's avatar
   
Dries committed
907
908
  $op = $_POST['op'];
  $edit = $_POST['edit'];
Dries's avatar
   
Dries committed
909

910
911
  if ($op == t('Save book pages')) {
    drupal_set_message(book_admin_save($nid, $edit));
Dries's avatar
   
Dries committed
912
  }
913
914
915
916
917
918
919
920
921

  if ($nid) {
    return book_admin_edit($nid);
  }
  else {
    return book_admin_overview();
  }
}

922
923
924
/**
 * Returns an administrative overview of all books.
 */
925
926
927
928
929
930
931
932
function book_admin_overview() {
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = 0 ORDER BY b.weight, n.title'));
  while ($book = db_fetch_object($result)) {
    $rows[] = array(l($book->title, "node/$book->nid"), l(t('outline'), "admin/node/book/$book->nid"));
  }
  $headers = array(t('Book'), t('Operations'));

  return theme('table', $headers, $rows);
Dries's avatar
   
Dries committed
933
934
}

935
936
937
/**
 * Implementation of hook_help().
 */
Dries's avatar
   
Dries committed
938
function book_help($section) {
Dries's avatar
   
Dries committed
939
  switch ($section) {
Dries's avatar
   
Dries committed
940
    case 'admin/help#book':
Dries's avatar
   
Dries committed
941
      return t("
Dries's avatar
   
Dries committed
942
      <p>The book organises content into a nested hierarchical structure. It is particularly good for manuals, Frequently Asked Questions (FAQs) and the like, allowing you to have chapters, sections, etc.</p>
943
      <p>A book is simply a collection of nodes that have been linked together. These nodes are usually of type <em>book page</em>, but you can insert nodes of any type into a book outline. Every node in the book has a <em>parent</em> node which  \"contains\" it. This is how book.module establishes its hierarchy. At any given level in the hierarchy, a book can contain many nodes. All these sibling nodes are sorted according to the <em>weight</em> that you give them.</p>
944
      <p>Book pages contain a <em>log message</em> field which helps your users understand the motivation behind an edit of a book page. Each edited version of a book page is stored as a new revision of a node. This capability makes it easy to revert to an old version of a page, should that be desirable.</p>
Dries's avatar
   
Dries committed
945
      <p>Like other node types, book submissions and edits may be subject to moderation, depending on your configuration.  Similarly, books use <a href=\"%permissions\">permissions</a> to determine who may read and write to them. Only administrators are allowed to create new books, which are really just nodes whose parent is <em>&lt;top-level&gt;</em>.  To include an existing node in your book, click on the \"outline\"-tab on the node's page.  This enables you to place the node wherever you'd like within the book hierarchy. To add a new node into your book, use the <a href=\"%create\">create content &raquo; book page</a> link.</p>
946
      <p>Administrators may review the hierarchy of their books by clicking on the <a href=\"%collaborative-book\">collaborative book</a> link in the administration pages. There, nodes may be edited, reorganized, removed from book, and deleted. This behavior may change in the future. When a parent node is deleted, it may leave behind child nodes.  These nodes are now <em>orphans</em>. Administrators should periodically <a href=\"%orphans-book\">review their books for orphans</a> and reaffiliate those pages as desired. Finally, administrators may also <a href=\"%export-book\">export their books</a> to a single, flat HTML page which is suitable for printing.</p>
Dries's avatar
   
Dries committed
947
948
      <h3>Maintaining a FAQ using a collaborative book</h3>
      <p>Collaborative books let you easily set up a Frequently Asked Questions (FAQ) section on your web site. The main benefit is that you don't have to write all the questions/answers by yourself - let the community do it for you!</p>
949
      <p>In order to set up the FAQ, you have to create a new book which will hold all your content. To do so, click on the <a href=\"%create\">create content &raquo; book page</a> link. Give it a thoughtful title, and body. A title like \"Estonia Travel - FAQ\" is nice. You may always edit these fields later. You will probably want to designate <em>&lt;top-level&gt;</em> as the parent of this page. Leave the <em>log message</em> and <em>type</em> fields blank for now. After you have submitted this book page, you are ready to begin filling up your book with questions that are frequently asked.</p>
950
      <p>Whenever you come across a post which you want to include in your FAQ, click on the <em>administer</em> link. Then click on the <em>edit book outline</em> button at the bottom of the page. Then place the relevant post wherever is most appropriate in your book by selecting a <em>parent</em>. Books are quite flexible. They can have sections like <em>Flying to Estonia</em>, <em>Eating in Estonia</em> and so on. As you get more experienced with the book module, you can reorganize posts in your book so that it stays organized.</p>
951
      <p>Notes:</p><ul><li>Any comments attached to those relevant posts which you designate as book pages will also be transported into your book. This is a great feature, since much wisdom is shared via comments. Remember that all future comments and edits will automatically be reflected in your book.</li><li>You may wish to edit the title of posts when adding them to your FAQ. This is done on the same page as the <em>Edit book outline</em> button. Clear titles improve navigability enormously.</li><li>Book pages may come from any content type (blog, story, page, etc.). If you are creating a post solely for inclusion in your book, then use the <a href=\"%create\">create content &raquo; book page</a> link.</li><li>If you don't see the <em>administer</em> link, then you probably have insufficient <a href=\"%permissions\">permissions</a>.</li></ul>", array('%permissions' => url('admin/access/permissions'), "%create" => url('node/add/book'), '%collaborative-book' => url('admin/node/book'), '%orphans-book' => url('admin/node/book/orphan'), '%export-book' => url('book/print')));
Dries's avatar
   
Dries committed
952
    case 'admin/modules#description':
Dries's avatar
   
Dries committed
953
      return t('Allows users to collaboratively author a book.');
Dries's avatar
   
Dries committed
954
    case 'admin/node/book':