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

Contents of /contributions/modules/relatedcontent/relatedcontent.module

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


Revision 1.23 - (show annotations) (download) (as text)
Thu Jan 10 14:51:27 2008 UTC (22 months, 2 weeks ago) by tbarregren
Branch: MAIN
CVS Tags: HEAD
Changes since 1.22: +156 -114 lines
File MIME type: text/x-php
Fixing minor erros in the documentation. Refactoring.
1 <?php
2
3 /* $Id: relatedcontent.module,v 1.16.2.2 2008/01/09 21:41:48 tbarregren Exp $
4 *
5 * Copyright (C) 2007-2008 Thomas Barregren.
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 */
21
22
23 /**
24 * @file
25 * RelatedContent – a Drupal module that Allows privileged users to associate
26 * a node with related nodes that can be displayed along with the node.
27 *
28 * Author:
29 * Thomas Barregren at Webbredaktören <http://drupal.org/user/16678>.
30 */
31
32
33 /******************************************************************************
34 * RELATEDCONTENT API THAT CAN BE USED BY THEMES AND OTHER MODULES
35 ******************************************************************************/
36
37 /**
38 * Gets the id numbers (nid) of nodes that are related to $node.
39 *
40 * @param $node
41 * is the node object, as loaded through node_load(), for which the related
42 * content is requested.
43 * @return
44 * an array of id numbers (nid) of nodes that are related to $node. The array
45 * is sorted as described in the documentation of the module.
46 */
47 function relatedcontent_get_nodes(&$node) {
48 $nodes = $node->nodes; // Keeps $node immutable.
49 asort($nodes, SORT_NUMERIC);
50 return array_keys($nodes);
51 }
52
53 /**
54 * Sets the id numbers (nid) of nodes that are related to $node.
55 *
56 * @param $node
57 * is the node object, as loaded through node_load(), for which the related
58 * content is to be set.
59 * @param $nodes
60 * is an array of id numbers (nid) of nodes that are related to $node. The
61 * array should be sorted as described in the documentation of the module.
62 */
63 function relatedcontent_set_nodes(&$node, $nodes) {
64 $ordinal = -count($nodes);
65 foreach ($nodes as $nid) {
66 $node_nodes[$nid] = $ordinal++;
67 }
68 $node->nodes = $node_nodes;
69 }
70
71 /**
72 * Loads, transform and group the related content of a given node.
73 *
74 * This function takes either a node id or a node object ($node); load those
75 * nodes that contains its related content; transform them by passing them
76 * through a callback function ($content_function), which may take additional
77 * arguments ($content_function_args); and finally group ($output_grouped).
78 *
79 * Example 1: After executing
80 *
81 * $node = 3;
82 * $output = relatedcontent($node);
83 *
84 * $output['all'] is an array of all node objects, as loaded through
85 * node_load(), with related content or the node with the id (3) in $node.
86 *
87 * Example 2: After executing
88 *
89 * $node = node_load(7);
90 * $output = relatedcontent($node, 'type', 'node_view', array(true));
91 *
92 * $output[$type][], where $type is the name of an content type, is an array of
93 * teasers, as provided by node_view(), with related content for the node
94 * with the node object in $node.
95 *
96 * Example 3: Following code print the full bodies of nodes with related
97 * content of the node with id 205. The output is grouped by the authors. Each
98 * group is preceded with a heading stating the author's name.
99 *
100 * $output = relatedcontent(205, 'name', 'node_view', array(false));
101 * foreach ($output as $group => $contents) {
102 * $author = $group ? $group : t('Anonymous');
103 * echo "<h3>$author</h3>";
104 * echo implode('', $contents);
105 * }
106 *
107 * @param $node
108 * is either the id of a node (nid) or a node object as loaded by node_load().
109 * @param $output_grouped
110 * is the name of the node field by which the content should be grouped, e.g.
111 * 'type', 'name', 'uid' and 'status', or FALSE if the output should not be
112 * grouped.
113 * @param $content_function
114 * is a callback function that transforms a node object, passed in as its
115 * first argument, to the desired representation, e.g. the teaser or body.
116 * If omitted or empty, the default transform is just returning the node
117 * object itself. If omitted or empty, grouping s not performed.
118 * @param $content_function_args
119 * is an optional array with values that are passed in as argument 2, 3, and
120 * so forth, when calling the callback function $content_function.
121 * @returns
122 * An array whose keys are the names by which the output should be grouped,
123 * i.e. names of content types or authors, or 'all', depending on
124 * $output_grouped, and whose values are arrays with the return values of
125 * calling $content_function for the nodes with related content.
126 */
127 function relatedcontent($node, $output_grouped = false, $content_function = '', $content_function_args = array()) {
128
129 // Load the node if only the node id was given.
130 if (is_numeric($node)) {
131 $node = node_load($node);
132 }
133
134 // Abort if RelatedContent is disabled for the node's content type.
135 if (!relatedcontent_variable_enabled($node->type)) return;
136
137 // Handle missing arguments.
138 if (!$content_function) {
139 $content_function = create_function('$n', 'return $n;');
140 }
141 if (!isset($content_function_args)) {
142 $content_function_args = array();
143 }
144
145 // Get the job done.
146 return _relatedcontent($node, $output_grouped, $content_function, $content_function_args);
147
148 }
149
150
151 /******************************************************************************
152 * THEMEABLE FUNCTIONS
153 ******************************************************************************/
154
155 /**
156 * Themeable function for the related content.
157 *
158 * @ingroup themeable
159 */
160 function theme_relatedcontent($output, $grouped = null, $teaser = null, $page = null) {
161
162 global $theme_engine;
163
164 // If the current theme engine is PHPTemplate and the current theme has a
165 // RelatedContent template file, process the provided template file, and
166 // return the resulting output. Otherwise, return the default theming.
167 if ($theme_engine == 'phptemplate' && file_exists(path_to_theme() .'/relatedcontent.tpl.php')) {
168 $variables = array('output' => $output, 'grouped' => $grouped, 'teaser' => $teaser, 'page' => $page);
169 $out = _phptemplate_callback('relatedcontent', $variables);
170 }
171 else {
172 foreach ($output as $group => $contents) {
173 $out .= "<div class=\"relatedcontent-nodes $group\">";
174 if ($grouped) {
175 $out .= "<h3>$group</h3>";
176 }
177 $out .= implode('', $contents);
178 $out .= '</div>';
179 }
180 }
181
182 return $out;
183
184 }
185
186 /**
187 * Themeable function for displaying the table with the nodes to select from.
188 *
189 * @ingroup themeable
190 */
191 function theme_relatedcontent_form($form) {
192
193 // Prepare the table for rendering.
194 $header = array(theme('table_select_header_cell'), t('Title'), t('Type'), t('Created'), t('Author'));
195 if (isset($form['title']) && is_array($form['title'])) {
196 foreach (element_children($form['title']) as $nid) {
197 $row = array();
198 $row[] = drupal_render($form['nodes'][$nid]);
199 $row[] = drupal_render($form['title'][$nid]);
200 $row[] = drupal_render($form['name'][$nid]);
201 $row[] = drupal_render($form['created'][$nid]);
202 $row[] = drupal_render($form['username'][$nid]);
203 $rows[] = $row;
204 }
205 }
206 else {
207 $rows[] = array(array('data' => t('No nodes available.'), 'colspan' => count($header)));
208 }
209
210 // Render the form.
211 $output .= drupal_render($form['intro']);
212 $output .= theme('table', $header, $rows);
213 $output .= drupal_render($form);
214
215 return $output;
216
217 }
218
219
220 /******************************************************************************
221 * HOOKS
222 ******************************************************************************/
223
224 /**
225 * Implementation of hook_menu().
226 */
227 function relatedcontent_menu($may_cache) {
228 $items = array();
229 if (!$may_cache) {
230 _relatedcontent_menu_css($items);
231 _relatedcontent_menu_relatedcontent($items);
232 }
233 return $items;
234 }
235
236 /**
237 * Implementation of hook_form_alter().
238 */
239 function relatedcontent_form_alter($form_id, &$form) {
240 if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
241 _relatedcontent_form_alter_node_type($form);
242 }
243 }
244
245 /**
246 * Implementation of hook_nodeapi().
247 */
248 function relatedcontent_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
249 if (relatedcontent_variable_enabled($node->type)) {
250 $function = "_relatedcontent_node_$op";
251 if (function_exists($function)) {
252 return $function($node, $a3, $a4);
253 }
254 }
255 }
256
257 /**
258 * Implementation of hook_help().
259 */
260 function relatedcontent_help($section='admin/help#relatedcontent') {
261 switch ($section) {
262 case 'admin/help#relatedcontent':
263 return _relatedcontent_help_help();
264 }
265 }
266
267
268 /******************************************************************************
269 * IMPLEMENTATION OF THE MENU HOOK
270 ******************************************************************************/
271
272 /**
273 * Add the module's CSS-file.
274 */
275 function _relatedcontent_menu_css(&$items) {
276 $base = drupal_get_path('module', 'relatedcontent');
277 drupal_add_css("$base/relatedcontent.css");
278 }
279
280 /**
281 * If viewing a node of a type for which RelatedContent is enabled,
282 * provide paths and tabs for adding and removing related content of the
283 * node.
284 */
285 function _relatedcontent_menu_relatedcontent(&$items) {
286
287 // Abort if the requested page is not a node.
288 if (arg(0) != 'node' || !is_numeric($nid = arg(1))) return;
289
290 // Abort if the module is not enabled for the content type of the node.
291 if (!relatedcontent_variable_enabled(_relatedcontent_db_content_type($nid))) return;
292
293 // Load the node.
294 $node = node_load($nid);
295
296 // Determine whether the user has right to update the node.
297 $node_access = node_access('update', $node);
298
299 // Add the primary tab for RelatedContent.
300 $items[] = array(
301 'path' => "node/$nid/relatedcontent",
302 'type' => MENU_LOCAL_TASK,
303 'title' => t('RelatedContent'),
304 'callback' => 'drupal_get_form',
305 'callback arguments' => array('_relatedcontent_form_list', $node),
306 'access' => $node_access,
307 'weight' => 5,
308 );
309
310 // Add the secondary tab listing all nodes with related content.
311 // Make it default.
312 $items[] = array(
313 'path' => "node/$nid/relatedcontent/list",
314 'type' => MENU_DEFAULT_LOCAL_TASK,
315 'title' => t('Overview'),
316 'callback' => 'drupal_get_form',
317 'callback arguments' => array('_relatedcontent_form_list', $node),
318 'access' => $node_access,
319 'weight' => -11,
320 );
321
322 // Add the secondary tab listing all nodes provided by the view.
323 $views = _relatedcontent_db_views();
324 $view = relatedcontent_variable_view($node->type);
325 $items[] = array(
326 'path' => "node/$nid/relatedcontent/$view",
327 'type' => MENU_LOCAL_TASK,
328 'title' => $views[$view],
329 'callback' => 'drupal_get_form',
330 'callback arguments' => array('_relatedcontent_form_view', $node, $view),
331 'access' => $node_access,
332 'weight' => 0,
333 );
334
335 }
336
337
338 /******************************************************************************
339 * IMPLEMENTATION OF THE NODEAPI HOOK
340 ******************************************************************************/
341
342 /**
343 * A node is being viewed.
344 */
345 function _relatedcontent_node_view(&$node, $teaser, $page) {
346
347 // Get settings.
348 $output_placing = relatedcontent_variable_output_placing($node->type);
349 $output_teasers = $teaser || relatedcontent_variable_output_teasers($node->type);
350 $output_grouped = relatedcontent_variable_output_grouped($node->type);
351
352 // Abort if output isn't wanted in general, or when viewing a teaser.
353 if (!$output_placing || $teaser && relatedcontent_variable_exclude_teasers($node->type)) return;
354
355 // Get the related content. Abort if there is no content.
356 if (!($output = _relatedcontent($node, $output_grouped, 'node_view', array($output_teasers)))) return;
357
358 // Theme the related content.
359 $output = theme('relatedcontent', $output, $output_grouped, $teaser, $page);
360
361 // Add the themed output to the node's body.
362 switch ($output_placing) {
363 case 'beginning':
364 $node->content['body']['#value'] = $output . $node->content['body']['#value'];
365 break;
366 case 'end':
367 $node->content['body']['#value'] = $node->content['body']['#value'] . $output;
368 break;
369 }
370
371 }
372
373 /**
374 * A node is being loaded.
375 */
376 function _relatedcontent_node_load(&$node) {
377 return array('nodes' => _relatedcontent_db_load($node->nid));
378 }
379
380 /**
381 * A node is being deleted.
382 */
383 function _relatedcontent_node_delete(&$node) {
384 _relatedcontent_db_delete($node->nid);
385 }
386
387
388 /******************************************************************************
389 * IMPLEMENTATION OF THE HELP HOOK
390 *****************************************************************************/
391
392 function _relatedcontent_help_help() {
393
394 // Load the help text.
395 $help = file_get_contents('relatedcontent.help', FILE_USE_INCLUDE_PATH);
396
397 // Abort if we can't find the file.
398 if (!$help) return;
399
400 // Substitute variables and translate.
401 $version = str_replace(array('$Re'.'vision:', ' $'), array('', ''), '$Revision: 1.16.2.2 $');
402 $year = substr('$Date: 2008/01/09 21:41:48 $', 7, 4);
403 $help = t($help, array('!version' => $version, '!year' => $year));
404
405 // Add some style. (This is really dirty, but...)
406 $style = <<<EOT
407 <style type="text/css" media="all">
408 /*<![CDATA[*/
409 code, kbd, pre { padding: 1px; font-family: "Bitstream Vera Sans Mono", Monaco, "Lucida Console", monospace; background-color: #EDF1F3; }
410 /*]]>*/
411 </style>
412 EOT;
413 $help = $style . $help;
414
415 return $help;
416
417 }
418
419
420 /******************************************************************************
421 * ALTER NODE TYPE FORM
422 ******************************************************************************/
423
424 /**
425 * Add RelatedContent settings to the node type forms.
426 */
427 function _relatedcontent_form_alter_node_type(&$form) {
428
429 // Get the node type
430 $type = $form['#node_type']->type;
431
432 // Insert RelatedContent's fieldset into the form.
433 $form['relatedcontent'] = array(
434 '#type' => 'fieldset',
435 '#title' => t('RelatedContent settings'),
436 '#description' => t(
437 'Configure how RelatedContent should work on nodes of the type <em>'. $type .'</em>. For more information, see the !help.',
438 array('!help' => l(t('help page'), 'admin/help/relatedcontent'))
439 ),
440 '#collapsible' => true,
441 '#collapsed' => !relatedcontent_variable_enabled($type),
442 '#weight' => $form['submit']['#weight'],
443 );
444
445 // Ask if related content should be enabled on this content type.
446 $form['relatedcontent']['relatedcontent_enabled'] = array(
447 '#type' => 'checkbox',
448 '#title' => t('Enable'),
449 '#description' => t('Check to enable RelatedContent on nodes of the content type <em>'. $type .'</em>.'),
450 '#default_value' => relatedcontent_variable_enabled($type),
451 );
452
453 // Ask which views, if any, to use as node list in the related content select form.
454 $form['relatedcontent']['relatedcontent_view'] = array(
455 '#type' => 'select',
456 '#title' => t('Source of nodes'),
457 '#description' => t(
458 'Select the view that will provide the nodes to select from. If no view is selected, the table with nodes to select from is disabled. Any previously selected nodes will remain selected.',
459 array('!view' => l('view' , 'admin/build/views'))
460 ),
461 '#options' => _relatedcontent_db_views(),
462 '#default_value' => relatedcontent_variable_view($type),
463 );
464
465 // Ask how many nodes to list in the related content select form.
466 $form['relatedcontent']['relatedcontent_length'] = array(
467 '#type' => 'select',
468 '#title' => t('Length of node table'),
469 '#description' => t('The number of nodes to shown on each page of the table with nodes to select from.'),
470 '#options' => array(10 => 10, 20 => 20, 50 => 50, 100 => 100, 0 => t('All nodes')),
471 '#default_value' => relatedcontent_variable_length($type),
472 );
473
474 // Ask where the related nodes are going to be placed in the body.
475 $form['relatedcontent']['relatedcontent_exclude_teasers'] = array(
476 '#type' => 'radios',
477 '#title' => t('Teasers'),
478 '#description' => t('Choose if related content should be shown in teasers.'),
479 '#options' => array(false => t('Include'), true => t('Exclude')),
480 '#default_value' => relatedcontent_variable_exclude_teasers($type),
481 );
482
483 // Ask where the related nodes are going to be placed in the body.
484 $form['relatedcontent']['relatedcontent_output_placing'] = array(
485 '#type' => 'radios',
486 '#title' => t('Placing'),
487 '#description' => t('Choose where the related content should be outputted.'),
488 '#options' => array(false => 'No output', 'beginning' => t('Beginning'), 'end' => t('End')),
489 '#default_value' => relatedcontent_variable_output_placing($type),
490 );
491
492 // Ask the kind of grouping, if any.
493 $form['relatedcontent']['relatedcontent_output_grouped'] = array(
494 '#type' => 'radios',
495 '#title' => t('Grouping'),
496 '#description' => t('Choose how the related content should be grouped.'),
497 '#options' => array(false => t('No grouping'), 'type' => t('Content type'), 'name' => t('Author')),
498 '#default_value' => relatedcontent_variable_output_grouped($type),
499 );
500
501 // Ask if the the related nodes are going to be output as teasers or bodies.
502 $form['relatedcontent']['relatedcontent_output_teasers'] = array(
503 '#type' => 'radios',
504 '#title' => t('Format'),
505 '#description' => t('Choose whether the related content should be viewed as teasers or bodies of the selected nodes.'),
506 '#options' => array(true => t('Teaser'), false => t('Body')),
507 '#default_value' => relatedcontent_variable_output_teasers($type),
508 );
509
510 // Move down the submit and delete/reset buttons.
511 $form['submit']['#weight'] += 1;
512 if ($form['delete']) {
513 $form['delete']['#weight'] = $form['submit']['#weight'];
514 }
515 else {
516 $form['reset']['#weight'] = $form['submit']['#weight'];
517 }
518
519 }
520
521
522 /******************************************************************************
523 * RELATED CONTENT LIST FORM
524 ******************************************************************************/
525
526 /**
527 * Builds the related content list form.
528 */
529 function _relatedcontent_form_list(&$node) {
530
531 $form = array();
532
533 // Build the form.
534 _relatedcontent_form_list_introduction($form, $node->type);
535 _relatedcontent_form_list_table($form, $node->nodes);
536 _relatedcontent_form_list_buttons($form);
537 _relatedcontent_form_list_directives($form);
538
539 return $form;
540
541 }
542
543 /**
544 * Helper function to _relatedcontent_form_list(). Adds a introductory text
545 * to the form.
546 */
547 function _relatedcontent_form_list_introduction(&$form, $type) {
548 $form['intro'] = array(
549 '#value' => t(
550 "Select nodes to be removed from this <em>$type</em>. For more information, see the !help.",
551 array('!help' => l(t('help page'), 'admin/help/relatedcontent'))
552 ),
553 );
554 }
555
556 /**
557 * Helper function to _relatedcontent_form_list(). Adds button to the form.
558 */
559 function _relatedcontent_form_list_table(&$form, $selected_nodes) {
560 foreach ($selected_nodes as $nid => $ordinal) {
561 $node = node_load($nid);
562 $form['nodes'][$node->nid] = array(
563 '#type' => 'checkbox',
564 '#return_value' => $ordinal,
565 '#default_value' => true,
566 );
567 $form['title'][$node->nid] = array('#value' => l($node->title, 'node/'. $node->nid) .' '. theme('mark', node_mark($node->nid, $node->changed)));
568 $form['name'][$node->nid] = array('#value' => node_get_types('name', $node));
569 $form['created'][$node->nid] = array('#value' => format_date($node->created, 'small'));
570 $form['username'][$node->nid] = array('#value' => theme('username', $node));
571 }
572 }
573
574 /**
575 * Helper function to _relatedcontent_form_list(). Adds the table of nodes with
576 * related content to the form.
577 */
578 function _relatedcontent_form_list_buttons(&$form) {
579 $form['buttons']['update'] = array(
580 '#type' => 'submit',
581 '#value' => t('Update'),
582 '#weight' => 5,
583 );
584 }
585
586
587 /**
588 * Helper function to _relatedcontent_form_list(). Adds form processing
589 * directives.
590 */
591 function _relatedcontent_form_list_directives(&$form) {
592 $form['#tree'] = true;
593 $form['#theme'] = 'relatedcontent_form';
594 }
595
596 /**
597 * Handles the submission of the related content list form.
598 */
599 function _relatedcontent_form_list_submit($form_id, &$form_values) {
600
601 // Get the nid of the current node.
602 $nid = arg(1);
603
604 // We are saving. Store the selected nodes in the node.
605 $nodes = array_filter($form_values['nodes']);
606 _relatedcontent_db_delete($nid);
607 _relatedcontent_db_insert($nid, $nodes);
608
609 // Return to the regular view of the node.
610 return "node/$nid/relatedcontent";
611
612 }
613
614
615 /******************************************************************************
616 * RELATED CONTENT VIEW FORM
617 ******************************************************************************/
618
619 /**
620 * Builds the related content view form.
621 */
622 function _relatedcontent_form_view(&$node, $view_id, $form_values = null) {
623
624 $form = array();
625
626 // Get the view that will provide the node list. Abort if the the view has
627 // been removed and the content type hasn't been updated accordingly.
628 if (!($view = _relatedcontent_form_get_view($view_id, $node->type))) return;
629
630 // Determine some data needed in the build process.
631 $page_length = relatedcontent_variable_length($node->type);
632 $page_number = _relatedcontent_form_get_next_page_number($form, $form_values['page_number'], $form_values['op']);
633 $db_result = views_build_view('result', $view, array(), false, $page_length, $page_number);
634 $max_page_number = _relatedcontent_form_get_max_page_number($form, $form_values['max_page_number'], $page_length, $db_result['countquery'], $db_result['rewrite_args']);
635
636 // If this is the initial call, initialize the selected node tracker with the
637 // nodes of related content already stored within the node.
638 if ($form_values == null) {
639 _relatedcontent_track_initialize($node->nid, $node->nodes);
640 }
641
642 // Build the form.
643 _relatedcontent_form_view_introduction($form, $node->type);
644 _relatedcontent_form_view_table($form, $db_result['result'], $node->nid);
645 _relatedcontent_form_view_buttons($form, $page_number, $max_page_number);
646 _relatedcontent_form_view_hidden_values($form, $view_id, $page_length, $page_number, $max_page_number);
647 _relatedcontent_form_view_directives($form);
648
649 return $form;
650
651 }
652
653 /**
654 * Load the view. Display an error message if it has been removed.
655 */
656 function _relatedcontent_form_get_view($view, $type) {
657
658 // Load the named view and return it.
659 if ($view = views_get_view($view)) return $view;
660
661 // No view was given, or the named view didn't exist. Log and display
662 // an error message.
663 $message = t(
664 'RelatedContent uses <a href="!views-url">views</a> as its source of nodes. The specified view <em>@view-name</em> doesn\'t exist any more. Please, go to the <a href="!settings-url">settings of the content type "@content-type"</a> and select another view.',
665 array(
666 '!views-url' => url('admin/build/views'),
667 '!settings-url' => url("admin/content/types/$type"),
668 '@view-name' => $view_name,
669 '@content-type' => $type,
670 )
671 );
672 watchdog('relatedcontent', $message, WATCHDOG_ERROR);
673 drupal_set_message($message, 'error');
674
675 }
676
677 /**
678 * Advance the page. If $page_number is not set, we are on the first page.
679 */
680 function _relatedcontent_form_get_next_page_number(&$form, $page_number, $op) {
681 if (!isset($page_number)) return 0;
682 switch ($op) {
683 case t('Next'):
684 return ++$page_number;
685 case t('Previous'):
686 return --$page_number;
687 }
688 }
689
690 /**
691 * Calculate the number of the very last page.
692 */
693 function _relatedcontent_form_get_max_page_number(&$form, $max_page_number, $page_length, $count_query, $rewrite_args) {
694 if (!isset($max_page_number)) {
695 if ($page_length) {
696 $count = db_rewrite_sql($count_query, 'node', 'nid', $rewrite_args);
697 $count = db_result(db_query($count));
698 $max_page_number = ceil($count / $page_length) - 1;
699 }
700 else {
701 $max_page_number = 0;
702 }
703 }
704 return $max_page_number;
705 }
706
707 /**
708 * Adds a introductory text to the form.
709 */
710 function _relatedcontent_form_view_introduction(&$form, $type) {
711 $form['intro'] = array(
712 '#value' => t(
713 "Select nodes to be added to this <em>$type</em>. For more information, see the !help.",
714 array('!help' => l(t('help page'), 'admin/help/relatedcontent'))
715 ),
716 );
717 }
718
719 /**
720 * Adds the table of nodes with related content to the form.
721 */
722 function _relatedcontent_form_view_table(&$form, $db_result, $nid) {
723 $ordinal = 0;
724 $selected_nodes = _relatedcontent_track_get_selected_nodes($nid);
725 while ($node = db_fetch_object($db_result)) {
726 $node = node_load($node->nid);
727 $form['nodes'][$node->nid] = array(
728 '#type' => 'checkbox',
729 '#return_value' => ++$ordinal,
730 '#attributes' => $selected_nodes[$node->nid] ? array('checked' => 'checked') : null, // See http://drupal.org/node/187413.
731 );
732 $form['title'][$node->nid] = array('#value' => l($node->title, 'node/'. $node->nid) .' '. theme('mark', node_mark($node->nid, $node->changed)));
733 $form['name'][$node->nid] = array('#value' => node_get_types('name', $node));
734 $form['created'][$node->nid] = array('#value' => format_date($node->created, 'small'));
735 $form['username'][$node->nid] = array('#value' => theme('username', $node));
736 }
737 }
738
739 /**
740 * Adds buttons to the form.
741 */
742 function _relatedcontent_form_view_buttons(&$form, $page_number, $max_page_number) {
743
744 // On all pages, except the first page, add a previous button.
745 if ($page_number > 0) {
746 $form['buttons']['previous'] = array(
747 '#type' => 'submit',
748 '#value' => t('Previous'),
749 '#weight' => -5,
750 );
751 }
752
753 // On all pages, except the last page, add a next button.
754 if ($page_number < $max_page_number) {
755 $form['buttons']['next'] = array(
756 '#type' => 'submit',
757 '#value' => t('Next'),
758 '#weight' => 0,
759 );
760 }
761
762 // On all pages, add an update button.
763 $form['buttons']['update'] = array(
764 '#type' => 'submit',
765 '#value' => t('Update'),
766 '#weight' => 5,
767 );
768
769 }
770
771 /**
772 * Adds hidden fields with information needed later.
773 */
774 function _relatedcontent_form_view_hidden_values(&$form, $view_id, $page_length, $page_number, $max_page_number) {
775
776 // Store the view's id.
777 $form['view_id'] = array(
778 '#type' => 'hidden',
779 '#value' => $view_id,
780 );
781
782 // Store the number of nodes shown on a page.
783 $form['page_length'] = array(
784 '#type' => 'hidden',
785 '#value' => $page_length,
786 );
787
788 // Store the number of the current page.
789 $form['page_number'] = array(
790 '#type' => 'hidden',
791 '#value' => $page_number,
792 );
793
794 // Store the number of the very last page.
795 $form['max_page_number'] = array(
796 '#type' => 'hidden',
797 '#value' => $max_page_number,
798 );
799
800 }
801
802 /**
803 * Adds form processing directives.
804 */
805 function _relatedcontent_form_view_directives(&$form) {
806 $form['#multistep'] = true;
807 $form['#tree'] = true;
808 $form['#theme'] = 'relatedcontent_form';
809 }
810
811 /**
812 * Handles the submission of the related content form.
813 */
814 function _relatedcontent_form_view_submit($form_id, &$form_values) {
815
816 // Get the nid of the current node.
817 $nid = arg(1);
818
819 // Track changes done on the current form page.
820 $offset = $form_values['page_number'] * $form_values['page_length'];
821 _relatedcontent_track_update_selected_nodes($nid, $form_values['nodes'], $offset);
822
823 // If we are not saving, return to the form again.
824 if ($form_values['op'] != t('Update')) return false;
825
826 // We are saving. Finalize the tracking; and store the tracked changes.
827 $nodes = _relatedcontent_track_finalize($nid, $form_values['view_id'], $form_values['page_length'], $form_values['max_page_number']);
828 _relatedcontent_db_delete($nid);
829 _relatedcontent_db_insert($nid, $nodes);
830
831 // Return to the regular view of the node.
832 return "node/$nid/relatedcontent";
833
834 }
835
836 /******************************************************************************
837 * SELECTED NODES TRACKING
838 ******************************************************************************/
839
840 /**
841 * Initalize the selected nodes tracker.
842 *
843 * @see _relatedcontent_track_finalize() for details.
844 */
845 function _relatedcontent_track_initialize($nid, $nodes) {
846
847 // Tracks selected nodes as we move from on epage to another.
848 $_SESSION['relatedcontent'][$nid]['nodes'] = $nodes;
849
850 // Tracks the highets page number visited.
851 $_SESSION['relatedcontent'][$nid]['highest_page_number'] = 0;
852
853 }
854
855 /**
856 * Add $nodes to the selected nodes tracker.
857 *
858 * @see _relatedcontent_track_finalize() for details.
859 */
860 function _relatedcontent_track_update_selected_nodes($nid, $nodes, $offset) {
861
862 // Keep track of the highest page number visited.
863 if ($_SESSION['relatedcontent'][$nid]['highest_page_number'] < $page_number) {
864 $_SESSION['relatedcontent'][$nid]['highest_page_number'] = $page_number;
865 }
866
867 // The ordinal numbers of the passed must be added by an offset to keep
868 // the sort order from one page to another.
869 foreach ($nodes as $id => $ordinal) {
870 if ($ordinal) {
871 $nodes[$id] += $offset;
872 }
873 }
874
875 // For keys in $_SESSION['relatedcontent'][$nid]['nodes'] for which there is no key
876 // in $nodes, append corresponding value $_SESSION['relatedcontent'][$nid]['nodes']
877 // to $node.
878 $nodes += $_SESSION['relatedcontent'][$nid]['nodes'];
879
880 // Remove all non-selcted nodes.
881 $nodes = array_filter($nodes);
882
883 // Store the nodes in the session.
884 $_SESSION['relatedcontent'][$nid]['nodes'] = $nodes;
885
886 }
887
888 /**
889 * Returns the traced selected nodes.
890 *
891 * @see _relatedcontent_track_finalize() for details.
892 */
893 function _relatedcontent_track_get_selected_nodes($nid) {
894 return $_SESSION['relatedcontent'][$nid]['nodes'];
895 }
896
897 /**
898 * Finalizing the tracking, and returns the selected nodes.
899 *
900 * Nodes with related content get those loaded into an array where the keys are
901 * the nodes' id and the values are ordinal numbers implying an order by which
902 * they should be displayed. These initial ordinal numbers are negative.
903 *
904 * Upon first display of the table with nodes to select from, the tracker is
905 * initialized with these tuples of node id and negative ordinal numbers.
906 * Each time the user moves from one page of the table to another, or click on
907 * the Update button, the tracker updates the ordinal number of those nodes
908 * displayed on the page the user is leaving. The updated numbers are positive,
909 * and chosen to reflect the sort order among the seen nodes.
910 *
911 * We have a contract with the user to not break the sort order of already
912 * selected nodes if they are available in the view. We must therefore
913 * "visit" all pages that has not yet been displayed, and update the ordinal
914 * numbers of those remaining nodes which we find on these pages. In many
915 * applications of RelatedContent it is likely to find the remaining nodes
916 * among the first non-visited pages. This given, and the potential that
917 * there might be many unvisited pages, which translates into many round-
918 * trips to the database, we abort the search as soon as possible. However,
919 * if there are orphan nodes (see the on-line help), we will visit all pages
920 * in vain.
921 */
922 function _relatedcontent_track_finalize($nid, $view_id, $page_length, $max_page_number) {
923
924 // Get the tracked information.
925 $nodes = $_SESSION['relatedcontent'][$nid]['nodes'];
926 $page = $_SESSION['relatedcontent'][$nid]['highest_page_number'];
927
928 // Get the node id of those nodes which haven't been seen. These are at the
929 // end of the array of tracked nodes and are recognized by negative ordinal
930 // numbers.
931 for (end($nodes); current($nodes) < 0 ; prev($nodes)) {
932 $remaining_nodes[key($nodes)] = true;
933 }
934
935 // Visit all pages that has not been displayed, and update the ordinal
936 // numbers of those remaining nodes which we find on these pages.
937 $view = views_get_view($view_id); // Get the view.
938 $ordinal = ($page + 1) * $page_length; // The last used ordinal number.
939 while (++$page <= $max_page_number) { // For each page not yet visited...
940 $info = views_build_view('result', $view, array(), false, $page_length, $page); // Get the nodes on the current page.
941 while ($node = db_fetch_object($info['result'])) { // For each node on the current page...
942 ++$ordinal; // Next ordinal number.
943 if ($remaining_nodes[$node->nid]) { // If current node is among the remaining ones...
944 $nodes[$node->nid] = $ordinal; // Assign an ordinal number.
945 unset($remaining_nodes[$node->nid]); // Remove it from the list of remaining nodes.
946 if (count($remaining_nodes) == 0) break 2; // Abort as soon as possible.
947 }
948 }
949 }
950
951 // Clean up.
952 unset($_SESSION['relatedcontent'][$nid]);
953
954 return $nodes;
955
956 }
957
958 /******************************************************************************
959 * IMPLEMENTATION OF THE RELATEDCONTENT API
960 *****************************************************************************/
961
962 /**
963 * For each node with related content and which the user is allowed to view,
964 * append the related content to the end of the output buffer of the relevant
965 * group.
966 */
967 function _relatedcontent(&$node, $output_grouped, $content_function, $args) {
968 array_unshift($args, 0);
969 foreach ($node->nodes as $nid => $ordinal) {
970 if ($nid && ($n = node_load($nid)) && node_access ('view', $n)) {
971 $args[0] = $n;
972 $key = $output_grouped ? $n->$output_grouped : 'all';
973 $output[$key][] = call_user_func_array($content_function, $args);
974 }
975 }
976 return $output;
977 }
978
979
980 /******************************************************************************
981 * PERSISTED VARIABLES
982 *****************************************************************************/
983
984 /**
985 * The persisted variable 'enabled' which is true/false whether the
986 * related content is enabled or not, respectively, for the content type.
987 * Default value, if the variable does not exists, is FALSE.
988 */
989 function relatedcontent_variable_enabled($type, $enabled = null) {
990 return _relatedcontent_variable("relatedcontent_enabled_$type", $enabled, false);
991 }
992
993 /**
994 * The persisted variable 'view' containing the name of the view to feed the
995 * assembler for a node of the given type. Default value, if the variable does
996 * not exists, is ''.
997 */
998 function relatedcontent_variable_view($type, $view = null) {
999 return _relatedcontent_variable("relatedcontent_view_$type", $view, '');
1000 }
1001
1002 /**
1003 * The persisted variable 'length' containing the number of nodes to show in
1004 * the assembler for a node of the given type. Default value, if the variable
1005 * does not exists, is 50.
1006 */
1007 function relatedcontent_variable_length($type, $page_length = null) {
1008 return _relatedcontent_variable("relatedcontent_length_$type", $page_length, 50);
1009 }
1010
1011 /**
1012 * The persisted variable 'exclude_teasers' which is true/false whether the
1013 * related content should excluded in teasers or not respectively. Default
1014 * value, if the variable does not exists, is TRUE.
1015 */
1016 function relatedcontent_variable_exclude_teasers($type, $exclude_teasers = null) {
1017 return _relatedcontent_variable("relatedcontent_exclude_teasers_$type", $exclude_teasers, true);
1018 }
1019
1020 /**
1021 * The persisted variable 'teasers' which is true/false whether the related
1022 * content should be viewed as teasers or full bodies respectively. Default
1023 * value, if the variable does not exists, is TRUE.
1024 */
1025 function relatedcontent_variable_output_teasers($type, $teasers = null) {
1026 return _relatedcontent_variable("relatedcontent_output_teasers_$type", $teasers, true);
1027 }
1028
1029 /**
1030 * The persisted variable 'grouped' which is false or a string with the name
1031 * of the node attribute, e.g. 'type' and 'name', by which the related content
1032 * should be grouped by. Default value, if the variable does not exists, is
1033 * 'type'.
1034 */
1035 function relatedcontent_variable_output_grouped($type, $grouped = null) {
1036 return _relatedcontent_variable("relatedcontent_output_grouped_$type", $grouped, 'type');
1037 }
1038
1039 /**
1040 * The persisted variable 'after' which is false if related content should not
1041 * be outputted, 'beginning' or 'end' if the output should be at the beginning
1042 * or end respectively. Default value, if the variable does not exists, is
1043 * 'end'.
1044 */
1045 function relatedcontent_variable_output_placing($type, $after = null) {
1046 return _relatedcontent_variable("relatedcontent_output_placing_$type", $after, 'end');
1047 }
1048
1049 /**
1050 * Sets and gets the named persisted variable.
1051 */
1052 function _relatedcontent_variable($name, $value = null, $default = null) {
1053 if (isset($value)) {
1054 variable_set($name, $value);
1055 }
1056 return variable_get($name, $default);
1057 }
1058
1059
1060 /******************************************************************************
1061 * DATABASE
1062 *****************************************************************************/
1063
1064 /**
1065 * Returns the content type of the node with the provided node id.
1066 */
1067 function _relatedcontent_db_content_type($nid) {
1068 $db_result = db_query('SELECT type FROM {node} WHERE nid = %d', $nid);
1069 $db_result = db_fetch_object($db_result);
1070 return $db_result->type;
1071 }
1072
1073 /**
1074 * Returns the available views provided by the Views module.
1075 */
1076 function _relatedcontent_db_views() {
1077 $views = array('' => '');
1078 $db_result = db_query('SELECT vid, name FROM {view_view} ORDER BY name');
1079 while ($view = db_fetch_object($db_result)) {
1080 $views[$view->vid] = $view->name;
1081 }
1082 return $views;
1083 }
1084
1085 /**
1086 * Insert nid of included nodes into database.
1087 */
1088 function _relatedcontent_db_insert($nid, $nodes) {
1089
1090 // Build the VALUES clause for inserting multiple rows.
1091 foreach ($nodes as $include_nid => $ordinal) {
1092 $values .= "($nid, $include_nid, $ordinal),";
1093 }
1094 $values = substr($values, 0, -1);
1095
1096 // Insert the values, if any, into the RelatedContent table.
1097 if ($values) {
1098 db_query('INSERT INTO {relatedcontent} (nid, include_nid, ordinal_number) VALUES %s', $values);
1099 }
1100
1101 }
1102
1103 /**
1104 * Load nid of included nodes from database.
1105 *
1106 * Get the related content of $nid, and put it into a map. The keys are the id
1107 * of the nodes with the related content. The values are ordinal numbers. The
1108 * ordinal numbers are negative, going from -N, where N is the count of pairs
1109 * in the map, to -1. The reason is to flag that these numbers doesn't come
1110 * from the user selecting nodes, but from the database. The ordinal number
1111 * itself is irrelevant; it's only their relation to each other that matters.
1112 */
1113 function _relatedcontent_db_load($nid) {
1114 $nodes = array();
1115 $db_result = db_query('SELECT include_nid FROM {relatedcontent} WHERE nid = %d GROUP BY ordinal_number', $nid);
1116 $ordinal = -db_num_rows($db_result);
1117 while ($row = db_fetch_object($db_result)) {
1118 $nodes[$row->include_nid] = $ordinal++;
1119 }
1120 return $nodes;
1121 }
1122
1123 /**
1124 * Delete nid of included nodes in database.
1125 */
1126 function _relatedcontent_db_delete($nid) {
1127 db_query('DELETE FROM {relatedcontent} WHERE nid = %d', $nid);
1128 }

  ViewVC Help
Powered by ViewVC 1.1.2