Index: votingapi.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/votingapi/votingapi.module,v
retrieving revision 1.46.2.18
diff -u -p -r1.46.2.18 votingapi.module
--- votingapi.module	11 Feb 2009 21:51:27 -0000	1.46.2.18
+++ votingapi.module	18 Feb 2009 19:54:01 -0000
@@ -78,6 +78,88 @@ function votingapi_cron() {
 }
 
 /**
+ * Implementation of hook_votingapi_tags().
+ *
+ * Provide the default 'vote' tag.
+ */
+function votingapi_votingapi_tags() {
+  return array(
+    'vote' => array(
+      'name' => t('Vote'),
+      'description' => t('Vote is a generic, multi-use voting tag often used as a default.'),
+      'data_types' => array(
+        'percent',
+      ),
+    ),
+  );
+}
+
+/**
+ * Builds a list of available voting tags and returns all or part of this list
+ * in the specified format.
+ *
+ * @see node_get_types().
+ *
+ * @param $op
+ *   The format in which to return the list. When this is set to 'tag' or
+ *   'name', only the specified tag is returned. When set to 'tags' or
+ *   'names', all tags are returned.
+ * @param $tag
+ *   A tag object, array, or string that indicates the tag to return.
+ *   Leave at default value (NULL) to return a list of all tags.
+ * @param $reset
+ *   Whether or not to reset this function's internal cache (defaults to
+ *   FALSE).
+ * @return
+ *   Either an array of all available tags or a single tag, in a
+ *   variable format. Returns FALSE if the tag is not found.
+ */
+function votingapi_get_tags($op = 'tags', $tag = NULL, $reset = FALSE) {
+  static $_tags, $_names;
+
+  if ($reset || !isset($_tags)) {
+    // If the votingapi_tags module is enabled, use that to get the list of tags
+    // defined in the database and via the 'votingapi_tags' hook.
+    if (module_exists('votingapi_tags')) {
+      list($_tags, $_names) = _votingapi_tags_tags_build();
+    }
+
+    // Otherwise just return the ones defined via the 'votingapi_tags' hook.
+    else {
+      $tags_array = module_invoke_all('votingapi_tags');
+      foreach ($tags_array as $tag => $info) {
+        $info['tag'] = $tag;
+        $_tags[$tag] = votingapi_tags_tag_set_defaults($info);
+        $_names[$tag] = $info['name'];
+      }
+      asort($_names);
+    }
+  }
+
+  if ($tag) {
+    if (is_array($tag)) {
+      $tag = $tag['tag'];
+    }
+    elseif (is_object($node)) {
+      $tag = $tag->tag;
+    }
+    if (!isset($_tags[$tag])) {
+      return FALSE;
+    }
+  }
+  switch ($op) {
+    case 'tags':
+      return $_tags;
+    case 'tag':
+      return isset($_tags[$tag]) ? $_tags[$tag] : FALSE;
+    case 'names':
+      return $_names;
+    case 'name':
+      return isset($_names[$tag]) ? $_names[$tag] : FALSE;
+  }
+}
+
+/**
  * Cast a vote on a particular piece of content.
  *
  * This function does most of the heavy lifting needed by third-party modules
Index: votingapi_tags/votingapi_tags.admin.inc
===================================================================
RCS file: votingapi_tags/votingapi_tags.admin.inc
diff -N votingapi_tags/votingapi_tags.admin.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ votingapi_tags/votingapi_tags.admin.inc	18 Feb 2009 19:54:01 -0000
@@ -0,0 +1,316 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Configuration forms and helper functions for the Voting API Tags module.
+ */
+
+/**
+ * Display the voting tag admin overview page.
+ */
+function votingapi_tags_admin_overview_tags() {
+  $tags = votingapi_get_tags();
+  $names = votingapi_get_tags('names');
+  $header = array(t('Name'), array('data' => t('Operations'), 'colspan' => '2'));
+  $rows = array();
+
+  foreach ($names as $key => $name) {
+    $tag = $tags[$key];
+    $row = array(theme('votingapi_tags_admin_overview_tags', $name, $tag));
+
+    if ($tag->custom) {
+      // Set the edit column.
+      $row[] = array('data' => l(t('edit'), 'admin/settings/votingapi/' . $tag->tag . '/edit'));
+    // Set the delete column.
+      $row[] = array('data' => l(t('delete'), 'admin/settings/votingapi/' . $tag->tag . '/delete'));
+    }
+    else {
+      // Have to do this, so an empty columns appear and the row is the correct
+      // length.
+      $row[] = array();
+      $row[] = array();
+    }
+    $rows[] = $row;
+  }
+
+  return theme('table', $header, $rows);
+}
+
+/**
+ * Theme a voting tag for output on the admin overview page.
+ */
+function theme_votingapi_tags_admin_overview_tags($name, $tag) {
+  $output = check_plain($name);
+  $output .= ' <small> (Machine name: ' . check_plain($tag->tag) . ')</small>';
+  $output .= '<div class="description">' . filter_xss_admin($tag->description) . '</div>';
+  return $output;
+}
+
+/**
+ * Build the tag editing form.
+ *
+ * If a tag is passed in, an edit form with both Save and Delete buttons will
+ * be built. Otherwise, a blank 'add new record' form, without the Delete button,
+ * will be built.
+ *
+ * @ingroup forms
+ * @see votingapi_tags_tag_form_validate()
+ * @see votingapi_tags_tag_form_submit()
+ * @see votingapi_tags_tag_form_delete()
+ * @see node_type_form()
+ */
+function votingapi_tags_tag_form(&$form_state, $tag = NULL) {
+  // TODO: write js file based on content_types.js in D7 core.
+  // drupal_add_js(drupal_get_path('module', 'votingapi') .'/votingtags.js');
+  if (!isset($tag->tag)) {
+    // This is a new tag.  Votingapi module managed types are custom and unlocked.
+    $tag = votingapi_tags_tag_set_defaults(array('custom' => 1, 'locked' => 0));
+  }
+  // Ensure it is not a system type - should never reach here in normal
+  // operations, but just in case...
+  elseif ($tag->locked) {
+    drupal_set_message(t('%tag is a module defined tag and can not be modified.', array('%tag' => $tag->name)), 'error');
+    return;
+  }
+
+  // Make the tag object available to implementations of hook_form_alter.
+  $form['#voting_tag'] = $tag;
+
+  $form['identity'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Identification'),
+  );
+  $form['identity']['name'] = array(
+    '#title' => t('Name'),
+    '#type' => 'textfield',
+    '#default_value' => $tag->name,
+    '#description' => t('The human-readable name of this voting tag. It is recommended that this name begin with a capital letter and contain only letters, numbers, and <strong>spaces</strong>. This name must be unique.'),
+    '#required' => TRUE,
+    '#field_suffix' => ' <small id="voting-tag-name-suffix">&nbsp;</small>',
+  );
+
+  if (!$tag->tag) {
+    $form['identity']['tag'] = array(
+      '#title' => t('Machine name'),
+      '#type' => 'textfield',
+      '#default_value' => $tag->tag,
+      '#maxlength' => 32,
+      '#required' => TRUE,
+      '#description' => t('The machine-readable name of this voting tag. This name must contain only lowercase letters, numbers, and dashes. This name must be unique.'),
+    );
+  }
+  else {
+    $form['identity']['tag'] = array(
+      '#type' => 'value',
+      '#value' => $tag->tag,
+    );
+    $form['identity']['tag_display'] = array(
+      '#title' => t('Machine name'),
+      '#type' => 'item',
+      // TODO: change to #markup when updating to Drupal 7.
+      '#value' => theme('placeholder', $tag->tag),
+      '#description' => t('The machine-readable name of this voting tag.'),
+    );
+  }
+
+  $form['identity']['description'] = array(
+    '#title' => t('Description'),
+    '#type' => 'textarea',
+    '#default_value' => $tag->description,
+    '#description' => t('A brief description of this voting tag.'),
+  );
+
+  $form['types'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Vote type'),
+  );
+
+  $form['types']['percent'] = array(
+    '#title' => t('Percent'),
+    '#type' => 'checkbox',
+    '#default_value' => (in_array('percent', $tag->data_types) ? TRUE : FALSE),
+  );
+
+  $form['types']['points'] = array(
+    '#title' => t('Points'),
+    '#type' => 'checkbox',
+    '#default_value' => (in_array('points', $tag->data_types) ? TRUE : FALSE),
+  );
+
+  $form['old_tag'] = array(
+    '#type' => 'value',
+    '#value' => $tag->tag,
+  );
+  $form['custom'] = array(
+    '#type' => 'value',
+    '#value' => $tag->custom,
+  );
+  $form['locked'] = array(
+    '#type' => 'value',
+    '#value' => $tag->locked,
+  );
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save voting tag'),
+    '#weight' => 40,
+  );
+
+  if ($tag->custom) {
+    if (!empty($tag->tag)) {
+      $form['delete'] = array(
+        '#type' => 'submit',
+        '#value' => t('Delete voting tag'),
+        '#weight' => 45,
+      );
+    }
+  }
+
+  return $form;
+}
+
+/**
+ * Implementation of hook_form_validate().
+ */
+function votingapi_tags_tag_form_validate($form, &$form_state) {
+  $tag = new stdClass();
+  $tag->tag = trim($form_state['values']['tag']);
+  $tag->name = trim($form_state['values']['name']);
+
+  // Work out what the tag was before the user submitted this form.
+  $old_tag = trim($form_state['values']['old_tag']);
+
+  $tags = votingapi_get_tags('names');
+
+  if (!$form_state['values']['locked']) {
+    if (isset($tags[$tag->tag]) && $tag->tag != $old_tag) {
+      form_set_error('tag', t('The machine-readable name %tag is already taken.', array('%tag' => $tag->tag)));
+    }
+    if (!preg_match('!^[a-z0-9\-]+$!', $tag->tag)) {
+      form_set_error('type', t('The machine-readable name must contain only lowercase letters, numbers, and dashes.'));
+    }
+    // '0' is invalid, since elsewhere we may check it using empty().
+    if ($tag->tag == '0') {
+      form_set_error('tag', t("Invalid machine-readable name. Please enter a name other than %invalid.", array('%invalid' => $tag->tag)));
+    }
+  }
+
+  $names = array_flip($tags);
+
+  if (isset($names[$tag->name]) && $names[$tag->name] != $old_tag) {
+    form_set_error('name', t('The human-readable name %name is already taken.', array('%name' => $tag->name)));
+  }
+}
+
+/**
+ * Implementation of hook_form_submit().
+ */
+function votingapi_tags_tag_form_submit($form, &$form_state) {
+  $op = isset($form_state['values']['op']) ? $form_state['values']['op'] : '';
+
+  $tag = votingapi_tags_tag_set_defaults();
+
+  $tag->tag = trim($form_state['values']['tag']);
+  $tag->name = trim($form_state['values']['name']);
+  $tag->old_tag = isset($form_state['values']['old_tag']) ? $form_state['values']['old_tag'] : $tag->tag;
+  $tag->description = $form_state['values']['description'];
+  $tag->custom = $form_state['values']['custom'];
+  $tag->locked = $form_state['values']['locked'];
+  if (!empty($form_state['values']['old_tag'])) {
+    $tag->is_new = FALSE;
+  }
+  // Handle vote value types.
+  if ($form_state['values']['percent']) {
+    array_push($tag->data_types, 'percent');
+  }
+  if ($form_state['values']['points']) {
+    array_push($tag->data_types, 'points');
+  }
+
+  if ($op == t('Delete voting tag')) {
+    $form_state['redirect'] = 'admin/settings/votingapi/' . str_replace('_', '-', $tag->old_tag) . '/delete';
+    return;
+  }
+
+  $status = votingapi_tags_tag_save($tag);
+
+  $variables = $form_state['values'];
+
+  votingapi_tags_tags_rebuild();
+  menu_rebuild();
+  $t_args = array('%name' => $tag->name);
+
+  if ($op == t('Reset to defaults')) {
+    drupal_set_message(t('The voting tag %name has been reset to its default values.', $t_args));
+    return;
+  }
+
+  if ($status == SAVED_UPDATED) {
+    drupal_set_message(t('The voting tag %name has been updated.', $t_args));
+  }
+  elseif ($status == SAVED_NEW) {
+    drupal_set_message(t('The voting tag %name has been added.', $t_args));
+    watchdog('node', 'Added voting tag %name.', $t_args, WATCHDOG_NOTICE, l(t('view'), 'admin/settings/votingapi'));
+  }
+
+  $form_state['redirect'] = 'admin/settings/votingapi';
+  return;
+}
+
+/**
+ * Delete button submit handler for the tag add/edit form.
+ *
+ * Redirects to the 'delete record' confirmation page without performing any
+ * operations.
+ *
+ * @see votiingapi_tag_form()
+ */
+function votingapi_tags_tag_form_delete($form, &$form_state) {
+  $form_state['redirect'] = 'admin/settings/votingapi/delete/' . $form_state['values']['tag'];
+}
+
+
+/**
+ * Build the delete confirmation form.
+ *
+ * A simple wrapper around Drupal's core confirm_form() function. Adds a value
+ * field to store the ID of the record being deleted.
+ *
+ * @see votingapi_tags_tag_delete_confirm_submit()
+ */
+function votingapi_tags_tag_delete_confirm(&$form_state, $tag) {
+  // Ensure it is not a system type - should never reach here in normal
+  // operations, but just in case...
+  if ($tag->locked) {
+    drupal_set_message(t('%tag is a module defined tag and can not be deleted.', array('%tag' => $tag->name)), 'error');
+    return;
+  }
+  $form['tag'] = array(
+    '#type' => 'value',
+    '#value' => $tag->tag,
+  );
+
+  return confirm_form($form,
+    t('Are you sure you want to delete %title?', array('%title' => $tag->name)),
+    isset($_GET['destination']) ? $_GET['destination'] : 'admin/settings/votingapi',
+    t('This action cannot be undone.'),
+    t('Delete'),
+    t('Cancel')
+  );
+}
+
+/**
+ * Submit handler for the delete confirmation form for a voting tag.
+ *
+ * @see votingapi_tags_tag_form()
+ */
+
+function votingapi_tags_tag_delete_confirm_submit($form, &$form_state) {
+  if ($form_state['values']['confirm']) {
+    votingapi_tags_tag_delete($form_state['values']['tag']);
+    drupal_set_message(t('The voting tag was deleted.'));
+  }
+  $form_state['redirect'] = 'admin/settings/votingapi';
+}
+
Index: votingapi_tags/votingapi_tags.info
===================================================================
RCS file: votingapi_tags/votingapi_tags.info
diff -N votingapi_tags/votingapi_tags.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ votingapi_tags/votingapi_tags.info	18 Feb 2009 19:54:01 -0000
@@ -0,0 +1,5 @@
+; $Id$
+name = Voting API Tags
+description = Provides a shared registry of Voting tags.
+package = Voting
+core = 6.x
Index: votingapi_tags/votingapi_tags.install
===================================================================
RCS file: votingapi_tags/votingapi_tags.install
diff -N votingapi_tags/votingapi_tags.install
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ votingapi_tags/votingapi_tags.install	18 Feb 2009 19:54:01 -0000
@@ -0,0 +1,150 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Installation file for VotingAPI Tags module.
+ */
+
+/**
+ * Implementation of hook_install().
+ */
+function votingapi_tags_install() {
+  drupal_install_schema('votingapi_tags');
+}
+
+/**
+ * Implementation of hook_uninstall().
+ */
+function votingapi_tags_uninstall() {
+  drupal_uninstall_schema('votingapi_tags');
+}
+
+/**
+ * Implementation of hook_schema().
+ */
+function votingapi_tags_schema() {
+  $schema['votingapi_tag'] = array(
+    'description' => 'Stores information about all defined {votingapi_vote} tags.',
+    'fields' => array(
+      'tag' => array(
+        'description' => 'The machine-readable name of this tag.',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ),
+      'name' => array(
+        'description' => 'The human-readable name of this tag.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'description' => array(
+        'description' => 'A brief description of this tag.',
+        'type' => 'text',
+        'not null' => TRUE,
+        'size' => 'medium',
+      ),
+      'custom' => array(
+        'description' => 'A boolean indicating whether this tag is defined by a module (FALSE) or by a user via a module like Voting API (TRUE).',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'size' => 'tiny',
+      ),
+      'locked' => array(
+        'description' => 'A boolean indicating whether the administrator can change the machine name of this tag.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'size' => 'tiny',
+      ),
+    ),
+    'primary key' => array('tag'),
+  );
+  $schema['votingapi_tag_value_type'] = array(
+    'description' => 'Stores information about all defined {votingapi_vote} tags.',
+    'fields' => array(
+      'tag' => array(
+        'description' => 'The {votingapi_tag}.tag value for a tag.',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ),
+      'value_type' => array(
+        'description' => 'A data type, e.g., "percent".',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ),
+    ),
+    'primary key' => array('tag', 'value_type'),
+  );
+  return $schema;
+}
+
+
+function votingapi_tags_update_6101() {
+  $ret = array();
+
+  $schema = array();
+  $schema['votingapi_tag'] = array(
+    'fields' => array(
+      'tag' => array(
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ),
+      'name' => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'description' => array(
+        'type' => 'text',
+        'not null' => TRUE,
+        'size' => 'medium',
+      ),
+      'custom' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'size' => 'tiny',
+      ),
+      'locked' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'size' => 'tiny',
+      ),
+    ),
+    'primary key' => array('tag'),
+  );
+  $schema['votingapi_tag_value_type'] = array(
+    'description' => 'Stores information about all defined {votingapi_vote} tags.',
+    'fields' => array(
+      'tag' => array(
+        'description' => 'The {votingapi_tag}.tag value for a tag.',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ),
+      'value_type' => array(
+        'description' => 'A data type, e.g., "percent".',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ),
+    ),
+    'primary key' => array('tag', 'value_type'),
+  );
+
+  foreach ($schema as $name => $table) {
+    db_create_table($ret, $name, $table);
+  }
+
+  return $ret;
+}
+
Index: votingapi_tags/votingapi_tags.module
===================================================================
RCS file: votingapi_tags/votingapi_tags.module
diff -N votingapi_tags/votingapi_tags.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ votingapi_tags/votingapi_tags.module	18 Feb 2009 19:54:01 -0000
@@ -0,0 +1,361 @@
+<?php
+// $Id$
+
+/**
+ * Implementation of hook_menu().
+ */
+function votingapi_tags_menu() {
+  $items = array();
+
+  $items['admin/settings/votingapi/overview'] = array(
+    'title' => 'List',
+    'weight' => 0,
+    'file' => 'votingapi_tags.admin.inc',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+  );
+  $items['admin/settings/votingapi/add'] = array(
+    'title' => 'Add voting tag',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('votingapi_tags_tag_form'),
+    'access arguments' => array('administer voting api'),
+    'weight' => 5,
+    'file' => 'votingapi_tags.admin.inc',
+    'type' => MENU_LOCAL_TASK,
+  );
+
+  foreach (votingapi_get_tags('tags', NULL, TRUE) as $tag) {
+    $tag_url_str = str_replace('_', '-', $tag->tag);
+
+    $items['admin/settings/votingapi/'. $tag_url_str] = array(
+      'title' => $tag->name,
+      'page callback' => 'drupal_get_form',
+      'page arguments' => array('votingapi_tags_tag_form', $tag),
+      'access arguments' => array('administer voting api'),
+      'file' => 'votingapi_tags.admin.inc',
+      'type' => MENU_CALLBACK,
+    );
+    $items['admin/settings/votingapi/'. $tag_url_str .'/edit'] = array(
+      'title' => $tag->name,
+      'page callback' => 'drupal_get_form',
+      'page arguments' => array('votingapi_tags_tag_form', $tag),
+      'access arguments' => array('administer voting api'),
+      'file' => 'votingapi_tags.admin.inc',
+      'type' => MENU_CALLBACK,
+    );
+    $items['admin/settings/votingapi/'. $tag_url_str .'/delete'] = array(
+      'title' => 'Delete',
+      'page arguments' => array('votingapi_tags_tag_delete_confirm', $tag),
+      'access arguments' => array('administer voting api'),
+      'file' => 'votingapi_tags.admin.inc',
+      'type' => MENU_CALLBACK,
+    );
+  }
+
+  return $items;
+}
+
+/**
+ * Implementation of hook_menu_alter().
+ */
+function votingapi_tags_menu_alter(&$callbacks) {
+  // Add a new 'admin/settings/votingapi/configure' path and make it the same as
+  // the 'admin/settings/votingapi'.  Also set it to be a local task -
+  // needed to make tabs work as expected.
+  $callbacks['admin/settings/votingapi/configure'] = $callbacks['admin/settings/votingapi'];
+  $callbacks['admin/settings/votingapi/configure']['type'] = MENU_LOCAL_TASK;
+  $callbacks['admin/settings/votingapi/configure']['title'] = 'Configure';
+  $callbacks['admin/settings/votingapi/configure']['weight'] = 10;
+
+  $callbacks['admin/settings/votingapi']['description'] = 'Configure voting tags and settings.';
+  $callbacks['admin/settings/votingapi']['file'] = 'votingapi_tags/votingapi_tags.admin.inc';
+  $callbacks['admin/settings/votingapi']['page callback'] = 'votingapi_tags_admin_overview_tags';
+  unset($callbacks['admin/settings/votingapi']['page arguments']);
+}
+
+/**
+ * Implementation of hook_theme().
+ */
+function votingapi_tags_theme() {
+  $items = array();
+  $items['votingapi_tags_admin_overview_tags'] = array(
+    'arguments' => array('name' => NULL, 'tag' => NULL),
+  );
+  return $items;
+}
+
+/**
+ * Set the default values for a votingapi tag.
+ *
+ * The defaults are for a tag defined through hook_votingapi_tag().
+ * When populating a custom tag, $info should have the 'custom'
+ * key set to 1.
+ *
+ * @param $info
+ *   An object or array containing values to override the defaults.
+ *
+ * @return
+ *  A tag object.
+ *
+ * @see node_type_set_defaults()
+ */
+function votingapi_tags_tag_set_defaults($info = array()) {
+  static $tag;
+
+  if (!isset($tag)) {
+    $tag = new stdClass();
+    $tag->tag = '';
+    $tag->name = '';
+    $tag->custom = 0;
+    $tag->locked = 1;
+    $tag->is_new = 1;
+    $tag->data_types = array();
+  }
+
+  $new_tag = clone $tag;
+  $info = (array) $info;
+
+  foreach ($info as $key => $data) {
+    $new_tag->$key = $data;
+  }
+
+  return $new_tag;
+}
+
+/**
+ * Builds and returns the list of available voting tags.
+ *
+ * The list of tags is built by querying hook_votingapi_tags() in all modules, and
+ * by comparing this information with the tags in the {votingapi_tag} table.
+ *
+ * @see _node_types_build()
+ */
+function _votingapi_tags_tags_build() {
+  $_tags = array();
+  $_names = array();
+
+  $tags_array = module_invoke_all('votingapi_tags');
+  foreach ($tags_array as $tag => $info) {
+    $info['tag'] = $tag;
+    $_tags[$tag] = votingapi_tags_tag_set_defaults($info);
+    $_names[$tag] = $info['name'];
+  }
+
+  $tag_result = db_query('SELECT vt.*, vtvt.value_type FROM {votingapi_tag} vt LEFT JOIN {votingapi_tag_value_type} vtvt ON vt.tag = vtvt.tag ORDER BY vt.name ASC');
+  while ($tag_object = db_fetch_object($tag_result)) {
+    $tag = $tag_object->tag;
+    // Check for tags from disabled modules and mark them for disabling.
+    // Tags defined by the votingapi module in the database (rather than by a separate
+    // module using hook_votingapi_tags()) are locked.
+    if ($tag_object->locked && empty($tags_array[$tag])) {
+      $tag_object->disabled = TRUE;
+    }
+    if (!isset($_tags[$tag])) {
+      $_names[$tag] = $tag_object->name;
+      $_tags[$tag] = $tag_object;
+      $_tags[$tag]->data_types = array();
+    }
+    // Add value type to the array; can be more than one row returned per tag if
+    // multiple value types defined for it.
+    if ($tag_object->value_type && !in_array($tag_object->value_type, $_tags[$tag]->data_types)) {
+      array_push($_tags[$tag]->data_types, $tag_object->value_type);
+    }
+    unset($_tags[$tag]->value_type);
+
+    // If we've selected it from the database, it's not new.
+    $_tags[$tag]->is_new = 0;
+  }
+
+  asort($_names);
+
+  return array($_tags, $_names);
+}
+
+/**
+ * Reset the database cache of votingapi tags and save all new or non-modified
+ * module-defined tags to the database.
+ *
+ * @see node_types_rebuild()
+ */
+function votingapi_tags_tags_rebuild() {
+  $tags = votingapi_get_tags();
+
+  foreach ($tags as $tag => $info) {
+    // If the module has been disabled, we don't delete the tag (unlike the
+    // node type system, where such node types are programmatically deleted).
+    // However, we do make the tag 'custom', so that it can be manually deleted
+    // by an admin.
+    if (!empty($info->disabled)) {
+      $info->custom = 1;
+      votingapi_tags_tag_save($info);
+    }
+  }
+
+  // Refresh cached tags.
+  votingapi_get_tags(NULL, NULL, TRUE);
+}
+
+/**
+ * Return tags that support given data types.
+ *
+ * @param $types
+ *   An array of data formats to be supported.
+ *
+ * @return
+ *   An array of all available tags.
+ */
+function votingapi_tags_get_tags_by_data_type($value_types = array()) {
+  $tags = votingapi_get_tags();
+  foreach ($tags as $tag => $info) {
+    if (!array_intersect($valuetypes, $info->value_types)) {
+      unset($tags[$tag]);
+    }
+  }
+  return $tags;
+}
+
+/**
+ * Return tags that support given data types.
+ *
+ * @param $types
+ *   An array of data formats to be supported.
+ *
+ * @return
+ *   An array of all available tags.
+ */
+function votingapi_tags_value_types() {
+  $value_types = array(
+    'percent' => t('percent'),
+    'points' => t('points'),
+    'option' => t('option'),
+  );
+  // Allow other modules to add to or alter the types.
+  return drupal_alter('votingapi_tags_value_types', $value_types);
+}
+
+/**
+ * Implementation of hook_form_alter().
+ *
+ * Add new submit handler for system_modules form.
+ */
+function votingapi_tags_form_system_modules_alter(&$form, $form_state) {
+  $form['#submit'][] = 'votingapi_tags_system_modules_submit';
+}
+
+/**
+ * Rebuild voting tags when the system modules form is submitted to
+ * respond to changes in which modules are enabled.
+ */
+function votingapi_tags_system_modules_submit($form, &$form_state) {
+  votingapi_tags_tags_rebuild();
+}
+
+/**
+ * Load a tag
+ *
+ * @param $tag
+ *   The machine-readable name of a tag.
+ * @return
+ *   A tag in object format, or FALSE if none matched the incoming ID.
+ */
+function votingapi_tags_tag_load($record_id) {
+  $sql = "SELECT * FROM {votingapi_tag} WHERE tag = '%s'";
+  $result = db_query($sql, $tag);
+  if ($tag = db_fetch_object($result)) {
+    return $tag;
+  }
+  else {
+    return FALSE;
+  }
+}
+
+/**
+ * Insert a new record, or update an existing one.
+ *
+ * @param $tag
+ *   A tag to be saved. If $tag['is_new'] isn't set, the tag will be updated.
+ *   Otherwise, a new tag will be inserted into the database.
+ */
+function votingapi_tags_tag_save(&$tag) {
+  $tag = (object) $tag;
+  $status = FALSE;
+
+  // Save new tag to database.
+  if ($tag->is_new) {
+    $status = drupal_write_record('votingapi_tag', $tag);
+    // Only save value types if we successfully saved the tag first.
+    if ($status) {
+      foreach ($tag->data_types as $type) {
+        $tag_type = new stdClass();
+        $tag_type->tag = $tag->tag;
+        $tag_type->value_type = $type;
+        $tag_type->update = FALSE;
+        votingapi_tags_tag_value_type_save($tag_type);
+      }
+    }
+  }
+
+  // Save modified tag to database.
+  else {
+    $status = drupal_write_record('votingapi_tag', $tag, 'tag');
+    // Only save value types if we successfully saved the tag first.
+    if ($status) {
+      // Delete existing value types first, as drupal_write_record() can't
+      // modify existing primary keys.
+      votingapi_tags_tag_value_type_delete($tag->tag);
+
+      // Add the modified list of value types.
+      foreach ($tag->data_types as $type) {
+        $tag_type = new stdClass();
+        $tag_type->tag = $tag->tag;
+        $tag_type->value_type = $type;
+        votingapi_tags_tag_value_type_save($tag_type);
+      }
+    }
+  }
+
+  return $status;
+}
+
+/**
+ * Save a voting api tag value type to the database.
+ *
+ * @param $tag_type
+ *   Array of tag name and value type
+ * @return
+ *  SAVED_NEW or SAVED_UPDATE
+ */
+function votingapi_tags_tag_value_type_save(&$tag_type) {
+  return drupal_write_record('votingapi_tag_value_type', $tag_type);
+}
+
+/**
+ * Delete a voting tag record.
+ *
+ * @param $tag
+ *   The machine-readable name of the tag.
+ */
+function votingapi_tags_tag_delete($tag) {
+  $sql = "DELETE FROM {votingapi_tag} WHERE tag = '%s'";
+  db_query($sql, $tag);
+  votingapi_tags_tag_value_type_delete($tag);
+}
+
+/**
+ * Delete a voting tag value type record.
+ *
+ * @param $tag
+ *   The machine-readable name of the tag.
+ * @param $tag_type
+ *   The value type for a tag.
+ */
+function votingapi_tags_tag_value_type_delete($tag, $tag_type = NULL) {
+  if ($tag_type) {
+    $sql = "DELETE FROM {votingapi_tag_value_type} WHERE tag = '%s' AND value_type = '%s'";
+    db_query($sql, $tag, $tag_type);
+  }
+  else {
+    $sql = "DELETE FROM {votingapi_tag_value_type} WHERE tag = '%s'";
+    db_query($sql, $tag);
+  }
+}
+
Index: votingapi_tags/votingapi_tags.test
===================================================================
RCS file: votingapi_tags/votingapi_tags.test
diff -N votingapi_tags/votingapi_tags.test
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ votingapi_tags/votingapi_tags.test	18 Feb 2009 19:54:01 -0000
@@ -0,0 +1,127 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Test file for VotingAPI Tags module.
+ */
+
+class VotingapiTagsTestCase extends DrupalWebTestCase {
+  /**
+   * Implementation of getInfo().
+   */
+  function getInfo() {
+    return array(
+      'name' => t('Voting API Tags'),
+      'desc' => t('Checks adding, editing and deleting of Voting API tags.'),
+      'group' => t('Voting API Tests'));
+  }
+
+  function setUp() {
+    parent::setUp('votingapi', 'votingapi_tags');
+  }
+
+  /**
+   * Ensure that the optional fields are truly optional.
+   */
+  function testVotingapiTagsConfiguration() {
+    $admin_user = $this->drupalCreateUser(array('administer voting api'));
+    $this->drupalLogin($admin_user);
+
+    // First get admin/settings/votingapi/configure and ensure it contains what
+    // used to be the admin/settings/votingapi form.
+    $this->drupalGet('admin/settings/votingapi/configure');
+    $this->assertText(t('Anonymous vote rollover'), t('Voting API form in new menu location'));
+
+    // Ensure the built-in 'vote' tag is present on admin/settings/votingapi
+    // page.
+    $this->drupalGet('admin/settings/votingapi');
+    $this->assertText(t('(Machine name: vote)'), t('Default vote tag is present.'));
+    // Ensure no 'edit' or 'delete' links appear for built-in tag.  There should
+    // be no custom tags entered yet, so we can just do a simple search.
+    $this->assertNoText(t('edit'), t('Default vote tag can not be modified.'));
+    $this->assertNoText(t('delete'), t('Default vote tag can not be deleted.'));
+
+    // Add a custom votingapi tag.
+    $tag_name = $this->randomName(8);
+    $tag_machine_name = 'machine-name1';
+    $tag_description = $this->randomName(16);
+    $tag = array(
+      'name' => $tag_name,
+      'tag' => $tag_machine_name,
+      'description' => $tag_description,
+      'percent' => 1,
+    );
+    $this->drupalPost('admin/settings/votingapi/add', $tag, t('Save voting tag'));
+    // Ensure the tag was added.
+    $this->assertRaw(t('The voting tag %tag has been added.', array('%tag' => $tag_name)), t('Voting tag added successfully.'));
+    $this->assertText($tag_name, t('Voting tag present in tag listing.'));
+    // Ensure the form redirected correctly.
+    $this->assertEqual($this->getUrl(), url('admin/settings/votingapi', array('absolute' => TRUE)), t('Correct page redirection.'));
+
+    // Try adding an invalid tag - names already in use. Reuse $tag array.
+    $this->drupalPost('admin/settings/votingapi/add', $tag, t('Save voting tag'));
+    $this->assertRaw(t('The human-readable name %name is already taken.', array('%name' => $tag_name)));
+    $this->assertRaw(t('The machine-readable name %tag is already taken.', array('%tag' => $tag_machine_name)));
+    // Try adding an invalid tag - invalid characters in machine name.
+    $tag = array(
+      'name' => $tag_name,
+      'tag' => 'In-valid Nam3',
+    );
+    $this->drupalPost('admin/settings/votingapi/add', $tag, t('Save voting tag'));
+    $this->assertRaw(t('The machine-readable name must contain only lowercase letters, numbers, and dashes.'));
+
+    // Ensure edit link for custom tag works.
+    $this->drupalGet('admin/settings/votingapi');
+    $this->clickLink(t('edit'));
+    $this->assertTitle(t('%tag | Drupal', array('%tag' => $tag_name)), t('Edit link works.'));
+    // Ensure 'percent' vote type is checked, but not 'points'.
+    $elements = $this->xpath('//input[@id="edit-percent"]');
+    $this->assertTrue(isset($elements[0]) && !empty($elements[0]['checked']), t('Percent checkbox checked.'));
+    $elements = $this->xpath('//input[@id="edit-points"]');
+    $this->assertTrue(isset($elements[0]) && empty($elements[0]['checked']), t('Points checkbox unchecked.'));
+    // Ensure there is no input box for editing tag machine name.
+    $elements = $this->xpath('//input[@id="edit-tag"]');
+    $this->assertTrue(empty($elements), t('No tag machine name input box.'));
+
+    // Test editing of tag.
+    $new_tag_name = $this->randomName(8);
+    $tag = array(
+      'name' => $new_tag_name,
+    );
+    $this->drupalPost('admin/settings/votingapi/' . $tag_machine_name . '/edit', $tag, t('Save voting tag'));
+    // Ensure we see the correct message, containing the updated tag name.
+    $this->assertRaw(t('The voting tag %tag has been updated.', array('%tag' => $new_tag_name)), t('Voting tag updated successfully.'));
+    // Ensure the form redirected correctly.
+    $this->assertEqual($this->getUrl(), url('admin/settings/votingapi', array('absolute' => TRUE)), t('Correct page redirection.'));
+
+    // Test the 'Delete voting tag' button on edit form.
+    $this->drupalGet('admin/settings/votingapi/' . $tag_machine_name . '/edit');
+    $tag = array(
+      'name' => $new_tag_name,
+    );
+    $this->drupalPost('admin/settings/votingapi/' . $tag_machine_name . '/edit', $tag, t('Delete voting tag'));
+    // Ensure we see the correct message.
+    $this->assertRaw(t('Are you sure you want to delete %tag', array('%tag' => $new_tag_name)), t('Delete voting tag button on edit form works.'));
+
+    // Don't delete the tag just yet; test 'delete' link on tag listing page.
+    $this->drupalGet('admin/settings/votingapi');
+    $this->clickLink(t('delete'));
+    // Ensure we see the correct message.
+    $this->assertRaw(t('Are you sure you want to delete %tag', array('%tag' => $new_tag_name)), t('Delete voting tag button on edit form works.'));
+    // Delete the tag.
+    $this->drupalPost('admin/settings/votingapi/' . $tag_machine_name . '/delete', array(), t('Delete'));
+    $this->assertText(t('The voting tag was deleted.'), t('Voting tag was deleted.'));
+    // Just to be sure, we check it no longer appears in the listing.
+    $this->assertNoText($new_tag_name, t('Voting tag was deleted successfully.'));
+    // Ensure the form redirected correctly.
+    $this->assertEqual($this->getUrl(), url('admin/settings/votingapi', array('absolute' => TRUE)), t('Correct page redirection.'));
+
+    // Ensure we can't edit a built-in tag, e.g. 'vote'.
+    $this->drupalGet('admin/settings/votingapi/vote/edit');
+    $this->assertRaw(t('%tag is a module defined tag and can not be modified.', array('%tag' => 'Vote')), t('Built-in tag can not be modified.'));
+    // Ensure we can't delete a built-in tag, e.g. 'vote'.
+    $this->drupalGet('admin/settings/votingapi/vote/delete');
+    $this->assertRaw(t('%tag is a module defined tag and can not be deleted.', array('%tag' => 'Vote')), t('Built-in tag can not be deleted.'));
+  }
+}
