diff --git a/private.info b/private.info
index 052ca31..0aec407 100644
--- a/private.info
+++ b/private.info
@@ -1,3 +1,6 @@
 name = Private
 description = Allows users to mark content as private, and hide that content from visitors.
-core = 6.x
+core = 7.x
+package = Node Access
+
+files[] = private_handler_filter_private.inc
diff --git a/private.install b/private.install
index 6bdd309..f12fc04 100644
--- a/private.install
+++ b/private.install
@@ -1,21 +1,41 @@
 <?php
+/**
+ * @file
+ * Install, update and uninstall functions for the private module.
+ */
 
+
+/**
+ * Implements hook_schema().
+ */
 function private_schema() {
   $schema['private'] = array(
     'fields' => array(
-      'nid'     => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
-      'private' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+      'nid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'private' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
     ),
     'primary key' => array('nid'),
   );
-  
-  return $schema;
-}
 
-function private_install() {
-  drupal_install_schema('private');
+  return $schema;
 }
 
+/**
+ * Implements hook_uninstall().
+ */
 function private_uninstall() {
-  drupal_uninstall_schema('private');
+  // Remove variables.
+  $node_types = array_keys(node_type_get_types());
+  foreach ($node_types as $node_type) {
+    variable_del('private_' . $node_type);
+  }
 }
diff --git a/private.module b/private.module
index 25336f5..83279c1 100644
--- a/private.module
+++ b/private.module
@@ -11,26 +11,27 @@ define('PRIVATE_DISABLED', 0);
 define('PRIVATE_ALLOWED', 1);
 define('PRIVATE_AUTOMATIC', 2);
 define('PRIVATE_ALWAYS', 3);
+define('PRIVATE_GRANT_ALL', 1);
 
 /**
- * Implementation of hook_enable().
+ * Implements hook_enable().
  *
  * A node access module needs to force a rebuild of the node access table
  * when it is enabled to ensure that things are set up.
  */
 function private_enable() {
-  node_access_rebuild(TRUE);
+  node_access_needs_rebuild(TRUE);
 }
 
 /**
- * Implementation of hook_disable().
+ * Implements hook_disable().
  *
  * A node access module needs to force a rebuild of the node access table
  * when it is disabled to ensure that its entries are removed from the table.
  */
 function private_disable() {
   private_disabling(TRUE);
-  node_access_rebuild(TRUE);
+  node_access_needs_rebuild(TRUE);
 }
 
 /**
@@ -42,21 +43,35 @@ function private_disabling($set = NULL) {
   if ($set !== NULL) {
     $disabling = $set;
   }
+
   return $disabling;
 }
 
 /**
- * Implementation of hook_perm().
+ * Implements hook_permission().
  *
  * In this example, we will use a simple permission to determine whether a user
  * has access to "private" content. This permission is defined here.
  */
-function private_perm() {
-  return array('mark content as private', 'access private content', 'edit private content');
+function private_permission() {
+  return array(
+    'mark content as private' => array(
+      'title' => t('Mark content as private'),
+      'description' => t('Make content only viewable by people with access to view private content'),
+    ),
+    'access private content' => array(
+      'title' => t('Access private content'),
+      'description' => t('Access any content marked as private'),
+    ),
+    'edit private content' => array(
+      'title' => t('Edit private content'),
+      'description' => t('Edit content marked as private'),
+    ),
+  );
 }
 
 /**
- * Implementation of hook_node_grants().
+ * Implements hook_node_grants().
  *
  * Tell the node access system what GIDs the user belongs to for each realm.
  * In this example, we are providing two realms: the example realm, which
@@ -69,26 +84,26 @@ function private_perm() {
  *
  */
 function private_node_grants($account, $op) {
+  // First grant a grant to the author for own content.
+  $grants['private_author'] = array($account->uid);
+
   if ($op == 'view' && user_access('access private content', $account)) {
-    $grants['private'] = array(1);
+    $grants['private_view'] = array(PRIVATE_GRANT_ALL);
   }
 
   if (($op == 'update' || $op == 'delete') && user_access('edit private content', $account)) {
-    $grants['private'] = array(1);
+    $grants['private_edit'] = array(PRIVATE_GRANT_ALL);
   }
 
-  $grants['private_author'] = array($account->uid);
   return $grants;
 }
 
 /**
- * Implementation of hook_node_access_records().
+ * Implements hook_node_access_records().
  *
  * All node access modules must implement this hook. If the module is
  * interested in the privacy of the node passed in, return a list
- * of node access values for each grant ID we offer. Since this
- * example module only offers 1 grant ID, we will only ever be
- * returning one record.
+ * of node access values for each grant ID we offer.
  */
 function private_node_access_records($node) {
   if (private_disabling()) {
@@ -97,57 +112,62 @@ function private_node_access_records($node) {
 
   // We only care about the node if it's been marked private. If not, it is
   // treated just like any other node and we completely ignore it.
-  if ($node->private) {
+  if (isset($node->private) && $node->private == 1) {
     $grants = array();
     $grants[] = array(
-      'realm' => 'private',
-      'gid' => TRUE,
-      'grant_view' => TRUE,
-      'grant_update' => FALSE,
-      'grant_delete' => FALSE,
+      'realm' => 'private_view',
+      'gid' => PRIVATE_GRANT_ALL,
+      'grant_view' => 1,
+      'grant_update' => 0,
+      'grant_delete' => 0,
+      'priority' => 0,
+    );
+    $grants[] = array(
+      'realm' => 'private_edit',
+      'gid' => PRIVATE_GRANT_ALL,
+      'grant_view' => 1,
+      'grant_update' => 1,
+      'grant_delete' => 1,
       'priority' => 0,
     );
-
-    // For the example_author array, the GID is equivalent to a UID, which
-    // means there are many many groups of just 1 user.
     $grants[] = array(
       'realm' => 'private_author',
       'gid' => $node->uid,
-      'grant_view' => TRUE,
-      'grant_update' => TRUE,
-      'grant_delete' => TRUE,
+      'grant_view' => 1,
+      'grant_update' => 1,
+      'grant_delete' => 1,
       'priority' => 0,
     );
+
     return $grants;
   }
 }
 
 /**
- * Implementation of hook_form_alter()
+ * Implements hook_form_alter().
  *
  * This module adds a simple checkbox to the node form labeled private. If the
  * checkbox is labelled, only the node author and users with 'access private content'
  * privileges may see it.
  */
-function private_form_alter(&$form, $form_state, $form_id) {
-  if ($form['#id'] == 'node-form') {
+function private_form_alter(&$form, &$form_state, $form_id) {
+  if (!empty($form['#node_edit_form'])) {
     $node = $form['#node'];
-    $default = variable_get('private_'. $node->type, PRIVATE_ALLOWED);
+    $default = variable_get('private_' . $node->type, PRIVATE_ALLOWED);
 
     if ($default != PRIVATE_DISABLED || !empty($node->privacy)) {
       if (empty($node->nid)) {
         $privacy = ($default > PRIVATE_ALLOWED);
       }
       else {
-        $privacy = $node->private;
+        $privacy = isset($node->private) ? $node->private : 0;
       }
 
       if (user_access('mark content as private') && $default != PRIVATE_ALWAYS) {
-        $form['private'] = array(
+        $form['options']['private'] = array(
           '#type' => 'checkbox',
           '#title' => t('Make this post private'),
-          '#return_value' => 1,
-          '#description' => t('When checked, only users with proper access permissions will be able to see this post.'),
+          '#attributes' => array('title' => t('When checked, only users with proper access permissions will be able to see this post.')),
           '#default_value' => $privacy,
         );
       }
@@ -159,9 +179,13 @@ function private_form_alter(&$form, $form_state, $form_id) {
       }
     }
   }
-  elseif($form_id == 'node_type_form') {
-    $node_type = (array)$form['#node_type'];
-    $type = $node_type['type'];
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function private_form_node_type_form_alter(&$form, &$form_state, $form_id) {
+  if (isset($form['type'])) {
     $form['workflow']['private'] = array(
       '#type' => 'radios',
       '#title' => t('Privacy'),
@@ -171,87 +195,127 @@ function private_form_alter(&$form, $form_state, $form_id) {
         PRIVATE_AUTOMATIC => t('Enabled (private by default)'),
         PRIVATE_ALWAYS => t('Hidden (always private)'),
       ),
-      '#default_value' => variable_get('private_'. $type, PRIVATE_ALLOWED),
+      '#default_value' => variable_get('private_' . $form['#node_type']->type, PRIVATE_ALLOWED),
     );
   }
 }
 
 /**
- * Implementation of hook_nodeapi().
- *
- * - "delete", "insert", and "update":
- * The module must track the access status of the node.
+ * Implements hook_node_load().
  */
-function private_nodeapi(&$node, $op, $arg = 0) {
-  switch ($op) {
-    case 'load':
-      $result = db_fetch_object(db_query('SELECT * FROM {private} WHERE nid = %d', $node->nid));
-      $node->private = $result->private;
-      break;
-    case 'delete':
-      db_query('DELETE FROM {private} WHERE nid = %d', $node->nid);
-      break;
-    case 'insert':
-    case 'update':
-      db_query('UPDATE {private} SET private = %d WHERE nid = %d', $node->private, $node->nid);
-      if (!db_affected_rows()) {
-        db_query('INSERT INTO {private} (nid, private) VALUES (%d, %d)', $node->nid, $node->private);
-      }
-      break;
+function private_node_load($nodes, $types) {
+  $result = db_query('SELECT * FROM {private} WHERE nid IN(:nids)', array(':nids' => array_keys($nodes)));
+  foreach ($result as $record) {
+    $nodes[$record->nid]->private = $record->private;
+  }
+}
+
+/**
+ * Implements hook_node_delete().
+ */
+function private_node_delete($node) {
+  db_delete('private')
+    ->condition('nid', $node->nid)
+    ->execute();
+}
+
+/**
+ * Implements hook_node_insert().
+ */
+function private_node_insert($node) {
+  private_node_update($node);
+}
+
+/**
+ * Implements hook_node_update().
+ */
+function private_node_update($node) {
+  if (isset($node->private)) {
+    db_merge('private')
+      ->key(array('nid' => $node->nid))
+      ->fields(array(
+        'nid' => $node->nid,
+        'private' => (int)$node->private,
+      ))
+      ->execute();
+
+    drupal_set_message(t('@type %title has private set to "%private".', array('@type' => node_type_get_name($node), '%title' => $node->title, '%private' => !empty($node->private) ? t('Yes') : t('No'))));
   }
 }
 
 /**
-* Implementation of hook_file_download().
-*/
+ * Implements hook_file_download().
+ */
 function private_file_download($file) {
-  $file = file_create_path($file);
-  $result = db_query("SELECT f.* FROM {files} f WHERE filepath = '%s'", $file);
-  if ($file = db_fetch_object($result)) {
-    $node = node_load($file->nid);
-    if ($node->private == 1) {
+  $file = file_prepare_directory($file);
+  $result = db_query("SELECT f.* FROM {files} f WHERE filepath = :filepath", array(':filepath' => $file));
+  foreach ($result as $record) {
+    if ($record) {
+      $node = node_load($record->nid);
+      if (isset($node->private) && $node->private == 1) {
         if (node_access('view', $node) == FALSE) {
-            return -1;
+          return -1;
         }
       }
+    }
   }
 }
 
-function private_link($type, $node = NULL, $teaser = FALSE) {
-  if ($type == 'node' && $node->private) {
-    $links['private_icon']['title'] = theme('private_node_link', $node);
+/**
+ * Implements hook_node_view().
+ */
+function private_node_view($node, $view_mode) {
+  if (isset($node->private) && $node->private == 1) {
+    $links['private_icon']['title'] = theme('private_node_link');
     $links['private_icon']['html'] = TRUE;
-    return $links;
+    $node->content['links'][$node->type] = array(
+      '#theme' => 'links__node__' . $node->type,
+      '#links' => $links,
+      '#attributes' => array('class' => array('links', 'inline')),
+    );
   }
 }
 
+/**
+ * Implements hook_theme().
+ */
 function private_theme() {
-  return array('private_node_link' => array('arguments' => array('node' => NULL)));
+  return array(
+    'private_node_link' => array(
+      'variables' => array(),
+    ),
+  );
 }
 
-function theme_private_node_link($node) {
-  return theme('image', drupal_get_path('module', 'private') . '/icon_key.gif', t('Private'), t('This content is private'));
+/**
+ * Custom theme function
+ * @see private_theme()
+ */
+function theme_private_node_link() {
+  return theme('image', array('path' => drupal_get_path('module', 'private') . '/icon_key.gif', 'width' => '16', 'height' => '16', 'alt' => t('Private'), 'title' => t('This content is private.')));
 }
 
 /**
- * Implementation of hook_action_info().
+ * Implements hook_action_info().
  */
 function private_action_info() {
   return array(
     'private_set_private_action' => array(
       'type' => 'node',
-      'description' => t('Make post private'),
+      'label' => t('Make post private'),
       'configurable' => FALSE,
-      'hooks' => array(
-        'nodeapi' => array('insert', 'update'),
+      'triggers' => array(
+        'nodeapi_insert',
+        'nodeapi_update',
       ),
     ),
     'private_set_public_action' => array(
       'type' => 'node',
-      'description' => t('Make post public'),
+      'label' => t('Make post public'),
       'configurable' => FALSE,
-      'hooks' => array(
-        'nodeapi' => array('insert', 'update'),
+      'triggers' => array(
+        'nodeapi_insert',
+        'nodeapi_update',
       ),
     ),
   );
@@ -278,7 +342,7 @@ function private_set_private_action(&$node, $context = array()) {
 }
 
 /**
- * Implementation of hook_node_operations().
+ * Implements hook_node_operations().
  */
 function private_node_operations() {
   $operations = array(
@@ -299,10 +363,13 @@ function private_node_operations() {
  */
 function private_node_mark_private($nids) {
   foreach ($nids as $nid) {
-    db_query('UPDATE {private} SET private = %d WHERE nid = %d', 1, $nid);
-    if (!db_affected_rows()) {
-      db_query('INSERT INTO {private} (nid, private) VALUES (%d, %d)', $nid, 1);
-    }
+    db_merge('private')
+      ->key(array('nid' => $nid))
+      ->fields(array(
+        'nid' => $nid,
+        'private' => 1,
+      ))
+      ->execute();
   }
 }
 
@@ -311,10 +378,13 @@ function private_node_mark_private($nids) {
  */
 function private_node_mark_public($nids) {
   foreach ($nids as $nid) {
-    db_query('UPDATE {private} SET private = %d WHERE nid = %d', 0, $nid);
-    if (!db_affected_rows()) {
-      db_query('INSERT INTO {private} (nid, private) VALUES (%d, %d)', $nid, 0);
-    }
+    db_merge('private')
+      ->key(array('nid' => $nid))
+      ->fields(array(
+        'nid' => $nid,
+        'private' => 0,
+      ))
+      ->execute();
   }
 }
 
@@ -326,4 +396,4 @@ function private_views_api() {
     'api' => 2,
     'path' => drupal_get_path('module', 'private'),
   );
-}
\ No newline at end of file
+}
diff --git a/private.test b/private.test
new file mode 100644
index 0000000..f70190a
--- /dev/null
+++ b/private.test
@@ -0,0 +1,164 @@
+<?php
+
+/**
+ * @file
+ * Tests for private module.
+ */
+class PrivateTestCase extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Private functionality',
+      'description' => 'Checks behavior of Private.',
+      'group' => 'Examples',
+    );
+  }
+
+  /**
+   * Enable modules and create user with specific permissions.
+   */
+  public function setUp() {
+    parent::setUp('private', 'search');
+    node_access_rebuild();
+  }
+
+  /**
+   * Test the "private" node access.
+   *
+   * - Create 3 users with "access content" and "create article" permissions.
+   * - Each user creates one private and one not private article.
+   * - Run cron to update search index.
+   * - Test that each user can view the other user's non-private article.
+   * - Test that each user cannot view the other user's private article.
+   * - Test that each user finds only appropriate (non-private + own private)
+   *   in search results.
+   * - Create another user with 'view private content'.
+   * - Test that user 4 can view all content created above.
+   * - Test that user 4 can search for all content created above.
+   * - Test that user 4 cannot edit private content above.
+   * - Create another user with 'edit private content'
+   * - Test that user 5 can edit private content.
+   * - Test that user 5 can delete private content.
+   * - Test listings of nodes with 'node_access' tag on database search.
+   */
+  function testNodeAccessBasic() {
+    $num_simple_users = 3;
+    $simple_users = array();
+
+    // nodes keyed by uid and nid: $nodes[$uid][$nid] = $is_private;
+    $nodes_by_user = array();
+    $titles = array(); // Titles keyed by nid
+    $private_nodes = array(); // Array of nids marked private.
+    for ($i = 0; $i < $num_simple_users; $i++) {
+      $simple_users[$i] = $this->drupalCreateUser(array('access content', 'create article content', 'search content'));
+    }
+    foreach ($simple_users as $web_user) {
+      $this->drupalLogin($web_user);
+      foreach (array(0 => 'Public', 1 => 'Private') as $is_private => $type) {
+        $edit = array(
+          'title' => t('@private_public Article created by @user', array('@private_public' => $type, '@user' => $web_user->name)),
+        );
+        if ($is_private) {
+          $edit['private'] = TRUE;
+          $edit['body[und][0][value]'] = 'private node';
+        }
+        else {
+          $edit['body[und][0][value]'] = 'public node';
+        }
+        $this->drupalPost('node/add/article', $edit, t('Save'));
+        debug(t('Created article with private=@private', array('@private' => $is_private)));
+        $this->assertText(t('Article @title has been created', array('@title' => $edit['title'])));
+        $nid = db_query('SELECT nid FROM {node} WHERE title = :title', array(':title' => $edit['title']))->fetchField();
+        $this->assertText(t('New node @nid was created and private=@private', array('@nid' => $nid, '@private' => $is_private)));
+        $private_status = db_query('SELECT private FROM {private} where nid = :nid', array(':nid' => $nid))->fetchField();
+        $this->assertTrue($is_private == $private_status, t('Node was properly set to private or not private in private table.'));
+        if ($is_private) {
+          $private_nodes[] = $nid;
+        }
+        $titles[$nid] = $edit['title'];
+        $nodes_by_user[$web_user->uid][$nid] = $is_private;
+      }
+    }
+    debug($nodes_by_user);
+    $this->cronRun();  // Build the search index.
+    foreach ($simple_users as $web_user) {
+      $this->drupalLogin($web_user);
+      // Check to see that we find the number of search results expected.
+      $this->checkSearchResults('Private node', 1);
+      // Check own nodes to see that all are readable.
+      foreach (array_keys($nodes_by_user) as $uid) {
+        // All of this user's nodes should be readable to same.
+        if ($uid == $web_user->uid) {
+          foreach ($nodes_by_user[$uid] as $nid => $is_private) {
+            $this->drupalGet('node/' . $nid);
+            $this->assertResponse(200);
+            $this->assertTitle($titles[$nid] . ' | Drupal', t('Correct title for node found'));
+          }
+        }
+        else {
+          // Otherwise, for other users, private nodes should get a 403,
+          // but we should be able to read non-private nodes.
+          foreach ($nodes_by_user[$uid] as $nid => $is_private) {
+            $this->drupalGet('node/' . $nid);
+            $this->assertResponse($is_private ? 403 : 200, t('Node @nid by user @uid should get a @response for this user (@web_user_uid)', array('@nid' => $nid, '@uid' => $uid, '@response' => $is_private ? 403 : 200, '@web_user_uid' => $web_user->uid)));
+            if (!$is_private) {
+              $this->assertTitle($titles[$nid] . ' | Drupal', t('Correct title for node was found'));
+            }
+          }
+        }
+      }
+
+      // Check to see that the correct nodes are shown on examples/node_access.
+      $this->drupalGet('examples/node_access');
+      $accessible = $this->xpath("//tr[contains(@class,'accessible')]");
+      $this->assertEqual(count($accessible), 1, t('One private item accessible'));
+      foreach ($accessible as $row) {
+        $this->assertEqual($row->td[2], $web_user->uid, t('Accessible row owned by this user'));
+      }
+    }
+
+    // Now test that a user with 'access private content' can view content.
+    $access_user = $this->drupalCreateUser(array('access content', 'create article content', 'access private content', 'search content'));
+    $this->drupalLogin($access_user);
+
+    // Check to see that we find the number of search results expected.
+    $this->checkSearchResults('Private node', 3);
+
+    foreach ($nodes_by_user as $uid => $private_status) {
+      foreach ($private_status as $nid => $is_private) {
+        $this->drupalGet('node/' . $nid);
+        $this->assertResponse(200);
+      }
+    }
+
+    // Test that a privileged user can edit and delete private content.
+    // This test should go last, as the nodes get deleted.
+    $edit_user = $this->drupalCreateUser(array('access content', 'access private content', 'edit private content'));
+    $this->drupalLogin($edit_user);
+    foreach ($private_nodes as $nid) {
+      $body = $this->randomName();
+      $edit = array('body[und][0][value]' => $body);
+      $this->drupalPost('node/' . $nid . '/edit', $edit, t('Save'));
+      $this->assertText(t('has been updated'));
+      $this->drupalPost('node/' . $nid . '/edit', array(), t('Delete'));
+      $this->drupalPost(NULL, array(), t('Delete'));
+      $this->assertText(t('has been deleted'));
+    }
+
+
+  }
+
+  /**
+   * On the search page, search for a string and assert the expected number
+   * of results.
+   * @param $search_query
+   *   String to search for
+   * @param $expected_result_count
+   *   Expected result count
+   */
+  function checkSearchResults($search_query, $expected_result_count) {
+    $this->drupalPost('search/node', array('keys' => $search_query), t('Search'));
+    $search_results = $this->xpath("//ol[contains(@class, 'search-results')]/li");
+    $this->assertEqual(count($search_results), $expected_result_count, t('Found the expected number of search results'));
+  }
+}
diff --git a/private.views.inc b/private.views.inc
index 45dd9ee..9deedf3 100644
--- a/private.views.inc
+++ b/private.views.inc
@@ -1,9 +1,17 @@
 <?php
 
+/**
+ * @file
+ * Views integration functions for the private module.
+ */
+
+/**
+ * Implements hook_views_data().
+ */
 function private_views_data() {
   $data = array();
 
-  $data['private']['table']['group'] = t('Node');
+  $data['private']['table']['group'] = t('Content');
 
   $data['private']['table']['join'] = array(
     'node' => array(
@@ -34,6 +42,9 @@ function private_views_data() {
 
 }
 
+/**
+ * Implements hook_views_handlers().
+ */
 function private_views_handlers() {
   return array(
     'info' => array(
diff --git a/private_handler_filter_private.inc b/private_handler_filter_private.inc
index 7973c8a..a61818f 100644
--- a/private_handler_filter_private.inc
+++ b/private_handler_filter_private.inc
@@ -1,5 +1,10 @@
 <?php
 
+/**
+ * @file
+ * Views handlers for the private module.
+ */
+
 class private_handler_filter_private extends views_handler_filter_boolean_operator {
   function construct() {
     parent::construct();
@@ -8,7 +13,7 @@ class private_handler_filter_private extends views_handler_filter_boolean_operat
 
   function query() {
     $this->ensure_my_table();
-    $qualified_name = "$this->table_alias.$this->real_field"; 
-    $this->query->add_where($this->options['group'], $qualified_name . (empty($this->value) ? " = 0 OR $qualified_name IS NULL" : ' = 1'));
+    $qualified_name = "$this->table_alias.$this->real_field";
+    $this->query->add_where_expression($this->options['group'], $qualified_name . (empty($this->value) ? " = 0 OR $qualified_name IS NULL" : ' = 1'));
   }
 }
