Index: advpoll-form.js
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/advpoll/advpoll-form.js,v
retrieving revision 1.1.2.5
diff -u -p -r1.1.2.5 advpoll-form.js
--- advpoll-form.js	2 Sep 2007 20:31:46 -0000	1.1.2.5
+++ advpoll-form.js	4 Sep 2007 15:24:30 -0000
@@ -5,13 +5,13 @@ if (!Drupal.advpoll) {
 }
 
 // Update maxchoices, called when adding and removing choices
-Drupal.advpoll.maxChoices = function(newChoiceN) {
+Drupal.advpoll.maxChoices = function(numChoices) {
   var selected = $("#edit-settings-maxchoices").val();
   var label = $("#edit-settings-maxchoices").prev();
   // Hard-code the HTML (not clone) as .html() doesn't work for select fields in IE and Opera.
   var newMaxChoices = '<select id="edit-settings-maxchoices" class="form-select" name="settings[maxchoices]">';
   // Build the options
-  for (var i = 0; i <= newChoiceN; i++) {
+  for (var i = 0; i <= numChoices; i++) {
     var name = (i ? i : Drupal.settings.advPoll.noLimit);
     newMaxChoices += '<option ';
     // Set the option user had selected
@@ -50,7 +50,31 @@ Drupal.advpoll.removeChoiceClick = funct
   });
 }
 
+// Show/hide "display write-ins" option when user checks unchecks the write-ins
+// box.
+Drupal.advpoll.updateWriteins = function() {
+  if ($("input.settings-writeins").attr("checked")) {
+    $(".edit-settings-show-writeins").show();
+    $("#edit-settings-show-writeins").removeAttr("disabled");
+  }
+  else {
+    $(".edit-settings-show-writeins").hide();
+    $("#edit-settings-show-writeins").attr("disabled", "disabled");
+  }
+}
+
 Drupal.advpoll.nodeFormAutoAttach = function() {
+  // This code is used on the node edit page and the content-type settings page.
+
+  // Add behavior when write-in box is (un)checked.
+  Drupal.advpoll.updateWriteins();
+  $("input.settings-writeins").click(Drupal.advpoll.updateWriteins);
+
+  if ($("div.poll-form").length == 0) {
+    // We're just on the settings page.
+    return;
+  }
+
   // Hide "need more choices" checkbox
   $("#morechoices").hide();
   
@@ -62,11 +86,15 @@ Drupal.advpoll.nodeFormAutoAttach = func
   var newChoice = $("input.choices:first").parent().clone();
   
   $('<a class="add-choice" href="#">' + Drupal.settings.advPoll.addChoice + '</a>').insertAfter("#morechoices").click(function() {
-    var newChoiceN = $("input.choices").length + 1;
+    var numChoices = $("input.choices").length + 1;
+    // Extract the last choice's offset from its id.
+    var newChoiceN = parseInt($("input.choices:last").id().match(/\d+/)) + 1;
     // If all choices are removed, use a "backup" of the first choice, else clone the first.
     newChoice = ($("input.choices:first").parent().html() ? $("input.choices:first").parent().clone() : newChoice);
     // Replace choice numbers in label, name and id with the new choice number
     newChoice.html(newChoice.html().replace(/\d+(?=<)|\d+(?=-)|\d+(?=\])/g, newChoiceN));
+    // Replace the label to use a more accurate count of choices.
+    $("label", newChoice).html($("label", newChoice).html().replace(/\d+(?=<)|\d+(?=-)|\d+(?=\])/g, numChoices));
     // Clear the value, insert and fade in.
     newChoice.find("input").val("").end().insertBefore("#morechoices").fadeIn();
     // Update hidden form values
@@ -75,7 +103,7 @@ Drupal.advpoll.nodeFormAutoAttach = func
     
     Drupal.advpoll.removeChoiceClick();
     
-    Drupal.advpoll.maxChoices(newChoiceN);
+    Drupal.advpoll.maxChoices(numChoices);
     
     return false;
   });
Index: advpoll-vote.js
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/advpoll/advpoll-vote.js,v
retrieving revision 1.1.2.7
diff -u -p -r1.1.2.7 advpoll-vote.js
--- advpoll-vote.js	1 Dec 2006 15:40:49 -0000	1.1.2.7
+++ advpoll-vote.js	4 Sep 2007 15:24:31 -0000
@@ -52,8 +52,58 @@ Drupal.advpoll.nodeVoteAutoAttach = func
   Drupal.advpoll.attachVoteAjax();
 }
 
+Drupal.advpoll.handleWriteins = function() {
+  $("div.poll").each(function() {
+    var poll = this;
+    var ranOnce = false;
+    // Toggle display of the write-in text box for radios/checkboxes.
+    $(".vote-choices input[@type=radio], .vote-choices input[@type=checkbox]", poll).click(function() {
+      var isLast = $(this).val() == $(".vote-choices input[@type=radio]:last, .vote-choices input[@type=checkbox]:last", poll).val();
+      var type = $(this).attr("type"); 
+      // The logic here is tricky but intentional.
+      if (isLast || type == "radio") {
+        var showChoice = isLast && (type == "radio" || $(this).attr("checked"));
+        if (!ranOnce && showChoice) {
+          // If this is our first time, clone the Drupal-added write-in element
+          // and add a new one next to the checkbox, then delete the old one.
+          $(".writein-choice input", poll).clone().addClass("writein-choice").insertAfter($(this).parent()).end().parent().parent().remove();
+          ranOnce = true;
+        }
+        $(".writein-choice", poll).css("display", showChoice ? "inline" : "none");
+        if (showChoice) {
+          $(".writein-choice", poll)[0].focus();
+        }
+        else {
+          $(".writein-choice", poll).val("");
+        }
+      }
+    });
+  
+    // Toggle display of the write-in text box for select boxes.
+    // Fire on change() rather than click(), for Safari compatibility.
+    $(".vote-choices select:last", poll).change(function() {
+      if (!ranOnce) {
+        // If this is our first time, clone the Drupal-added write-in element
+        // and add a new one next to the checkbox, then delete the old one.
+        $(".writein-choice input", poll).clone().addClass("writein-choice").insertAfter($(this)).end().parent().parent().remove();
+        ranOnce = true;
+      }
+      var showChoice = $(this).val() > 0;
+      var alreadyVisible = $(".writein-choice", poll).css("display") == "inline";
+      $(".writein-choice", poll).css("display", showChoice ? "inline" : "none");
+      if (!showChoice) {
+        $(".writein-choice", poll).val("");
+      }
+      else if (!alreadyVisible) {
+        $(".writein-choice", poll)[0].focus();
+      }
+    });
+  });
+};
+
 if (Drupal.jsEnabled) {
   $(document).ready(function(){  
     Drupal.advpoll.nodeVoteAutoAttach();
+    Drupal.advpoll.handleWriteins();
   });
 };
Index: advpoll.css
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/advpoll/advpoll.css,v
retrieving revision 1.2.2.3
diff -u -p -r1.2.2.3 advpoll.css
--- advpoll.css	30 Jul 2007 20:36:39 -0000	1.2.2.3
+++ advpoll.css	4 Sep 2007 15:24:31 -0000
@@ -10,3 +10,11 @@ a.remove-choice {
   font-size: 0.85em;
   cursor:pointer;
 }
+
+html.js .writein-choice, html.js .edit-settings-show-writeins {
+  display: none;
+}
+
+html.js input.writein-choice {
+  margin-left: 0.6em;
+}
Index: advpoll.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/advpoll/advpoll.install,v
retrieving revision 1.5.2.12
diff -u -p -r1.5.2.12 advpoll.install
--- advpoll.install	1 Sep 2007 22:11:45 -0000	1.5.2.12
+++ advpoll.install	4 Sep 2007 15:24:31 -0000
@@ -20,6 +20,8 @@ function advpoll_install() {
         showvotes tinyint,
         startdate int NOT NULL default '0',
         enddate int NOT NULL default '0',
+        writeins tinyint NOT NULL default '0',
+        show_writeins tinyint NOT NULL default '0',
         PRIMARY KEY (nid)
       ) /*!40100 DEFAULT CHARACTER SET utf8 */");
 
@@ -33,6 +35,7 @@ function advpoll_install() {
         nid int(10) NOT NULL,
         label text NOT NULL,
         vote_offset int(2) unsigned default NULL,
+        writein tinyint NOT NULL default '0',
         PRIMARY KEY (nid, vote_offset),
         KEY vote_offset (vote_offset)
       ) /*!40100 DEFAULT CHARACTER SET utf8 */");
@@ -50,6 +53,8 @@ function advpoll_install() {
         showvotes smallint,
         startdate integer NOT NULL DEFAULT '0',
         enddate integer NOT NULL DEFAULT '0',
+        writeins smallint NOT NULL DEFAULT '0',
+        show_writeins smallint NOT NULL DEFAULT '0',
         PRIMARY KEY (nid)
       )");
 
@@ -63,6 +68,7 @@ function advpoll_install() {
         nid integer NOT NULL,
         label text NOT NULL,
         vote_offset smallint DEFAULT NULL,
+        writein smallint NOT NULL DEFAULT '0',
         PRIMARY KEY (nid, vote_offset)
       )");
       db_query("CREATE INDEX {advpoll_choices}_vote_offset_idx ON {advpoll_choices} (vote_offset)");
@@ -178,3 +184,24 @@ function advpoll_update_3() {
   }
   return $ret;
 }
+
+/**
+ * Add columns for write-in support.
+ */
+function advpoll_update_4() {
+  $ret = array();
+  switch ($GLOBALS['db_type']) {
+    case 'mysql':
+    case 'mysqli':
+      $ret[] = update_sql("ALTER TABLE {advpoll} ADD writeins TINYINT NOT NULL DEFAULT '0'");
+      $ret[] = update_sql("ALTER TABLE {advpoll} ADD show_writeins TINYINT NOT NULL DEFAULT '0'");
+      $ret[] = update_sql("ALTER TABLE {advpoll_choices} ADD writein TINYINT NOT NULL DEFAULT '0'");
+      break;
+    case 'pgsql':
+      db_add_column($ret, 'advpoll', 'writeins', 'smallint', array('default' => 0, 'not null' => TRUE));
+      db_add_column($ret, 'advpoll', 'show_writeins', 'smallint', array('default' => 0, 'not null' => TRUE));
+      db_add_column($ret, 'advpoll_choices', 'writein', 'smallint', array('default' => 0, 'not null' => TRUE));
+      break;    
+  }
+  return $ret;
+}
Index: advpoll.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/advpoll/advpoll.module,v
retrieving revision 1.21.2.60
diff -u -p -r1.21.2.60 advpoll.module
--- advpoll.module	3 Sep 2007 06:51:58 -0000	1.21.2.60
+++ advpoll.module	4 Sep 2007 15:24:31 -0000
@@ -10,6 +10,8 @@ define('ADVPOLL_MAXCHOICES', 0);
 define('ADVPOLL_RUNTIME', 0);
 define('ADVPOLL_ELECTORAL_LIST', 0);
 define('ADVPOLL_SHOWVOTES', 1);
+define('ADVPOLL_WRITEINS', 0);
+define('ADVPOLL_SHOW_WRITEINS', 0);
 // Options: always, aftervote, or afterclose.
 define('ADVPOLL_VIEW_RESULTS', 'aftervote');
 
@@ -176,6 +178,10 @@ function advpoll_form($node, $form_value
     '#tree' => TRUE,
     '#weight' => 1,
   );
+
+  $form['choice']['choice_note'] = array(
+    '#value' => '<div id="edit-settings-choice-note" class="description">'. t('Note: adding or removing choices after voting has begun is not recommended.') .'</div>',
+  );
   
   $form['choice']['morechoices'] = array(
     '#type' => 'checkbox',
@@ -188,13 +194,33 @@ function advpoll_form($node, $form_value
     '#weight' => 1
   );
 
-  for ($a = 1; $a <= $choices; $a++) {
-    $form['choice'][$a]['label'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Choice %n', array('%n' => $a)),
-      '#default_value' => $node->choice[$a]['label'],
-      '#attributes' => array('class' => 'choices'),
-    );
+  // First, loop through any currently existing choices.
+  $current_choices = 0;
+  if (isset($node->choice)) {
+    foreach ($node->choice as $index => $choice) {
+       $form['choice'][$index]['label'] = array(
+        '#type' => 'textfield',
+        '#title' => t('Choice %n', array('%n' => $current_choices + 1)) . ($choice['writein'] ? ' '. t('(write-in)') : ''),
+        '#default_value' => $choice['label'],
+        '#attributes' => array('class' => 'choices'),
+      );
+      $current_choices++;
+      $next_index = $index + 1;
+    }
+  }
+  else {
+    $next_index = 1;
+  }
+
+  // Now add on extra choices if we need to.
+  if ($current_choices < $choices) {
+    for ($index = $next_index; $current_choices < $choices; $index++, $current_choices++) {
+      $form['choice'][$index]['label'] = array(
+        '#type' => 'textfield',
+        '#title' => t('Choice %n', array('%n' => $current_choices + 1)),
+        '#attributes' => array('class' => 'choices'),
+      );
+    }
   }
 
   $form['settings'] = array(
@@ -269,10 +295,30 @@ function advpoll_form($node, $form_value
   // Settings available for users with 'administer polls' permission.
   $default_uselist = isset($node->uselist) ? $node->uselist : variable_get('advpoll_electoral_list_'. $type->type, ADVPOLL_ELECTORAL_LIST);
   $default_showvotes = isset($node->showvotes) ? $node->showvotes : variable_get('advpoll_showvotes_'. $type->type, ADVPOLL_SHOWVOTES);
+  $default_writeins = isset($node->writeins)? $node->writeins : variable_get('advpoll_writeins_'. $type->type, ADVPOLL_WRITEINS);
+  $default_show_writeins = isset($node->show_writeins)? $node->show_writeins : variable_get('advpoll_show_writeins_'. $type->type, ADVPOLL_SHOW_WRITEINS);
   if (user_access('administer polls')) {
     $form['settings']['admin_note'] = array(
       '#value' => '<div id="edit-settings-admin-note" class="description">'. t('The settings below are only available for users with the <em>administer polls</em> permission.') .'</div>',
     );
+
+    $form['settings']['writeins'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Allow users to cast a write-in vote'),
+      '#default_value' => $default_writeins,
+      '#description' => t('Allow voters with the "add write-ins" permission to write-in up to one choice each.'),
+      '#attributes' => array('class' => 'settings-writeins'),
+    );
+
+    $form['settings']['show_writeins'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Display write-in votes as choices for future voters'),
+      '#default_value' => $default_show_writeins,
+      '#description' => t('Allow voters to see and choose from previously submitted write-in votes.'),
+      '#prefix' => '<div class="edit-settings-show-writeins">',
+      '#suffix' => '</div>',
+    );
+
     $form['settings']['uselist'] = array(
       '#type' => 'checkbox',
       '#title' => t('Restrict voting to electoral list'),
@@ -297,15 +343,13 @@ function advpoll_form($node, $form_value
   }
   else {
     // Just pass the values for users without the 'administer polls' permission.
-    $form['settings']['uselist'] = array(
-      '#type' => 'value',
-      '#default_value' => $default_uselist,
-    );
-  
-    $form['settings']['showvotes'] = array(
-      '#type' => 'value',
-      '#default_value' => $default_showvotes,
-    );
+    $defaults = array('uselist' => $default_uselist, 'showvotes' => $default_showvotes, 'writeins' => $default_writeins, 'show_writeins' => $default_show_writeins);
+    foreach ($defaults as $name => $value) {
+      $form['settings'][$name] = array(
+        '#type' => 'value',
+        '#value' => $value,
+      );
+    }
   }
   
   $form['#multistep'] = TRUE;
@@ -321,6 +365,9 @@ function advpoll_form_alter($form_id, &$
     $node_type = $form['old_type']['#value'];
     // Display poll settings if this is an advpoll content type.
     if ($form['module']['#value'] == 'advpoll') {
+      // We need to include the JS and CSS for the show_writeins setting toggle.
+      drupal_add_js(drupal_get_path('module', 'advpoll') .'/advpoll-form.js', 'module');
+      drupal_add_css(drupal_get_path('module', 'advpoll') .'/advpoll.css', 'module');
       $form['advpoll'] = array(
         '#type' => 'fieldset',
         '#title' => t('Poll settings'),
@@ -356,6 +403,23 @@ function advpoll_form_alter($form_id, &$
         '#description' => t('The date the poll was created is used as start date for the default duration. This setting can be overridden on the poll edit page.'),
       );
 
+      $form['advpoll']['writeins'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Allow users to cast a write-in vote by default'),
+        '#default_value' => variable_get('advpoll_writeins_'. $node_type, ADVPOLL_WRITEINS),
+        '#description' => t("Allow voters with the 'add write-ins' permission to write-in up to one choice each. Users with the <em>administer polls</em> permission will be able to override this setting."),
+        '#attributes' => array('class' => 'settings-writeins'),
+      );
+
+      $form['advpoll']['show_writeins'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Display write-in votes as choices for future voters by default'),
+        '#default_value' => variable_get('advpoll_show_writeins_'. $node_type, ADVPOLL_SHOW_WRITEINS),
+        '#description' => t("Allow voters to see and choose from previous voters' write-in votes. Users with the <em>administer polls</em> permission will be able to override this setting."),
+        '#prefix' => '<div class="edit-settings-show-writeins">',
+        '#suffix' => '</div>',
+      );
+
       $form['advpoll']['advpoll_electoral_list'] = array(
         '#type' => 'checkbox',
         '#title' => t('Use electoral list by default'),
@@ -406,7 +470,7 @@ function advpoll_help($section) {
 function advpoll_load($node) {
   global $user;
   $poll = db_fetch_object(db_query("SELECT * FROM {advpoll} WHERE nid = %d", $node->nid));
-  $result = db_query("SELECT vote_offset, label FROM {advpoll_choices} WHERE nid = %d ORDER BY vote_offset", $node->nid);
+  $result = db_query("SELECT vote_offset, label, writein FROM {advpoll_choices} WHERE nid = %d ORDER BY vote_offset", $node->nid);
   while ($choice = db_fetch_array($result)) {
     $poll->choice[$choice['vote_offset']] = $choice;
   }
@@ -552,6 +616,27 @@ function advpoll_menu($may_cache) {
           'weight' => 3,
           'type' => MENU_CALLBACK,
         );
+
+        // Show the write-ins tab if there is at least one.
+        if ($node->writeins) {
+          $has_writeins = FALSE;
+          foreach ($node->choice as $choice) {
+            if ($choice['writein']) {
+              $has_writeins = TRUE;
+              break;
+            }
+          }
+          if ($has_writeins) {
+            $items[] = array(
+              'path' => 'node/'. $nid .'/writeins',
+              'title' => t('Write-ins'),
+              'callback' => 'advpoll_tab_writeins',
+              'access' => user_access('administer polls'),
+              'weight' => 3,
+              'type' => MENU_LOCAL_TASK,
+            );
+          }
+        }
       }
     }
   }
@@ -825,7 +910,7 @@ function advpoll_node_info() {
  * Implementation of hook_perm().
  */
 function advpoll_perm() {
-  return array('create polls', 'edit polls', 'edit own polls', 'vote on polls', 'cancel own vote', 'administer polls', 'inspect all votes', 'access electoral list');
+  return array('create polls', 'edit polls', 'edit own polls', 'vote on polls', 'cancel own vote', 'administer polls', 'inspect all votes', 'access electoral list', 'add write-ins'); 
 }
 
 /**
@@ -844,7 +929,7 @@ function advpoll_cancel_form($nid) {
  */
 function advpoll_update($node) {
 
-  db_query("UPDATE {advpoll} SET active = %d, maxchoices = %d, algorithm = '%s', uselist = %d, showvotes = %d, startdate = '%s', enddate = '%s' WHERE nid = %d", !$node->settings['close'], $node->settings['maxchoices'], $node->settings['algorithm'], $node->settings['uselist'], $node->settings['showvotes'], $node->settings['startdate'] ? strtotime($node->settings['startdate']) : 0, $node->settings['enddate'] ? strtotime($node->settings['enddate']) : 0, $node->nid);
+  db_query("UPDATE {advpoll} SET active = %d, maxchoices = %d, algorithm = '%s', uselist = %d, showvotes = %d, startdate = '%s', enddate = '%s', writeins = %d, show_writeins = %d WHERE nid = %d", !$node->settings['close'], $node->settings['maxchoices'], $node->settings['algorithm'], $node->settings['uselist'], $node->settings['showvotes'], $node->settings['startdate'] ? strtotime($node->settings['startdate']) : 0, $node->settings['enddate'] ? strtotime($node->settings['enddate']) : 0, $node->settings['writeins'], $node->settings['show_writeins'], $node->nid);
 
   _advpoll_insert_choices($node->nid);
   votingapi_recalculate_results('advpoll', $node->nid);
@@ -889,12 +974,11 @@ function _advpoll_is_active($node, $retu
 }
 
 function _advpoll_insert_choices($nid) {
+  $node = node_load($nid);
   db_query('DELETE FROM {advpoll_choices} WHERE nid = %d', $nid);
-  // Start at one rather than 0 due to Drupal FormAPI
-  $i = 1;
-  foreach ($_POST['choice'] as $choice) {
+  foreach ($_POST['choice'] as $index => $choice) {
     if ($choice['label'] != '') {
-      db_query("INSERT INTO {advpoll_choices} (nid, label, vote_offset) VALUES (%d, '%s', %d)", $nid, $choice['label'], $i++);
+      db_query("INSERT INTO {advpoll_choices} (nid, label, vote_offset, writein) VALUES (%d, '%s', %d, %d)", $nid, $choice['label'], $index, isset($node->choice[$index]) ? $node->choice[$index]['writein'] : 0);
     }
   }
 }
@@ -905,7 +989,7 @@ function _advpoll_get_mode($node_type) {
     return $mode[1];
   }
   else {
-    drupal_set_message(t('No mode specified for this content type'), 'error');
+    drupal_set_message(t('No mode specified for content type %type.', array('%type' => $node_type)), 'error');
     return '';
   }
 }
@@ -917,7 +1001,7 @@ function _advpoll_get_mode($node_type) {
  */
 function advpoll_insert($node) {
   $mode = _advpoll_get_mode($node->type);
-  db_query("INSERT INTO {advpoll} (nid, mode, uselist, active, maxchoices, algorithm, showvotes, startdate, enddate) VALUES (%d, '%s', %d, %d, %d, '%s', %d, '%s', '%s')", $node->nid, $mode, $node->settings['uselist'], !$node->settings['close'], $node->settings['maxchoices'], $node->settings['algorithm'], $node->settings['showvotes'], $node->settings['startdate'] ? strtotime($node->settings['startdate']) : 0, $node->settings['enddate'] ? strtotime($node->settings['enddate']) : 0);
+  db_query("INSERT INTO {advpoll} (nid, mode, uselist, active, maxchoices, algorithm, showvotes, startdate, enddate, writeins, show_writeins) VALUES (%d, '%s', %d, %d, %d, '%s', %d, '%s', '%s', %d, %d)", $node->nid, $mode, $node->settings['uselist'], !$node->settings['close'], $node->settings['maxchoices'], $node->settings['algorithm'], $node->settings['showvotes'], $node->settings['startdate'] ? strtotime($node->settings['startdate']) : 0, $node->settings['enddate'] ? strtotime($node->settings['enddate']) : 0, $node->settings['writeins'], $node->settings['show_writeins']);
 
   // Insert the choices
   _advpoll_insert_choices($node->nid);
@@ -1011,17 +1095,11 @@ function advpoll_validate(&$node) {
   }
 }
 
-function advpoll_submit(&$node) {
-  $node->choice = array_values($node->choice);
-  // Start keys at 1 rather than 0
-  array_unshift($node->choice, '');
-  unset($node->choice[0]);
-}
-
 /**
  * Implementation of hook_view().
  */
 function advpoll_view($node, $teaser = FALSE, $page = FALSE) {
+  drupal_add_css(drupal_get_path('module', 'advpoll') .'/advpoll.css', 'module');
   $status = _advpoll_is_active($node, TRUE);
   
   if ($node->in_preview) {
@@ -1069,7 +1147,7 @@ function theme_advpoll_results($title, $
   $output = '<div class="poll">';
   if ($results) {
     $output .= $results;
-    $output .= '<div class="total">'. t('Total votes: %votes', array('%votes' => $votes)) .'</div>';
+    $output .= '<div class="total">'. t('Total voters: %votes', array('%votes' => $votes)) .'</div>';
   }
   else {
     $output .= '<p class="message">'. t('No votes have been recorded for this poll.') .'</p>';
@@ -1086,8 +1164,8 @@ function _advpoll_show_cancel_form($node
   return $output;
 }
 
-function theme_advpoll_bar($title, $percentage, $votes) {
-  $output  = '<div class="text">'. $title .'</div>';
+function theme_advpoll_bar($title, $percentage, $votes, $choice = NULL) {
+  $output  = '<div class="text">'. $title . ($choice && $choice['writein'] ? ' '. t('(write-in)') : '') .'</div>';
   $output .= '<div class="bar"><div style="width: '. $percentage .'%;" class="foreground"></div></div>';
   $output .= '<div class="percent">'. $percentage .'% <span class="votes">('. $votes .')</span></div>';
   return $output;
@@ -1111,6 +1189,26 @@ function _advpoll_vote_response($node, $
   $msg = t('Your vote was registered.');
   // Ajax response
   if ($form_values['ajax']) {
+    // Unset the array of choices so duplicates aren't shown.
+    unset($node->choice);
+    // Get all choices from database. This is necessary to get information about
+    // newly submitted write-in choices.
+    $result = db_query("SELECT vote_offset, label, writein FROM {advpoll_choices} WHERE nid = %d ORDER BY vote_offset", $node->nid);
+    while ($choice = db_fetch_array($result)) {
+      $node->choice[$choice['vote_offset']] = $choice;
+    }
+    // Update the number of choices.
+    $node->choices = count($poll->choice);
+    // Get updated total number of votes from database.
+    $result = db_query("SELECT value FROM {votingapi_cache} WHERE content_type = 'advpoll' AND content_id = %d AND tag = '_advpoll' AND function = 'total_votes'", $node->nid);
+    if (db_num_rows($result) > 0) {
+      $cache = db_fetch_object($result);
+      $node->votes = $cache->value;
+    }
+    else {
+      $node->votes = 0;
+    }
+
     list($node->voted, $node->cancel_vote) = _advpoll_user_voted($node->nid);
     $ajax_output .= advpoll_view_results($node, NULL, NULL);
     // Remove linebreaks as they will break jQuery's insert-HTML methods
@@ -1151,9 +1249,9 @@ function advpoll_view_results(&$node, $t
 function advpoll_cancel($nid) {
   global $user;
   $nid = arg(2);
-  if ($node = node_load(array('nid' => $nid))) {
-    if ($node->voted && _advpoll_is_active($node)) {
-      if ($user->uid && count(votingapi_get_user_votes('advpoll', $node->nid)) > 0) {
+  if ($nid && $node = node_load(array('nid' => $nid))) {
+    if (isset($node->type) && $node->voted && _advpoll_is_active($node)) {
+      if ($user->uid && count($user_vote = votingapi_get_user_votes('advpoll', $node->nid)) > 0) {
         votingapi_unset_vote('advpoll', $node->nid, $user->uid);
       }
       else {
@@ -1161,13 +1259,19 @@ function advpoll_cancel($nid) {
         db_query("DELETE FROM {votingapi_vote} WHERE content_id=%d and hostname = '%s' AND uid=0", $node->nid, $host);
         votingapi_recalculate_results('advpoll', $nid);
       }
+
+      $mode = _advpoll_get_mode($node->type);
+      $function = 'advpoll_cancel_'. $mode;
+      if (function_exists($function)) {
+        $function($node, $user_vote);
+      }
       drupal_set_message(t('Your vote was canceled.'));
     }
     else {
       drupal_set_message(t("You are not allowed to cancel an invalid choice."), 'error');
     }
     drupal_goto('node/'. $nid);
-   }  
+  }  
   else {
     drupal_not_found();
   }
@@ -1245,3 +1349,226 @@ function _advpoll_form_set_error($name =
     return form_set_error('choice[', $message);
   }
 }
+
+/**
+ * Voting form validation logic specific to writeins. This has been abstracted 
+ * away from includes in the modes directory.
+ */
+function _advpoll_writeins_voting_form_validate($node, $writein_option, $writein_text, $ajax) {
+  // Do write-in specific checks if write-ins are enabled and user has permission.
+  if ($node->writeins && user_access('add write-ins')) {
+    // Something is in the write-in textbox.
+    if ($writein_text) {
+      $writein_choice_lower = strtolower($writein_text);
+      foreach ($node->choice as $i => $value) {
+        // Check that user isn't writing in an existing visible choice. (User is
+        // writing in an existing choice and either write-ins are all being 
+        // displayed or the existing choice is not a write-in).
+        if ((strtolower($val['label']) == $writein_choice_lower) && ($node->show_writeins || !$value['writein'])) {
+          _advpoll_form_set_error('writein_choice', t("A write-in vote can not be for an existing choice. Select the choice's option instead."), $ajax);
+        }
+      }
+    }
+
+    // The write-in option is selected and there is nothing in the write-in textbox.
+    if ($writein_option && !$writein_text) {
+      _advpoll_form_set_error('writein_choice', t('If the "write-in" option is selected, a choice must be written in.'), $ajax);
+    }
+
+    // The write-in option is not selected, but there is something in the write-in textbox.
+    if (!$writein_option && $writein_text) {
+      _advpoll_form_set_error('writein_choice', t('If a choice is written in, the "write-in" option must be selected.'), $ajax);
+    }
+  }
+}
+
+/**
+ * Voting form submission logic specific to writeins. This has been abstracted 
+ * away from includes in the modes directory.
+ */
+function _advpoll_writeins_voting_form_submit($node, $form_values, &$vote, $vote_value) {
+  // A write-in vote is being made.
+  if ($form_values['writein_choice']) {
+    // Check if someone has previously voted for this choice.
+    $result = db_query("SELECT vote_offset FROM {advpoll_choices} WHERE nid = %d AND LOWER(label) = LOWER('%s')", $node->nid, $form_values['writein_choice']);
+    // If there's more than one match, redo the query, being more exact.
+    if (db_num_rows($result) > 1) {
+      $result = db_query("SELECT vote_offset FROM {advpoll_choices} WHERE nid = %d AND label = '%s'", $node->nid, $form_values['writein_choice']);      
+    }
+    // If there is at least one match, add a vote for the first one returned. It
+    // should be rare to find more than one choice for any one node with a given
+    // label.
+    if (db_num_rows($result)) {
+      $obj = db_fetch_object($result);
+      $existing_vote_offset = $obj->vote_offset;
+      // Set a vote
+      unset($temp);
+      $temp->value = $vote_value;
+      $temp->tag = $existing_vote_offset;
+      $temp->value_type = 'option';
+      $vote[] = $temp;
+    }
+    // This write-in choice has not been previously voted for.
+    else {
+      // Get last vote offset for this node.
+      $highest_offset = db_result(db_query("SELECT MAX(vote_offset) FROM {advpoll_choices} WHERE nid = %d", $node->nid));
+      $next_offset = $highest_offset ? $highest_offset + 1 : 1;
+
+      // Insert new choice into node.
+      db_query("INSERT INTO {advpoll_choices} (nid, label, vote_offset, writein) VALUES (%d, '%s', %d, 1)", $node->nid, check_plain($form_values['writein_choice']), $next_offset);
+      
+      // Add vote
+      unset($temp);
+      $temp->value = $vote_value;
+      $temp->tag = $next_offset;
+      $temp->value_type = 'option';
+      $vote[] = $temp;
+    }
+  }
+}
+
+function advpoll_tab_writeins() {
+  $node = node_load(arg(1));
+  drupal_set_title(check_plain($node->title));
+  $output .= drupal_get_form('advpoll_writein_promote_form', $node);
+  $output .= drupal_get_form('advpoll_writein_merge_form', $node);
+  echo theme('page', $output);
+}
+
+function advpoll_writein_merge_form($node) {
+  $form = array();
+  $form['fieldset'] = array(
+    '#type' => 'fieldset',
+    '#collapsible' => FALSE,
+    '#title' => t('Merge write-ins'),
+  );
+  $form['fieldset']['note'] = array(
+    '#value' => '<div class="description">'. t('This will delete the write-in and change any votes for it into votes for the selected chocie.') .'</div>',
+  );
+  $form['fieldset']['merge'] = array(
+    '#prefix' => '<div class="container-inline">'. t('Merge') .' ',
+    '#suffix' => '</div>',
+  );
+  $writein_list = array();
+  $choice_list = array();
+  foreach ($node->choice as $index => $choice) {
+    $choice_list[$index] = $choice['label'];
+    if ($choice['writein']) {
+      $writein_list[$index] = $choice['label'];
+    }
+  }
+  $form['fieldset']['merge']['source'] = array(
+    '#type' => 'select',
+    '#options' => $writein_list,
+  );
+  $form['fieldset']['merge']['into'] = array(
+    '#value' => t(' into '),
+  );
+  $form['fieldset']['merge']['destination'] = array(
+    '#type' => 'select',
+    '#options' => $choice_list,
+  );
+  $form['fieldset']['merge']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Merge'),
+  );
+  $form['nid'] = array(
+    '#type' => 'value',
+    '#value' => $node->nid,
+  );
+  return $form;
+}
+
+function advpoll_writein_merge_form_validate($form_id, $form_values) {
+  if ($form_values['source'] == $form_values['destination']) {
+    form_set_error('destination', t('The write-in cannot be merged into itself.'));
+  }
+}
+
+function advpoll_writein_merge_form_submit($form_id, $form_values) {
+  // Get a list of votes in this node.
+  $raw_votes = db_query('SELECT * FROM {votingapi_vote} WHERE content_id = %d', $form_values['nid']);
+  $voters = array();
+  $affected_voters = array();
+  while ($vote = db_fetch_object($raw_votes)) {
+    $key = $vote->uid .'-'. $vote->hostname;
+    if (!isset($voters[$key])) {
+      $voters[$key] = array();
+    }
+    array_push($voters[$key], $vote);
+    if ($vote->tag == $form_values['source']) {
+      // This voter is affected by the merge; save the index of the source vote.
+      $affected_voters[$key] = count($voters[$key]) - 1;
+    }
+  }
+  // Now fix the affected voters.
+  foreach ($affected_voters as $key => $source_index) {
+    // Find out if they voted for the destination or not.
+    $voted_for_destination = FALSE;
+    foreach ($voters[$key] as $index => $vote) {
+      if ($vote->tag == $form_values['destination']) {
+        $voted_for_destination = TRUE;
+        break;
+      }
+    }
+    if ($voted_for_destination) {
+      // Since they already voted for the destination choice,  delete the vote
+      // for the source.
+      db_query('DELETE FROM {votingapi_vote} WHERE vote_id = %d AND tag = %d', $voters[$key][$index]->vote_id, $form_values['source']);
+    }
+    else {
+      // They didn't already vote for the destination, so transfer the vote for
+      // the source to the destination.
+      db_query('UPDATE {votingapi_vote} SET tag = %d WHERE vote_id = %d AND tag = %d', $form_values['destination'], $voters[$key][$index]->vote_id, $form_values['source']);
+    }
+  }
+
+  // Delete the merged choice.
+  db_query('DELETE FROM {advpoll_choices} WHERE vote_offset = %d', $form_values['source']);
+  votingapi_recalculate_results('advpoll', $form_values['nid']);
+  drupal_set_message(t('Write-in merged.'));
+  // Unset destination form element so that drupal_goto() doesn't use it
+  // mistakenly.
+  unset($_REQUEST['destination']);
+  drupal_goto('node/'. $form_values['nid'] .'/writeins');
+}
+
+function advpoll_writein_promote_form($node) {
+  $form = array();
+  $form['fieldset'] = array(
+    '#type' => 'fieldset',
+    '#collapsible' => FALSE,
+    '#title' => t('Promote write-ins'),
+  );
+  $form['fieldset']['note'] = array(
+    '#value' => '<p class="description">'. t('Write-ins can be converted to regular choices. This is useful if users cannot see past write-ins but you want to promote specific write-ins so that they can be seen by users who vote in the future.') .'</p>',
+  );
+  $writein_list = array();
+  foreach ($node->choice as $index => $choice) {
+    if ($choice['writein']) {
+      $writein_list[$index] = $choice['label'];
+    }
+  }
+
+  $form['fieldset']['promote'] = array(
+    '#type' => 'checkboxes',
+    '#options' => $writein_list,
+  );
+  $form['fieldset']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Promote'),
+  );
+  $form['nid'] = array(
+    '#type' => 'value',
+    '#value' => $node->nid,
+  );
+  return $form;
+}
+
+function advpoll_writein_promote_form_submit($form_id, $form_values) {
+  if (count($form_values['promote'])) {
+    db_query('UPDATE {advpoll_choices} SET writein = 0 WHERE nid = %d AND vote_offset IN(%s)', $form_values['nid'], check_plain(implode(', ', $form_values['promote'])));
+    drupal_set_message(format_plural(count($form_values['promote']), 'Write-in promoted.', 'Write-ins promoted.'));
+  }
+  drupal_goto('node/'. $form_values['nid']);
+}
Index: modes/binary.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/advpoll/modes/binary.inc,v
retrieving revision 1.7.2.24
diff -u -p -r1.7.2.24 binary.inc
--- modes/binary.inc	1 Sep 2007 22:11:46 -0000	1.7.2.24
+++ modes/binary.inc	4 Sep 2007 15:24:31 -0000
@@ -33,13 +33,24 @@ function advpoll_voting_binary_form(&$no
     // If previewing check the format against the current users permissions.
     $check = $node->in_preview;
     foreach ($node->choice as $i => $choice) {
-      // Don't show blank choices
-      if ($choice['label']) {
-         $list[$i] = _advpoll_choice_markup($choice['label'], $node->format, $check);
+      // Don't show blank choices or write-in votes if the setting is disabled.
+      if ($choice['label'] && ($node->show_writeins || !$choice['writein'])) {
+         $list[$i] = _advpoll_choice_markup($choice['label'], $node->format, $check) . ($choice['writein'] ? ' '. t('(write-in)') : '');
       }
     }
+    // Add write-in checkbox/radio if write-ins are enabled and user has permission.
+    if ($node->writeins && user_access('add write-ins')) {
+      $list[$i + 1] = t('(write-in)');
+      $form['writein_key'] = array(
+        '#type' => 'value',
+        '#value' => $i + 1,
+      );
+    }
+
     $form['choice'] = array(
       '#options' => $list,
+      '#prefix' => '<div class="vote-choices">',
+      '#suffix' => '</div>',
     );
 
     if ($node->in_preview) {
@@ -59,6 +70,17 @@ function advpoll_voting_binary_form(&$no
     }
   }
 
+  // Add write-in text field if write-ins are enabled and user has permission.
+  if ($node->writeins && user_access('add write-ins')) {
+    $form['writein_choice'] = array (
+      '#prefix' => '<div class="writein-choice">',
+      '#suffix' => '</div>',
+      '#type' => 'textfield',
+      '#title' => t('Write-in vote'),
+      '#size' => 25,
+    );
+  }
+
   $form['nid'] = array(
     '#type' => 'hidden',
     '#value' => $node->nid,
@@ -127,7 +149,7 @@ function advpoll_view_results_binary($no
     foreach ($votes as $i => $count) {
       $choice = $node->choice[$i];
       $percentage = round(100 * $votes[$i] / $total_votes, 0);
-      $output .= theme('advpoll_bar', _advpoll_choice_markup($choice['label'], $node->format, false), $percentage, format_plural($count, '1 vote', '@count votes'));
+      $output .= theme('advpoll_bar', _advpoll_choice_markup($choice['label'], $node->format, false), $percentage, format_plural($count, '1 vote', '@count votes'), $choice);
     }
   }
 
@@ -154,23 +176,33 @@ function advpoll_calculate_results_binar
 function advpoll_voting_binary_form_submit($form_id, $form_values) {
   $vote = array();
   $node = node_load($form_values['nid']);
+
+  // Do submission specific to writeins.
+  _advpoll_writeins_voting_form_submit($node, $form_values, $vote, 1);
+
   if ($node->maxchoices == 1) {
     // Plurality voting
-    $temp->value = 1;
-    $temp->tag = $form_values['choice'];
-    $temp->value_type = 'option';
-    $vote[] = $temp;
+    // Ignore write-in choice that has already been taken care of.
+    if (!$form_values['choice'][$form_values['writein_key']]) {
+      $temp->value = 1;
+      $temp->tag = $form_values['choice'];
+      $temp->value_type = 'option';
+      $vote[] = $temp;
+    }
   }
   else {
     // Approval voting
     foreach ($form_values['choice'] as $choice => $selected) {
-      unset($temp);
-      $temp->value = $choice;
-      if ($selected) {
-        $temp->value_type = 'option';
-        $temp->tag = $choice;
-        $temp->value = 1;
-        $vote[] = $temp;
+      // Ignore write-in choice that has already been taken care of.
+      if ($choice != $form_values['writein_key']) {
+        unset($temp);
+        $temp->value = $choice;
+        if ($selected) {
+          $temp->value_type = 'option';
+          $temp->tag = $choice;
+          $temp->value = 1;
+          $vote[] = $temp;
+        }
       }
     }
   }
@@ -198,6 +230,11 @@ function advpoll_voting_binary_form_vali
     _advpoll_form_set_error('choice[', t('This poll is closed.'), $ajax);
   }
 
+  // Whether the write-in option is selected. This is calculated differently for
+  // radio buttons and checkboxes.
+  $writein_option = FALSE;
+  $writein_text = $form_values['writein_key'] ? $form_values['writein_choice'] : '';
+
   // Check if user has already voted
   list($voted, $cancel_vote) = _advpoll_user_voted($node->nid);
   if ($voted) {
@@ -206,7 +243,14 @@ function advpoll_voting_binary_form_vali
 
   if ($node->maxchoices == 1) {
     // Plurality voting
-    if (!array_key_exists($form_values['choice'], $node->choice)) {
+    // Write-ins are enabled, user has permission, and it's the write-in option.
+    if ($node->writeins && user_access('add write-ins') && ($form_values['choice'] == $form_values['writein_key'])) {
+      // Set the flag to true for additional checks.
+      $writein_option = TRUE;
+    }
+    // The choice is invalid (not between 0 and the write-in key).
+    elseif (!($form_values['choice'] > 0 ) && ($form_values['choice'] < $form_values['writein_key'])) {
+      // Nothing is selected.
       _advpoll_form_set_error('choice[', t('At least one choice must be selected.'), $ajax);
     }
   }
@@ -219,6 +263,14 @@ function advpoll_voting_binary_form_vali
         $numchoices++;
       }
     }
+
+    // Write-ins are enabled, user has permission, and the write-in box is checked.
+    if ($node->writeins && user_access('add write-ins') && $form_values['choice'][$form_values['writein_key']]) {
+      // Add one to number of choices for check on min/max boxes checked.
+      $numchoices++;
+      // Set the flag to true for additional checks.
+      $writein_option = TRUE;
+    }
   
     // Too many choices ranked
     if ($node->maxchoices != 0 && $numchoices > $node->maxchoices) {
@@ -233,6 +285,9 @@ function advpoll_voting_binary_form_vali
       _advpoll_form_set_error('choice[', t('At least one choice must be selected.'), $ajax);
     }
   }
+
+  // Do validation specific to writeins.
+  _advpoll_writeins_voting_form_validate($node, $writein_option, $writein_text, $ajax);
 }
 
 /**
@@ -249,3 +304,27 @@ function theme_advpoll_voting_binary_for
   $output .= "</div>\n";
   return $output;
 }
+
+/**
+ * Hook to handle a cancelled vote for a binary poll.
+ */
+function advpoll_cancel_binary($node, $user_vote) {
+  // Remove choice if this was the last vote for a write-in.
+  if ($node->writeins) {
+    $recalculate = FALSE;
+    foreach ($user_vote as $vote) {
+      if ($node->choice[$vote->tag]['writein']) {
+        // Check if there are any other votes for this write-in.
+        $count = db_result(db_query('SELECT COUNT(1) FROM {votingapi_vote} WHERE content_id = %d AND tag = %d', $node->nid, $vote->tag));
+        if ($count == 0) {
+          // Delete the write-in because no one else voted for it.
+          db_query('DELETE FROM {advpoll_choices} WHERE vote_offset = %d AND nid = %d', $vote->tag, $node->nid);
+          $recalculate = TRUE;
+        }
+      }
+    }
+    if ($recalculate) {
+      votingapi_recalculate_results('advpoll', $node->nid);
+    }
+  }
+}
Index: modes/ranking.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/advpoll/modes/ranking.inc,v
retrieving revision 1.8.2.29
diff -u -p -r1.8.2.29 ranking.inc
--- modes/ranking.inc	1 Sep 2007 22:11:46 -0000	1.8.2.29
+++ modes/ranking.inc	4 Sep 2007 15:24:31 -0000
@@ -64,18 +64,45 @@ function advpoll_voting_ranking_form(&$n
       '#tree' => TRUE,
       // XXX: Workaround for FormAPI bug in PHP 4, see http://drupal.org/node/86657
       '#type' => 'checkboxes',
+      '#prefix' => '<div class="vote-choices">',
+      '#suffix' => '</div>',
     );
 
     foreach ($node->choice as $key => $choice) {
-      // Don't show blank choices
-      if ($choice['label']) {
+      // Don't show blank choices or write-in votes if the setting is disabled.
+      if ($choice['label'] && ($node->show_writeins || !$choice['writein'])) {
         $form['choice'][$key] = array(
           '#type' => 'select',
-          '#title' => _advpoll_choice_markup($choice['label'], $node->format),
+          '#title' => _advpoll_choice_markup($choice['label'], $node->format) . ($choice['writein'] ? ' '. t('(write-in)') : ''),
           '#options' => $choices,
         );
       }
     }
+
+    // Add write-in select box if write-ins are enabled and user has permission.
+    if ($node->writeins && user_access('add write-ins')) {
+      $form['choice'][$key + 1] = array (
+        '#type' => 'select',
+        '#title' => t('(write-in)'),
+        '#options' => $choices,
+      );
+      // Key index of the write-in option
+      $form['writein_key'] = array(
+        '#type' => 'value',
+        '#value' => $key + 1,
+      );
+    }
+  }
+
+  // Add write-in text field if write-ins are enabled and user has permission.
+  if ($node->writeins && user_access('add write-ins')) {
+    $form['writein_choice'] = array (
+      '#prefix' => '<div class="writein-choice">',
+      '#suffix' => '</div>',
+      '#type' => 'textfield',
+      '#title' => t('Write-in vote'),
+      '#size' => 25,
+    );
   }
 
   $form['nid'] = array(
@@ -140,6 +167,9 @@ function advpoll_view_results_ranking($n
         $rounds[$round][$result->function] = $result->value;
       }
       else if (isset($node->choice[$tag])) {
+        // Note: choices that have been removed will not pass the previous
+        // line's test even though their values are still in the vote table.
+
         // Choice-specific cached value
         if ($result->function == 'ranking') {
           $ranking_list[$result->value][] = $tag;
@@ -164,18 +194,19 @@ function advpoll_view_results_ranking($n
     }
 
     if ($node->algorithm == 'borda_count') {
-      for ($i = 0; $i < count($ranking); $i++) {
+      foreach ($ranking as $i => $ranking) {
         $first_one = true;
         $this_rank = '';
 
         // Loop through all choices with this ranking
-        foreach ($ranking[$i]->choices as $choice) {
-          $this_rank .= ($first_one? '' : ', ') . _advpoll_choice_markup($node->choice[$choice]['label'], $node->format, false);
+        foreach ($ranking->choices as $choice) {
+          $label = isset($node->choice[$choice])? _advpoll_choice_markup($node->choice[$choice]['label'], $node->format, FALSE) . ($node->choice[$choice]['writein']? ' '. t('(write-in)') : '') : t('(deleted)');
+          $this_rank .= ($first_one ? '' : ', ') . $label;
           $first_one = false;
         }
 
-        $percentage = round(100 * $ranking[$i]->percentage, 0);
-        $output .= theme('advpoll_bar', $this_rank, $percentage, $ranking[$i]->viewscore);
+        $percentage = round(100 * $ranking->percentage, 0);
+        $output .= theme('advpoll_bar', $this_rank, $percentage, $ranking->viewscore);
 
       }
     }
@@ -190,7 +221,8 @@ function advpoll_view_results_ranking($n
 
         // Loop through all choices with this ranking
         foreach ($ranking[$i]->choices as $choice) {
-          $output .= ($first_one? '' : ', ') . _advpoll_choice_markup($node->choice[$choice]['label'], $node->format, $check);
+          $label = isset($node->choice[$choice])? _advpoll_choice_markup($node->choice[$choice]['label'], $node->format, FALSE) . ($node->choice[$choice]['writein']? ' '. t('(write-in)') : '') : t('(deleted)');
+          $output .= ($first_one? '' : ', ') . $label;
           $first_one = false;
         }
 
@@ -604,19 +636,26 @@ function _advpoll_calculate_instantrunof
  */
 function advpoll_voting_ranking_form_submit($form_id, $form_values) {
   $vote = array();
+  $node = node_load($form_values['nid']);
+
+  // Do submission specific to writeins.
+  _advpoll_writeins_voting_form_submit($node, $form_values, $vote, $form_values['choice'][$form_values['writein_key']]);
+
   foreach ($form_values['choice'] as $choice => $rank) {
-    unset($temp);
-    $temp->value = $rank;
-    // A zero value indicates they didn't rank that choice
-    if ($temp->value != 0) {
-      $temp->value_type = 'option';
-      $temp->tag = $choice;
-      $vote[] = $temp;
+    // Ignore write-in choice that has already been taken care of.
+    if ($choice != $form_values['writein_key']) {
+      unset($temp);
+      $temp->value = $rank;
+      // A zero value indicates they didn't rank that choice.
+      if ($temp->value != 0) {
+        $temp->value_type = 'option';
+        $temp->tag = $choice;
+        $vote[] = $temp;
+      }
     }
   }
 
   votingapi_set_vote('advpoll', $form_values['nid'], $vote);
-  $node = node_load($form_values['nid']);
   _advpoll_vote_response($node, $form_values);
 }
 
@@ -642,6 +681,10 @@ function advpoll_voting_ranking_form_val
     _advpoll_form_set_error('choice[', t('This poll is closed.'), $ajax);
   }
 
+  // Whether the write-in option is selected.
+  $writein_option = FALSE;
+  $writein_text = $form_values['writein_key'] ? $form_values['writein_choice'] : '';
+
   // Check if user has already voted
   list($voted, $cancel_vote) = _advpoll_user_voted($node->nid);
   if ($voted) {
@@ -652,9 +695,17 @@ function advpoll_voting_ranking_form_val
   $setvalues = array();
   
   $numchoices = 0;
+
+  // Write-ins are enabled, user has permission, and the write-in box is checked.
+  if ($node->writeins && user_access('add write-ins') && $form_values['choice'][$form_values['writein_key']]) {
+    $numchoices++;
+    // Set a flag for additional checks.
+    $writein_option = TRUE;
+  }
+
   foreach ($node->choice as $key => $choice) {
     
-    // Count the number of choices that are ranked
+    // Count the number of choices that are ranked.
     if ($form_values['choice'][$key]) {
       $numchoices++;
     }
@@ -668,7 +719,24 @@ function advpoll_voting_ranking_form_val
       _advpoll_form_set_error('choice]['. $key, $message, $ajax);
     }
   }
-  
+
+  // Write-ins are enabled, user has permission, and the write-in box is checked.
+  if ($writein_option) {
+    $intvalue = intval($form_values['choice'][$form_values['writein_key']]);
+    // Mark this value as seen
+    $setvalues[$intvalue]++;
+    // Check range
+    if ($intvalue > count($node->choice) || $intvalue < 0) {
+      // TODO: clean up this error message
+      $message = "Illegal rank for the write-in choice: $intvalue (min: 1, max: ". count($node->choice) .')';
+      _advpoll_form_set_error('choice]['. $form_values['writein_key'], $message, $ajax);
+      $ok = FALSE;
+    }
+  }
+
+  // Do validation specific to writeins.
+  _advpoll_writeins_voting_form_validate($node, $writein_option, $writein_text, $ajax);
+
   // Too many choices ranked
   if ($node->maxchoices != 0 && $numchoices > $node->maxchoices) {
     $message = t('%num choices were selected but only %max are allowed.', array('%num' => $numchoices, '%max' => $node->maxchoices));
@@ -681,7 +749,7 @@ function advpoll_voting_ranking_form_val
     _advpoll_form_set_error('choice', t('At least one choice must be selected.'), $ajax);
   }
 
-  // Check that multiple choices are not set to the same value
+  // Check that multiple choices are not set to the same value.
   foreach ($setvalues as $val => $count) {
     if ($val != 0 && $count > 1) {
       $message = t('Multiple choices given the rank of %value.', array('%value' => $val));
@@ -705,3 +773,27 @@ function theme_advpoll_voting_ranking_fo
   return $output;
 }
 
+/**
+ * Hook to handle a cancelled vote for a ranking poll.
+ */
+function advpoll_cancel_ranking($node, $user_vote) {
+  // Remove choice if this was the last vote for a write-in.
+  if ($node->writeins) {
+    $recalculate = FALSE;
+    foreach ($user_vote as $vote) {
+      if ($node->choice[$vote->tag]['writein']) {
+        // Check if there are any other votes for this write-in.
+        $count = db_result(db_query('SELECT COUNT(1) FROM {votingapi_vote} WHERE content_id = %d AND tag = %d', $node->nid, $vote->tag));
+        if ($count == 0) {
+          // Delete the write-in because no one else voted for it.
+          db_query('DELETE FROM {advpoll_choices} WHERE vote_offset = %d AND nid = %d', $vote->tag, $node->nid);
+          $recalculate = TRUE;
+          watchdog('content', t('Removed write-in choice %choice after the last vote was cancelled.', array('%choice' => $node->choice[$vote->tag]['label'])));
+        }
+      }
+    }
+    if ($recalculate) {
+      votingapi_recalculate_results('advpoll', $node->nid);
+    }
+  }
+}
