diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 27680ceee4ed21b96769b184203850ac690e6c96..b563a7adefa666c297826487f05ac79a327b6580 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -10,6 +10,7 @@ Drupal 7.0, xxxx-xx-xx (development version) hashing and authentication schemes. - Usability: * Implemented drag-and-drop positioning for input format listings. + * Implemented drag-and-drop positioning for poll options. * Provided descriptions for user permissions. - Search: * Added support for language-aware searches. diff --git a/modules/poll/poll.css b/modules/poll/poll.css index 2aaf228641cddd499eee5c5a1bcbbb5e4eca47c3..bc8ddd73fa8588def0e09af806c6326d710a0fa8 100644 --- a/modules/poll/poll.css +++ b/modules/poll/poll.css @@ -33,6 +33,14 @@ .node-form #edit-poll-more { margin: 0; } +.node-form #poll-choice-table .form-text { + display: inline; + width: auto; +} +.node-form #poll-choice-table td.choice-flag { + white-space: nowrap; + width: 4em; +} td.poll-chtext { width: 80%; } diff --git a/modules/poll/poll.install b/modules/poll/poll.install index 3d9b8b9ae8a094fdecd1049293799d4b4ddd3d42..bd4b031b82e6e39a84eebc716e20de4274106629 100644 --- a/modules/poll/poll.install +++ b/modules/poll/poll.install @@ -77,8 +77,9 @@ function poll_schema() { 'default' => 0, 'description' => t('The total number of votes this choice has received by all users.'), ), - 'chorder' => array( + 'weight' => array( 'type' => 'int', + 'size' => 'tiny', 'not null' => TRUE, 'default' => 0, 'description' => t('The sort order of this choice among all choices for the same node.'), @@ -93,6 +94,12 @@ function poll_schema() { $schema['poll_votes'] = array( 'description' => t('Stores per-{users} votes for each {poll}.'), 'fields' => array( + 'chid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => t("The {users}'s vote for this poll."), + ), 'nid' => array( 'type' => 'int', 'unsigned' => TRUE, @@ -106,12 +113,6 @@ function poll_schema() { 'default' => 0, 'description' => t('The {users}.uid this vote is from unless the voter was anonymous.'), ), - 'chorder' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => -1, - 'description' => t("The {users}'s vote for this poll."), - ), 'hostname' => array( 'type' => 'varchar', 'length' => 128, @@ -122,6 +123,7 @@ function poll_schema() { ), 'primary key' => array('nid', 'uid', 'hostname'), 'indexes' => array( + 'chid' => array('chid'), 'hostname' => array('hostname'), 'uid' => array('uid'), ), diff --git a/modules/poll/poll.module b/modules/poll/poll.module index 2a1ab86a23760739ede992134364695bf37560ea..d1d21cc938f371bbcdc6a38e1c7dce526070840f 100644 --- a/modules/poll/poll.module +++ b/modules/poll/poll.module @@ -227,11 +227,23 @@ function poll_form(&$node, $form_state) { ); // Add the current choices to the form. - for ($delta = 0; $delta < $choice_count; $delta++) { - $text = isset($node->choice[$delta]['chtext']) ? $node->choice[$delta]['chtext'] : ''; - $votes = isset($node->choice[$delta]['chvotes']) ? $node->choice[$delta]['chvotes'] : 0; + $delta = 0; + $weight = 0; + if (isset($node->choice)) { + $delta = count($node->choice); + $weight = -$delta; + foreach ($node->choice as $chid => $choice) { + $key = 'chid:'. $chid; + $form['choice_wrapper']['choice'][$key] = _poll_choice_form($key, $choice['chid'], $choice['chtext'], $choice['chvotes'], $choice['weight'], $choice_count); + $weight = ($choice['weight'] > $weight) ? $choice['weight'] : $weight; + } + } - $form['choice_wrapper']['choice'][$delta] = _poll_choice_form($delta, $text, $votes); + // Add initial or additional choices. + $existing_delta = $delta; + for ($delta; $delta < $choice_count; $delta++) { + $key = 'new:'. ($delta - $existing_delta); + $form['choice_wrapper']['choice'][$key] = _poll_choice_form($key, NULL, '', 0, $weight, $choice_count); } // We name our button 'poll_more' to avoid conflicts with other modules using @@ -251,30 +263,30 @@ function poll_form(&$node, $form_state) { ); // Poll attributes - $_duration = array(0 => t('Unlimited')) + drupal_map_assoc(array(86400, 172800, 345600, 604800, 1209600, 2419200, 4838400, 9676800, 31536000), "format_interval"); - $_active = array(0 => t('Closed'), 1 => t('Active')); - - if ($admin) { - $form['settings'] = array( - '#type' => 'fieldset', - '#collapsible' => TRUE, - '#title' => t('Poll settings'), - '#weight' => -3, - ); + $duration = array(0 => t('Unlimited')) + drupal_map_assoc(array(86400, 172800, 345600, 604800, 1209600, 2419200, 4838400, 9676800, 31536000), "format_interval"); + $active = array(0 => t('Closed'), 1 => t('Active')); + + $form['settings'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#title' => t('Poll settings'), + '#weight' => -3, + '#access' => $admin, + ); - $form['settings']['active'] = array( - '#type' => 'radios', - '#title' => t('Poll status'), - '#default_value' => isset($node->active) ? $node->active : 1, - '#options' => $_active, - '#description' => t('When a poll is closed, visitors can no longer vote for it.') - ); - } + $form['settings']['active'] = array( + '#type' => 'radios', + '#title' => t('Poll status'), + '#default_value' => isset($node->active) ? $node->active : 1, + '#options' => $active, + '#description' => t('When a poll is closed, visitors can no longer vote for it.'), + '#access' => $admin, + ); $form['settings']['runtime'] = array( '#type' => 'select', '#title' => t('Poll duration'), '#default_value' => isset($node->runtime) ? $node->runtime : 0, - '#options' => $_duration, + '#options' => $duration, '#description' => t('After this period, the poll will be closed automatically.'), ); @@ -296,7 +308,7 @@ function poll_more_choices_submit($form, &$form_state) { } } -function _poll_choice_form($delta, $value = '', $votes = 0) { +function _poll_choice_form($key, $chid = NULL, $value = '', $votes = 0, $weight = 0, $size = 10) { $admin = user_access('administer nodes'); $form = array( @@ -305,23 +317,33 @@ function _poll_choice_form($delta, $value = '', $votes = 0) { // We'll manually set the #parents property of these fields so that // their values appear in the $form_state['values']['choice'] array. + $form['chid'] = array( + '#type' => 'value', + '#value' => $chid, + '#parents' => array('choice', $key, 'chid'), + ); + $form['chtext'] = array( '#type' => 'textfield', - '#title' => t('Choice @n', array('@n' => ($delta + 1))), '#default_value' => $value, - '#parents' => array('choice', $delta, 'chtext'), + '#parents' => array('choice', $key, 'chtext'), ); - if ($admin) { - $form['chvotes'] = array( - '#type' => 'textfield', - '#title' => t('Votes for choice @n', array('@n' => ($delta + 1))), - '#default_value' => $votes, - '#size' => 5, - '#maxlength' => 7, - '#parents' => array('choice', $delta, 'chvotes'), - ); - } + $form['chvotes'] = array( + '#type' => 'textfield', + '#default_value' => $votes, + '#size' => 5, + '#maxlength' => 7, + '#parents' => array('choice', $key, 'chvotes'), + '#access' => user_access('administer nodes'), + ); + + $form['weight'] = array( + '#type' => 'weight', + '#default_value' => $weight, + '#delta' => $size, + '#parents' => array('choice', $key, 'weight'), + ); return $form; } @@ -330,20 +352,39 @@ function _poll_choice_form($delta, $value = '', $votes = 0) { * Menu callback for AHAH additions. */ function poll_choice_js() { + // Add the new element to the stored form state. Without adding the element + // to the form, Drupal is not aware of this new elements existence and will + // not process it. We retreive the cached form, add the element, and resave. + $form_build_id = $_POST['form_build_id']; + $form_state = array('submitted' => FALSE); + $form = form_get_cache($form_build_id, $form_state); + $delta = count($_POST['choice']); + $key = isset($form['#node']->choice) ? 'new:'. ($delta - count($form['#node']->choice)) : 'new:'. $delta; + + // Match the new choice at the current greatest weight. + $weight = 0; + foreach ($_POST['choice'] as $choice) { + $weight = $choice['weight'] > $weight ? $choice['weight'] : $weight; + } // Build our new form element. - $form_element = _poll_choice_form($delta); + $form_element = _poll_choice_form($key, NULL, NULL, NULL, $weight, $delta + 1); drupal_alter('form', $form_element, array(), 'poll_choice_js'); - // Build the new form. - $form_state = array('submitted' => FALSE); - $form_build_id = $_POST['form_build_id']; - // Add the new element to the stored form. Without adding the element to the - // form, Drupal is not aware of this new elements existence and will not - // process it. We retreive the cached form, add the element, and resave. - $form = form_get_cache($form_build_id, $form_state); - $form['choice_wrapper']['choice'][$delta] = $form_element; + // Dynamically increase the delta of the weight field for every field added. + foreach(element_children($form['choice_wrapper']['choice']) as $n) { + $form['choice_wrapper']['choice'][$n]['weight']['#delta'] = $delta + 1; + } + + // Add the new poll choice. + $form['choice_wrapper']['choice'][$key] = $form_element; + + // Reorder the form to use the same order as post. + $order = array_flip(array_keys($_POST['choice'])); + $form['choice_wrapper']['choice'] = array_merge($order, $form['choice_wrapper']['choice']); + + // Resave the cache. form_set_cache($form_build_id, $form, $form_state); $form += array( '#post' => $_POST, @@ -356,8 +397,8 @@ function poll_choice_js() { // Render the new output. $choice_form = $form['choice_wrapper']['choice']; unset($choice_form['#prefix'], $choice_form['#suffix']); // Prevent duplicate wrappers. - $choice_form[$delta]['#attributes']['class'] = empty($choice_form[$delta]['#attributes']['class']) ? 'ahah-new-content' : $choice_form[$delta]['#attributes']['class'] . ' ahah-new-content'; - $choice_form[$delta]['chvotes']['#value'] = 0; + $choice_form[$key]['#attributes']['class'] = empty($choice_form[$key]['#attributes']['class']) ? 'ahah-new-content' : $choice_form[$key]['#attributes']['class'] .' ahah-new-content'; + $choice_form[$key]['chvotes']['#value'] = 0; $output = theme('status_messages') . drupal_render($choice_form); drupal_json(array('status' => TRUE, 'data' => $output)); @@ -405,22 +446,22 @@ function poll_load($node) { $poll = db_fetch_object(db_query("SELECT runtime, active FROM {poll} WHERE nid = %d", $node->nid)); // Load the appropriate choices into the $poll object. - $result = db_query("SELECT chtext, chvotes, chorder FROM {poll_choices} WHERE nid = %d ORDER BY chorder", $node->nid); + $result = db_query("SELECT chid, chtext, chvotes, weight FROM {poll_choices} WHERE nid = %d ORDER BY weight", $node->nid); while ($choice = db_fetch_array($result)) { - $poll->choice[$choice['chorder']] = $choice; + $poll->choice[$choice['chid']] = $choice; } // Determine whether or not this user is allowed to vote. $poll->allowvotes = FALSE; if (user_access('vote on polls') && $poll->active) { if ($user->uid) { - $result = db_fetch_object(db_query('SELECT chorder FROM {poll_votes} WHERE nid = %d AND uid = %d', $node->nid, $user->uid)); + $result = db_fetch_object(db_query('SELECT chid FROM {poll_votes} WHERE nid = %d AND uid = %d', $node->nid, $user->uid)); } else { - $result = db_fetch_object(db_query("SELECT chorder FROM {poll_votes} WHERE nid = %d AND hostname = '%s'", $node->nid, ip_address())); + $result = db_fetch_object(db_query("SELECT chid FROM {poll_votes} WHERE nid = %d AND hostname = '%s'", $node->nid, ip_address())); } - if (isset($result->chorder)) { - $poll->vote = $result->chorder; + if (isset($result->chid)) { + $poll->vote = $result->chid; } else { $poll->vote = -1; @@ -444,10 +485,9 @@ function poll_insert($node) { db_query("INSERT INTO {poll} (nid, runtime, active) VALUES (%d, %d, %d)", $node->nid, $node->runtime, $node->active); - $i = 0; foreach ($node->choice as $choice) { if ($choice['chtext'] != '') { - db_query("INSERT INTO {poll_choices} (nid, chtext, chvotes, chorder) VALUES (%d, '%s', %d, %d)", $node->nid, $choice['chtext'], $choice['chvotes'], $i++); + db_query("INSERT INTO {poll_choices} (nid, chtext, chvotes, weight) VALUES (%d, '%s', %d, %d)", $node->nid, $choice['chtext'], $choice['chvotes'], $choice['weight']); } } } @@ -459,29 +499,19 @@ function poll_update($node) { // Update poll settings. db_query('UPDATE {poll} SET runtime = %d, active = %d WHERE nid = %d', $node->runtime, $node->active, $node->nid); - // Clean poll choices. - db_query('DELETE FROM {poll_choices} WHERE nid = %d', $node->nid); - - // Poll choices come in the same order with the same numbers as they are in - // the database, but some might have an empty title, which signifies that - // they should be removed. We remove all votes to the removed options, so - // people who voted on them can vote again. - $new_chorder = 0; - foreach ($node->choice as $old_chorder => $choice) { - $chvotes = isset($choice['chvotes']) ? (int)$choice['chvotes'] : 0; - $chtext = $choice['chtext']; - - if (!empty($chtext)) { - db_query("INSERT INTO {poll_choices} (nid, chtext, chvotes, chorder) VALUES (%d, '%s', %d, %d)", $node->nid, $chtext, $chvotes, $new_chorder); - if ($new_chorder != $old_chorder) { - // We can only remove items in the middle, not add, so - // new_chorder is always <= old_chorder, making this safe. - db_query("UPDATE {poll_votes} SET chorder = %d WHERE nid = %d AND chorder = %d", $new_chorder, $node->nid, $old_chorder); + // Poll choices with empty titles signifies removal. We remove all votes to + // the removed options, so people who voted on them can vote again. + foreach ($node->choice as $key => $choice) { + if (!empty($choice['chtext'])) { + if (isset($choice['chid'])) { + db_query("UPDATE {poll_choices} SET chtext = '%s', chvotes = %d, weight = %d WHERE chid = %d", $choice['chtext'], (int)$choice['chvotes'], $choice['weight'], $choice['chid']); + } + else { + db_query("INSERT INTO {poll_choices} (nid, chtext, chvotes, weight) VALUES (%d, '%s', %d, %d)", $node->nid, $choice['chtext'], (int)$choice['chvotes'], $choice['weight']); } - $new_chorder++; } else { - db_query("DELETE FROM {poll_votes} WHERE nid = %d AND chorder = %d", $node->nid, $old_chorder); + db_query("DELETE FROM {poll_votes} WHERE nid = %d AND chid = %d", $node->nid, $key); } } } @@ -607,14 +637,14 @@ function poll_vote($form, &$form_state) { global $user; if ($user->uid) { - db_query('INSERT INTO {poll_votes} (nid, chorder, uid) VALUES (%d, %d, %d)', $node->nid, $choice, $user->uid); + db_query('INSERT INTO {poll_votes} (nid, chid, uid) VALUES (%d, %d, %d)', $node->nid, $choice, $user->uid); } else { - db_query("INSERT INTO {poll_votes} (nid, chorder, hostname) VALUES (%d, %d, '%s')", $node->nid, $choice, ip_address()); + db_query("INSERT INTO {poll_votes} (nid, chid, hostname) VALUES (%d, %d, '%s')", $node->nid, $choice, ip_address()); } // Add one to the votes. - db_query("UPDATE {poll_choices} SET chvotes = chvotes + 1 WHERE nid = %d AND chorder = %d", $node->nid, $choice); + db_query("UPDATE {poll_choices} SET chvotes = chvotes + 1 WHERE chid = %d", $choice); cache_clear_all(); drupal_set_message(t('Your vote was recorded.')); @@ -672,35 +702,40 @@ function poll_view_results(&$node, $teaser, $page, $block) { * @ingroup themeable */ function theme_poll_choices($form) { - // Change the button title to reflect the behavior when using JavaScript. - drupal_add_js('if (Drupal.jsEnabled) { $(document).ready(function() { $("#edit-poll-more").val("' . t('Add another choice') . '"); }); }', 'inline'); + drupal_add_tabledrag('poll-choice-table', 'order', 'sibling', 'poll-weight'); + $delta = 0; $rows = array(); $headers = array( + '', t('Choice'), t('Vote count'), + t('Weight'), ); foreach (element_children($form) as $key) { - // No need to print the field title every time. - unset($form[$key]['chtext']['#title'], $form[$key]['chvotes']['#title']); + $delta++; + // Set special classes for drag and drop updating. + $form[$key]['weight']['#attributes']['class'] = 'poll-weight'; // Build the table row. $row = array( 'data' => array( - array('data' => drupal_render($form[$key]['chtext']), 'class' => 'poll-chtext'), - array('data' => drupal_render($form[$key]['chvotes']), 'class' => 'poll-chvotes'), + array('class' => 'choice-flag'), + drupal_render($form[$key]['chtext']), + drupal_render($form[$key]['chvotes']), + drupal_render($form[$key]['weight']), ), + 'class' => 'draggable', ); - // Add additional attributes to the row, such as a class for this row. - if (isset($form[$key]['#attributes'])) { - $row = array_merge($row, $form[$key]['#attributes']); - } + // Add any additional classes set on the row. + $row['class'] .= isset($form[$key]['#attributes']['class']) ? ' '. $form[$key]['#attributes']['class'] : ''; + $rows[] = $row; } - $output = theme('table', $headers, $rows); + $output = theme('table', $headers, $rows, array('id' => 'poll-choice-table')); $output .= drupal_render($form); return $output; } @@ -784,7 +819,7 @@ function poll_cancel($form, &$form_state) { } // Subtract from the votes. - db_query("UPDATE {poll_choices} SET chvotes = chvotes - 1 WHERE nid = %d AND chorder = %d", $node->nid, $node->vote); + db_query("UPDATE {poll_choices} SET chvotes = chvotes - 1 WHERE chid = %d", $node->vote); } /** diff --git a/modules/poll/poll.pages.inc b/modules/poll/poll.pages.inc index c5895f902e88eb366e27760e95b43e5f7117715d..39d4563983ba2e9359bec87c3e87955a59615e32 100644 --- a/modules/poll/poll.pages.inc +++ b/modules/poll/poll.pages.inc @@ -33,13 +33,14 @@ function poll_votes($node) { $header[] = array('data' => t('Visitor'), 'field' => 'u.name'); $header[] = array('data' => t('Vote'), 'field' => 'pv.chorder'); + $header[] = array('data' => t('Vote'), 'field' => 'pc.weight'); - $result = pager_query("SELECT pv.chorder, pv.uid, pv.hostname, u.name FROM {poll_votes} pv LEFT JOIN {users} u ON pv.uid = u.uid WHERE pv.nid = %d" . tablesort_sql($header), 20, 0, NULL, $node->nid); + $result = pager_query("SELECT pv.chid, pv.uid, pv.hostname, u.name FROM {poll_votes} pv INNER JOIN {poll_choices} pc ON pv.chid = pc.chid LEFT JOIN {users} u ON pv.uid = u.uid WHERE pv.nid = %d". tablesort_sql($header), 20, 0, NULL, $node->nid); $rows = array(); while ($vote = db_fetch_object($result)) { $rows[] = array( $vote->name ? theme('username', $vote) : check_plain($vote->hostname), - check_plain($node->choice[$vote->chorder]['chtext'])); + check_plain($node->choice[$vote->chid]['chtext'])); } $output .= theme('table', $header, $rows); $output .= theme('pager', NULL, 20, 0); diff --git a/modules/system/system.install b/modules/system/system.install index 89c2afde288423455096feb00de6475bd4671693..7e2d240b78bf2cbf0d8592914116caafb2d8dad7 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -2996,6 +2996,27 @@ function system_update_7007() { } +/** + * Use the poll_choice primary key to record votes in poll_votes rather than + * the choice order. Rename chorder to weight. + */ +function system_update_7008() { + $ret = array(); + if (db_table_exists('poll_votes')) { + // Add chid column and convert existing votes. + db_add_field($ret, 'poll_votes', 'chid', array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0)); + db_add_index($ret, 'poll_votes', 'chid', array('chid')); + $ret[] = update_sql("UPDATE {poll_votes} v SET chid = (SELECT chid FROM {poll_choices} c WHERE v.chorder = c.chorder AND v.nid = c.nid)"); + // Remove old chorder column. + db_drop_field($ret, 'poll_votes', 'chorder'); + } + if (db_table_exists('poll_choices')) { + // Change the chorder column to weight in poll_choices. + db_change_field($ret, 'poll_choices', 'chorder', 'weight', array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'tiny')); + } + return $ret; +} + /** * @} End of "defgroup updates-6.x-to-7.x" * The next series of updates should start at 8000.