background image
HomeRecent PostsDrupalSearchTagsRSSContactAboutAccount
Eric.London's picture

Last year I wrote a quick code snippet to Prevent the user from creating more than one node of a certain type for Drupal 5. I received a comment request to update this code for Drupal 6.

<?php
function MYMODULE_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {

 
// define node type that a user will only be allowed to create one instance of
 
$singleNodeType = 'YOUR-NODE-TYPE';
 
 
// test for node/add/NODETYPE page
 
if ($node->type==$singleNodeType && $op=='prepare' && arg(0)=='node' && arg(1)=='add') {

   
// define sql to create node table
   
$sql = "select nid from {node} where type='%s' and uid='%d'";
   
   
// execute sql
   
$resource = db_query($sql, $singleNodeType, db_escape_string($GLOBALS['user']->uid));
   
$result = db_result($resource);

   
// test for result
   
if (!empty($result)) {
     
     
// set a message
     
drupal_set_message("Sorry, you are only allow to create one $singleNodeType.");
     
     
// redirect the user
     
drupal_goto('node/add');
     
    }
       
  }
 
}
?>

The above code tests if the user is on the node/add/NODETYPE page and if the user has already created an instance of the node type, sets a message and redirects them away from the node/add page.

The Fivestar module offers a great way to allow users to rate your content. Enabling this functionality for a node type was straight forward. My next goal was to show the fivestar widget in a view for each node row. Unfortunately, the row styling for my view was "Fields", so the fivestar widget would not appear. Apparently, the fivestar widget is only available for the teaser & full node display options.

No worries, you can always override the themable output of a view. Using the "Theme information" section on the view edit screen, I decided to create a new template file in my theme directory for "Row style output". For example:

views-view-fields--MY-VIEW-NAME.tpl.php

I then copied the contents of default view template into this file.

<?php
// $Id: views-view-fields.tpl.php,v 1.6 2008/09/24 22:48:21 merlinofchaos Exp $
/**
* @file views-view-fields.tpl.php
* Default simple view template to all the fields as a row.
*
* - $view: The view in use.
* - $fields: an array of $field objects. Each one contains:
*   - $field->content: The output of the field.
*   - $field->raw: The raw data for the field, if it exists. This is NOT output safe.
*   - $field->class: The safe class id to use.
*   - $field->handler: The Views field handler object controlling this field. Do not use
*     var_export to dump this object, as it can't handle the recursion.
*   - $field->inline: Whether or not the field should be inline.
*   - $field->inline_html: either div or span based on the above flag.
*   - $field->separator: an optional separator that may appear before a field.
* - $row: The raw result object from the query, with all data it fetched.
*
* @ingroup views_templates
*/
?>

<?php foreach ($fields as $id => $field): ?>
  <?php if (!empty($field->separator)): ?>
    <?php print $field->separator; ?>
  <?php endif; ?>

  <<?php print $field->inline_html;?> class="views-field-<?php print $field->class; ?>">
    <?php if ($field->label): ?>
      <label class="views-label-<?php print $field->class; ?>">
        <?php print $field->label; ?>:
      </label>
    <?php endif; ?>
      <?php
     
// $field->element_type is either SPAN or DIV depending upon whether or not
      // the field is a 'block' element type or 'inline' element type.
     
?>

      <<?php print $field->element_type; ?> class="field-content"><?php print $field->content; ?></<?php print $field->element_type; ?>>
  </<?php print $field->inline_html;?>>
<?php endforeach; ?>

By examining the hook_nodeapi() function of the Fivestar module, I decided to use the fivestar_widget_form() function to embed the widget in my view. This function accepts one argument, the $node object. Inside the first php tag in my new template file, I added the following code to load the node object:

<?php
$node
= node_load($row->nid);
?>

Last, I determined a good insertion point for the fivestar widget in this template file, and added the following code:

<?php
if (function_exists('fivestar_widget_form')) print fivestar_widget_form($node);
?>

If done correctly the Fivestar widget should now be embedded in the view for each node row.

Eric.London's picture

In a recent project I used the content profile module to improve flexibility in creating user profiles. This module replaces the regular profile creation with a CCK node type. One of the requested profile fields was job title, which is confusing since title is already a required field of a node. In most cases, I would use first and last name as the title, but in this case those fields were required to be separate. Since the node title did not fit into the list of requested fields, I decided to remove it from the form and dynamically generate a node title from the user's first and last name. Here's how this can be accomplished:

<?php
// create a form_alter hook to remove the field from the form:
function MYMODULE_form_alter(&$form, $form_state, $form_id) {

  if (
$form_id == 'profile_node_form') {
   
// remove title from view
   
$form['title']['#access'] = false;
  }
   
}

// create a nodeapi function to create the node title when the node is created
function MYMODULE_nodeapi(&$node, $op, $a3=NULL, $a4=NULL) {
  if (
$node->type =='profile' && $op=='insert') {
   
$node->title = $node->field_profile_name_first[0]['value'] . " " . $node->field_profile_name_last[0]['value'];
   
node_save($node);      
  }
}
?>

This code snippet shows you how to refresh a view using AJAX and form controls, and allows users to save their search parameters as nodes. This tutorial consists of 2 major parts: 1) regenerating a view using arguments; and 2) saving these arguments in a node which can be reloaded at a later point.

1) Create a multiple select taxonomy vocabulary and add some terms to it. For my example, I called the vocabulary "Eric's Vocab 1" and assigned 5 terms to it: Term 1, Term 2, Term 3, Term 4, Term 5. It will be used to pass term IDs as arguments to the view.

2) Create a view that has a "Taxonomy: Term ID" argument. Make sure you choose "Display all values" for the action to take if argument is not present. Add a validator for Taxonomy term. Choose "Term IDs separated by , or +" for argument type. Enable allow multiple terms per argument and allow multiple arguments to work together. Add view filters and fields as necessary. For my example, I created a view called "eric_ajax_reload", added filter for published nodes, and added the node title and taxonomy terms as fields.

3) To test my view and argument, I added a bunch of content that had various taxonomy terms assigned to each one. I used the Live Preview functionality at the bottom of the view-edit screen to ensure my view was working with arguments.

4) Create a menu callback to embed the view into a page and bypass the theme layer:

<?php
// define menu item
function ericview_menu() {
 
$items[] = array();

 
$items['ericview/page/view'] = array(
   
'page callback' => 'ericview_callback_page_view',
   
'access arguments' => array('access content'),
   
'title' => t("Eric's Title"),
   
'type' => MENU_CALLBACK,
  );
   
  return
$items;
}

// define menu callback
function ericview_callback_page_view() {
 
$page_content = "";
 
$page_contents .= _ericview_view_create();
  print
$page_contents
 
exit();
}

// define function to create view html
function _ericview_view_create() {

 
// TODO: validate $_GET args
   
  // create view arguments
 
if (is_array($_GET['vocab1'])) {
   
$selectArgs = implode('+', $_GET['vocab1']);
  }

 
$html = "";
 
$viewName = 'eric_ajax_reload';
 
$display_id = 'default';
 
$html .= views_embed_view($viewName, $display_id, $selectArgs);
  return
$html;
}
?>

Now, if you flush your menu cache, you should be able to get to this new page. At this point, modifying the vocab1 query string value and reloading the page should update your view. If not, make sure your argument is working properly using the Live Preview.

5) Create another page callback that will consist of 2 columns: 1 column that has a form used to update the view, and 1 column that shows the view.

<?php
// define menu item
function ericview_menu() {
 
$items[] = array();
 
// ...code...
 
$items['ericview/page'] = array(
   
'page callback' => 'ericview_callback_page',
   
'access arguments' => array('access content'),
   
'title' => t("Eric's Title"),
   
'type' => MENU_CALLBACK,
  );
 
// ...code... 
 
return $items;
}

// define menu callback
function ericview_callback_page() {
   
 
// include css
 
drupal_add_css(drupal_get_path('module','ericview') . '/ericview.css');

 
// include javascript
 
drupal_add_js(drupal_get_path('module','ericview') . '/ericview.js');
   
 
$page_contents = "";
 
$page_contents .= "<div id='eric_column_left'>"
   
. drupal_get_form('_ericview_form') . "</div>";
 
$page_contents .= "<div id='eric_column_right'>"
   
. _ericview_view_create() . "</div>";
 
$page_contents .= "<div class='clearBoth'></div>";
   
  return
$page_contents;
   
}

// create a form that uses the taxonomy vocab as a multiple select element
function _ericview_form() {

 
$form = array();
   
 
$vocabName = "Eric's Vocab 1";
   
 
// TODO: use a function instead :)
 
$sql = "
    select td.tid, td.name
    from {vocabulary} v
    join {term_data} td on td.vid = v.vid
    where v.name = '%s'"
;
 
$resource = db_query($sql, db_escape_string($vocabName));
 
$results = array();
  while (
$row = db_fetch_array($resource)) $results[$row['tid']] = $row['name'];
   
 
$form['vocab1'] = array(
   
'#title' => $vocabName,
   
'#type' => 'select',
   
'#options' => $results,
   
'#multiple' => true,
   
'#attributes' => array(
     
'onChange' => 'ericview_select_change(this);',
    ),
   
'#default_value' => is_array($_GET['vocab1']) ? $_GET['vocab1'] : array(),
  );
   
 
$form['save'] = array(
   
'#type' => 'submit',
   
'#value' => 'Save',
  );
   
  return
$form;
  
}
?>

Here is the jQuery I added to get the selected options from the select element, make the AJAX call to regenerate the view, and update the contents of the right column

<?php
function ericview_select_change(whichThis) {
  var
vocab1Options = new Array();
  $(
'option:selected', whichThis).each(function(i){
   
vocab1Options[i] = $(this).val();
  });
   
 
// define args
  // note: the following will not work
  // var args = { 'vocab1': vocab1Options };
  // generates: ?vocab1=2&vocab1=3&vocab1=4
  // not:       ?vocab1[]=2&vocab1[]=3&vocab1[]=4
 
var args = { };

 
// create get path       
 
var getPath = '/ericview/page/view'   

 
// add query string arguments to get path
 
getArgs = "";
  for (var
i=1; i<=vocab1Options.length; i++) {
    if (
getArgs.length==0) getArgs += "?";
   
getArgs += 'vocab1[]=' + vocab1Options[i-1];
    if (
i != vocab1Options.length) getArgs += "&";
  }
 
getPath = getPath + getArgs;
   
 
// make ajax call using get
 
$.get(getPath, args,
    function(
result){
      if (
result) $('#eric_column_right').html(result);
    }
  );
   
}
?>

And, here is some CSS to create a simple 2 column layout:

.clearBoth {
  clear: both;
}

#eric_column_left {
  float: left;
  width: 25%;
}

#eric_column_right {
  float: left;
  width: 75%;
}

After flushing the menu cache, and going to the new page, you should see a 2 column layout: the left column containing a select element with the taxonomy terms and the right column containing the view. When selecting options in the taxonomy select element, they options should be passed to view as arguments, and the view should be reloaded:

The second part of this tutorial is adding the functionality to save the search as a node. There should be a Save button already on the form that does not do anything. We'll need to add a submit handler to save the selected options into a node. So, first you'll need to create a new node type. For my example, I created a node called "saved_search" with a single textarea field called "field_search_parameters". Here's the code I added to make the submit button function:

<?php
// define a submit handler that serializes the selected form data and creates a node
function _ericview_form_submit($form, &$form_state) {

 
// define a list of values to save
  // TODO: make this more dynamic?
 
$saveValues = array('vocab1');
   
 
$savedParams = array();
  foreach (
$form_state['values'] as $k => $v) {
    if (
in_array($k, $saveValues)) {
     
$savedParams[$k] = $v;
    }
  }
   
 
// create saved search node
 
$node = (object) NULL;
 
$node->type = 'saved_search';
 
$nodeData = array();
 
$nodeData['values'] = array(
   
'title' => 'Saved Search: ' . date('r'),
   
'field_search_parameters' => array(
      array(
       
'value' => serialize($savedParams)
      )
    ),
   
'name' => $GLOBALS['user']->name,
   
'op' => t('Save'),
  );
   
 
// include node file, necessary for node generation
 
module_load_include('inc', 'node', 'node.pages');
   
 
// create node using drupal_execute
 
drupal_execute('saved_search_node_form', $nodeData, $node);
   
}
?>

Now when the user clicks on the save button, a node will be created that contains serialized search form data in a field. If you view one of these newly created nodes, you get some unusable serialized data on the screen:

Instead, you can add a nodeapi hook to redirect the user back to the menu callback:

<?php
function ericview_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  if (
$node->type == 'saved_search' && $op=='view') {
       
   
// unserialize data
   
$data = unserialize($node->field_search_parameters[0]['value']);
       
   
// create query string       
   
$queryString = array();
    foreach (
$data as $k => $v) {
      if (
is_array($v)) {
        foreach (
$v as $v2) {
         
$queryString[] = $k."[]=".$v2;
        }
      } else {
       
$queryString[] = "$k=$v";
      }
    }
   
$queryString = implode("&", $queryString);
       
   
// redirect user
   
drupal_goto('ericview/page', $queryString);
       
  }
}
?>

For my example, I unserialize the data in the node's field and create a query string based on the data. That way, the $_GET parameters will be passed into the form and the view, acting as a saved search.

Syndicate content