diff --git a/election-candidate.pages.inc b/election-candidate.pages.inc
index d3a1974ed6e49175e6c71bd9b65a363489b657e7..12c168673346fca3eda81b71d903536b46530c2e 100644
--- a/election-candidate.pages.inc
+++ b/election-candidate.pages.inc
@@ -17,7 +17,7 @@ function election_candidate_page_view(stdClass $election, stdClass $candidate, $
     t(
       'Candidate: @first-name @last-name',
       array(
-        '@first-name' => $candidate->first_name,
+        '@first-name' => $candidatle->first_name,
         '@last-name' => $candidate->last_name,
       )
     ),
@@ -194,6 +194,7 @@ function election_candidate_nomination_form($form, &$form_state, stdClass $elect
   $post_options = array();
   foreach ($available_posts as $post) {
     if ($bypass || $post->nstatus_inheritance == ELECTION_POST_STATUS_INHERIT) {
+      // @todo make this alterable
       $post_options[$post->post_id] = truncate_utf8($post->title, 100, TRUE, TRUE, 80);
     }
   }
@@ -315,12 +316,13 @@ function election_candidate_nomination_form_submit($form, &$form_state) {
 
   // election object won't be changed, no need to pass by reference
   $election = $form_state['election'];
+  $post = election_post_load($values['post_id']);
 
   $values = $form_state['values'];
 
   $candidate->election_id = $election->election_id;
 
-  $candidate->post_id = $values['post'];
+  $candidate->post_id = $values['post_id'];
 
   $candidate->first_name = $values['first_name'];
   $candidate->last_name = $values['last_name'];
@@ -341,6 +343,11 @@ function election_candidate_nomination_form_submit($form, &$form_state) {
   // Save the nomination.
   election_candidate_save($candidate);
 
+  // Trigger a Rules action.
+  if (function_exists('rules_invoke_event')) {
+    rules_invoke_event('election_candidate_nomination_submitted', $candidate, $post, $election);
+  }
+
   // Notify the user that the nomination was saved.
   drupal_set_message(t(
     'The nomination for the @posts_name %title (name %first_name %last_name) was successfully submitted.',
@@ -610,6 +617,50 @@ function election_candidate_edit_form_submit($form, &$form_state) {
 
 }
 
+/**
+ * Page callback for election/%election/candidates.
+ */
+function election_candidate_list_page(stdClass $election) {
+
+ // @todo this should not be necessary
+  drupal_set_title(t('Candidates'), PASS_THROUGH);
+  drupal_set_breadcrumb(
+    _election_build_breadcrumb($election)
+  );
+
+  return array(
+    'totals' => election_candidate_list_totals($election),
+    'list' => drupal_get_form('election_candidate_list_form', $election),
+  );
+
+}
+
+/**
+ * Display total numbers of candidates per post.
+ */
+function election_candidate_list_totals(stdClass $election) {
+
+  $result = db_query(
+    'SELECT ep.post_id, ep.title, COUNT(DISTINCT ec.candidate_id) AS num_candidates FROM {election_post} ep LEFT JOIN {election_candidate} ec USING (post_id) WHERE ep.election_id = :election_id GROUP BY ep.post_id HAVING num_candidates > 0 ORDER BY ep.title',
+    array(
+      ':election_id' => $election->election_id,
+    )
+  );
+  $totals = $result->fetchAllAssoc('post_id');
+
+  $items = array();
+
+  foreach ($totals as $post_id => $total) {
+    $items[] = check_plain($total->title) . ': ' . $total->num_candidates;
+  }
+
+  return array(
+    '#theme' => 'item_list',
+    '#title' => t('Totals'),
+    '#items' => $items,
+  );
+
+}
 
 /**
  * Form builder function for the main administrative election candidate list.
@@ -623,22 +674,16 @@ function election_candidate_edit_form_submit($form, &$form_state) {
  */
 function election_candidate_list_form($form, $form_state, $election) {
 
- // @todo this should not be necessary
-  drupal_set_title(t('Candidates'), PASS_THROUGH);
-  drupal_set_breadcrumb(
-    _election_build_breadcrumb($election)
-  );
-
   $post_name = _election_get_posts_name($election->type);
 
   // Build the sortable table header.
   $header = array(
     'created' => array('data' => t('Nominated'), 'field' => 'ec.created', 'sort' => 'desc'),
-    'post_title' => array('data' => drupal_ucfirst($post_name)),
+    'post_title' => array('data' => drupal_ucfirst($post_name), 'field' => 'ep.title'),
     'first_name' => array('data' => t('First name'), 'field' => 'ec.first_name'),
     'last_name' => array('data' => t('Last name'), 'field' => 'ec.last_name'),
     'username' => array('data' => t('Username'), 'field' => 'ec.username'),
-    'cstatus' => array('data' => t('Status'), 'field' => 'ec.cstatus'),
+    'cstatus' => array('data' => t('Status')),
     'operations' => array('data' => t('Operations')),
   );
 
diff --git a/election.module b/election.module
index 92fbd773108139c1f6dfbffbb89c4400b4f99c94..abcc887080a40214629ad162a44a380440070361 100644
--- a/election.module
+++ b/election.module
@@ -846,8 +846,8 @@ function election_menu() {
 
   $items['election/%election/candidates'] = array(
     'title' => 'Candidates',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('election_candidate_list_form', 1),
+    'page callback' => 'election_candidate_list_page',
+    'page arguments' => array(1),
     'access callback' => 'election_access',
     'access arguments' => array('edit candidates', 1),
     'file' => 'election-candidate.pages.inc',
@@ -1430,38 +1430,55 @@ function election_candidate_get_available_posts(stdClass $election, $nominations
  * See http://api.drupal.org/api/drupal/modules--system--system.api.php/function/hook_token_info/7.
  */
 function election_token_info() {
-  $type = array(
-    'name' => t('Elections'),
-    'description' => t('Tokens related to elections.'),
-    'needs-data' => 'election',
-  );
-  $tokens = array(
-    'id' => array(
-      'name' => t('Election ID'),
-      'description' => t('The unique ID of the election.'),
-    ),
-    'type' => array(
-      'name' => t('Election type'),
-      'description' => t('The machine-readable name of the election type.'),
+  $info = array();
+  $info['types'] = array(
+    'election' => array(
+      'name' => t('Elections'),
+      'description' => t('Tokens related to elections.'),
+      'needs-data' => 'election',
     ),
-    'type-name' => array(
-      'name' => t('Election type name'),
-      'description' => t('The human-readable name of the election type.'),
+    'election_candidate' => array(
+      'name' => t('Candidates'),
+      'description' => t('Tokens related to election candidates.'),
+      'needs-data' => 'election_candidate',
     ),
-    'title' => array(
-      'name' => t('Title'),
-      'description' => t('The title of the election.'),
+  );
+  $info['tokens'] = array(
+    'election' => array(
+      'id' => array(
+        'name' => t('Election ID'),
+        'description' => t('The unique ID of the election.'),
+      ),
+      'type' => array(
+        'name' => t('Election type'),
+        'description' => t('The machine-readable name of the election type.'),
+      ),
+      'type_name' => array(
+        'name' => t('Election type name'),
+        'description' => t('The human-readable name of the election type.'),
+      ),
+      'title' => array(
+        'name' => t('Title'),
+        'description' => t('The title of the election.'),
+      ),
+      'created' => array(
+        'name' => t('Date created'),
+        'description' => t('The date the election was created.'),
+        'type' => 'date',
+      ),
     ),
-    'created' => array(
-      'name' => t('Date created'),
-      'description' => t('The date the election was created.'),
-      'type' => 'date',
+    'election_candidate' => array(
+      'id' => array(
+        'name' => t('Election candidate ID'),
+        'description' => t('The unique ID of the candidate.'),
+      ),
+      'mail' => array(
+        'name' => t('Email address'),
+        'description' => t('The email address of the candidate.'),
+      ),
     ),
   );
-  return array(
-    'types' => array('election' => $type),
-    'tokens' => array('election' => $tokens),
-  );
+  return $info;
 }
 
 /**
@@ -1480,7 +1497,7 @@ function election_tokens($type, array $tokens, array $data = array(), array $opt
         case 'type':
           $replacements[$original] = $sanitize ? check_plain($election->type) : $election->type;
           break;
-        case 'type-name':
+        case 'type_name':
           $type = _election_type_get_info($election->type);
           $type_name = $type['name'];
           $replacements[$original] = $sanitize ? check_plain($type_name) : $type_name;
@@ -1494,6 +1511,19 @@ function election_tokens($type, array $tokens, array $data = array(), array $opt
       }
     }
   }
+  else if ($type == 'election_candidate' && !empty($data['election_candidate'])) {
+    $candidate = $data['election_candidate'];
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        case 'id':
+          $replacements[$original] = $candidate->candidate_id;
+          break;
+        case 'mail':
+          $replacements[$original] = $sanitize ? check_plain($candidate->mail) : $candidate->mail;
+          break;
+      }
+    }
+  }
   return $replacements;
 }