diff --git a/2375019-diff-published.patch b/2375019-diff-published.patch
new file mode 100644
index 0000000..2c396ad
--- /dev/null
+++ b/2375019-diff-published.patch
@@ -0,0 +1,83 @@
+diff --git a/includes/diff.inc b/includes/diff.inc
+index 883c3f0..065f8db 100644
+--- a/includes/diff.inc
++++ b/includes/diff.inc
+@@ -216,40 +216,51 @@ function cps_render_changed_entity($entity_type, $change, $entity) {
+   $uri = entity_uri($entity_type, $change);
+   $changeset_uri = $entity->uri();
+ 
+-  // Don't render diff for published changesets:
+-  if (!$entity->published) {
+-    // We can't actually trust $change->published_revision because it might have
+-    // loaded incompletely, so let's load a fresh version.
+-
+-    // Make sure that CPS doesn't swap out the entity it loads!
+-    cps_override_changeset(CPS_PUBLISHED_CHANGESET);
+-    $original = entity_load_single($entity_type, $entity_id);
+-    cps_override_changeset(NULL);
+-
+-    if (cps_entity_in_published_changeset($entity_type, $original)) {
+-      $diff = cps_render_diff($entity_type, $original, $change);
+-      if (!empty($diff['#changes'])) {
+-        $title = format_plural($diff['#changes'], '1 change', '@count changes');
++  // Cache this on the changeset entity so that we don't run the query
++  // over and over again on the changeset view page.
++  if (!isset($entity->cps_changeset_to_diff_against)) {
++    if (!$entity->published) {
++      $entity->cps_changeset_to_diff_against = CPS_PUBLISHED_CHANGESET;
++    }
++    else {
++      $entity->cps_changeset_to_diff_against = db_query("SELECT changeset_id FROM {cps_changeset} WHERE published < :published ORDER by published DESC LIMIT 1", [':published' => $entity->published])->fetchField();
++    }
++  }
++
++  // We can't actually trust $change->published_revision because it might have
++  // loaded incompletely, so let's load a fresh version.
++
++  // Make sure that CPS doesn't swap out the entity it loads!
++  cps_override_changeset($entity->cps_changeset_to_diff_against);
++  $original = entity_load_single($entity_type, $entity_id);
++  cps_override_changeset(NULL);
++
++  if ($entity->cps_changeset_to_diff_against != CPS_PUBLISHED_CHANGESET) {
++    $new = !empty($original->default_revision);
++  }
++  else {
++    $new = !cps_entity_in_published_changeset($entity_type, $original);
++  }
++
++  if (!$new) {
++    $diff = cps_render_diff($entity_type, $original, $change);
++    if (!empty($diff['#changes'])) {
++      $title = format_plural($diff['#changes'], '1 change', '@count changes');
++    }
++    else {
++      // If there are no changes, we should get either new entity or no visible changes.
++      if ($diff) {
++        $title = reset($diff['#rows']);
++        $title = $title[0]['data'];
+       }
+       else {
+-        // If there are no changes, we should get either new entity or no visible changes.
+-        if ($diff) {
+-          $title = reset($diff['#rows']);
+-          $title = $title[0]['data'];
+-        }
+-        else {
+-          $title = t('No visible changes');
+-        }
++        $title = t('No visible changes');
+       }
+     }
+-    else {
+-      $diff = array();
+-      $title = t('Newly created');
+-    }
+   }
+   else {
+     $diff = array();
+-    $title = t('No diffs for published site versions');
++    $title = t('Newly created');
+   }
+ 
+   $can_edit = cps_can_edit_entities('update', $entity_type, $change, $entity->changeset_id);
diff --git a/2463961-query.patch b/2463961-query.patch
new file mode 100644
index 0000000..ac1d5c4
--- /dev/null
+++ b/2463961-query.patch
@@ -0,0 +1,103 @@
+diff --git a/includes/query.inc b/includes/query.inc
+index c597842..fa66a72 100644
+--- a/includes/query.inc
++++ b/includes/query.inc
+@@ -379,17 +379,7 @@ function cps_views_query_alter_entity($view, $query, $entity_type) {
+   // to this one. So we recorded at what index we saw that table, and then
+   // use array_splice() to move the cps_entity table join to the right position.
+   foreach ($move_cps_entities as $cps_entity_table => $alias) {
+-    // Find the index of the $alias in table_queue keys.
+-    $keys = array_keys($query->table_queue);
+-    $index = array_search($alias, $keys);
+-    $splice = array($cps_entity_table => $query->table_queue[$cps_entity_table]);
+-    unset($query->table_queue[$cps_entity_table]);
+-
+-    // Now move the item to the proper location in the array. Don't use array_splice
+-    // because that breaks indices.
+-    $query->table_queue = array_slice($query->table_queue, 0, $index, TRUE) +
+-      $splice +
+-      array_slice($query->table_queue, $index, NULL, TRUE);
++    cps_move_entity_table($query, $cps_entity_table, $alias);
+   }
+ 
+   $entity_table = $entity_info['base table'];
+@@ -505,10 +495,39 @@ function cps_ensure_revision_table($entity_type, $query, $relationship) {
+ 
+   // Add this table only once.
+   if (isset($query->tables[$relationship][$table])) {
+-    return $query->tables[$relationship][$table]['alias'];
++    $alias = $query->tables[$relationship][$table]['alias'];
++    // If this table previously existed, but was not added by us, we need
++    // to modify the join and make sure that $cps_entity_table comes first.
++    if (empty($query->table_queue[$alias]['join']->cps_adjusted)) {
++      $query->table_queue[$alias]['join'] = cps_get_revision_table_join($relationship, $table, $field, $cps_entity_table);
++      // We also have to ensure that our cps_entity_table comes before this.
++      cps_move_entity_table($query, $cps_entity_table, $alias);
++    }
++
++    return $alias;
+   }
+ 
+   // Construct the join.
++  $join = cps_get_revision_table_join($relationship, $table, $field, $cps_entity_table);
++  return $query->queue_table($table, $relationship, $join);
++}
++
++/**
++ * Fetch a join object for a revision table, constructed to use cps_entity
++ *
++ * @param string $relationship
++ *   The $relationship to use in the view.
++ * @param $table
++ *   The table name.
++ * @param $field
++ *   The field to join on.
++ * @param $cps_entity_table
++ *   The alias of the cps_entity table joined to the main entity table.
++ *
++ * @return views_join
++ *   An adjusted views join object to add to the query.
++ */
++function cps_get_revision_table_join($relationship, $table, $field, $cps_entity_table) {
+   $join = new views_join();
+   $join->definition = array(
+     'table' => $table,
+@@ -520,8 +539,37 @@ function cps_ensure_revision_table($entity_type, $query, $relationship) {
+ 
+   $join->construct();
+   $join->adjusted = TRUE;
++  $join->cps_adjusted = TRUE;
+ 
+-  return $query->queue_table($table, $relationship, $join);
++  return $join;
++}
++
++/**
++ * Move a cps_entity table to appear before $alias
++ *
++ * Because CPS chains possibly pre-existing tables onto the cps_entity table,
++ * we have to ensure that the cps_entity table appears in the query before
++ * the alias it's chained on or the SQL is invalid. This uses array_slice
++ * to reconstruct the query table_queue object.
++ *
++ * @param views_plugin_query_default $query
++ *   The SQL query object.
++ * @param $cps_entity_table
++ *   The alias of the CPS entity table
++ * @param $alias
++ *   The alias of the table it needs to appear before.
++ */
++function cps_move_entity_table($query, $cps_entity_table, $alias) {
++  $keys = array_keys($query->table_queue);
++  $index = array_search($alias, $keys);
++  $splice = array($cps_entity_table => $query->table_queue[$cps_entity_table]);
++  unset($query->table_queue[$cps_entity_table]);
++
++  // Now move the item to the proper location in the array. Don't use array_splice
++  // because that breaks indices.
++  $query->table_queue = array_slice($query->table_queue, 0, $index, TRUE) +
++    $splice +
++    array_slice($query->table_queue, $index, NULL, TRUE);
+ }
+ 
+ /**
diff --git a/2482723-archived.patch b/2482723-archived.patch
new file mode 100644
index 0000000..e8aea1c
--- /dev/null
+++ b/2482723-archived.patch
@@ -0,0 +1,31 @@
+diff --git a/cps.module b/cps.module
+index 2787d77..39ff531 100644
+--- a/cps.module
++++ b/cps.module
+@@ -354,14 +354,23 @@ function cps_entity_load(&$entities, $entity_type) {
+   }
+ 
+   $revision_key = $entity_info['entity keys']['revision'];
+-  $tracked = cps_get_tracked_entities($changeset);
++  $keys = array_keys($entities);
++  // We don't use cps_get_tracked_entities() because when viewing archived
++  // changesets, would either have to load all the entities on the site or
++  // would not correctly get entities that were tagged for their state at
++  // the time of publication.
++  $result = db_query('SELECT * FROM {cps_entity} WHERE entity_type = :entity_type AND entity_id IN (:ids) AND changeset_id = :changeset_id', array(':entity_type' => $entity_type, ':ids' => $keys, ':changeset_id' => $changeset));
++  foreach ($result as $entity_state) {
++    $tracked[$entity_state->entity_id] = $entity_state->revision_id;
++  }
++
+   foreach ($entities as $entity) {
+     list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
+ 
+     // If this entity has a revision set for the current change set,
+     // we need to swap it out for the correct revision.
+-    if (isset($tracked[$entity_type][$entity_id]) && $tracked[$entity_type][$entity_id] != $revision_id) {
+-      $swap_revision_id = $tracked[$entity_type][$entity_id];
++    if (isset($tracked[$entity_id]) && $tracked[$entity_id] != $revision_id) {
++      $swap_revision_id = $tracked[$entity_id];
+       $draft_revisions = entity_load($entity_type, array($entity_id), array($revision_key => $swap_revision_id));
+ 
+       // Swap the entity!
diff --git a/2482727-move.patch b/2482727-move.patch
new file mode 100644
index 0000000..11def08
--- /dev/null
+++ b/2482727-move.patch
@@ -0,0 +1,104 @@
+diff --git a/cps.module b/cps.module
+index 39ff531..285e206 100644
+--- a/cps.module
++++ b/cps.module
+@@ -1384,6 +1384,55 @@ function cps_publish_lock_time() {
+   return FALSE;
+ }
+ 
++/**
++ * Move or copy an entity from one changeset to another.
++ *
++ * It is the caller's responsibility to validate that this is okay.
++ *
++ * @param CPSChangeset $changeset_from
++ *   The source changeset.
++ * @param CPSChangeset $changeset_to
++ *   The destination changeset.
++ * @param string $entity_type
++ *   The type of entity being moved.
++ * @param object $entity
++ *   The entity being moved.
++ * @param string $move_type
++ *   Either 'move' or 'copy'. 'copy' is the default if not known.
++ */
++function cps_move_changeset($changeset_from, $changeset_to, $entity_type, $entity, $move_type) {
++  module_invoke_all('cps_move_changeset', $changeset_from, $changeset_to, $entity_type, $entity, $move_type);
++  list($entity_id, $revision_id) = entity_extract_ids($entity_type, $entity);
++
++  $transaction = db_transaction();
++  try {
++    // When moving just update the record for the old changeset to point to
++    // the new one.
++    if ($move_type == 'move') {
++      db_update('cps_entity')
++        ->condition('entity_type', $entity_type)
++        ->condition('entity_id', $entity_id)
++        ->condition('changeset_id', $changeset_from->changeset_id)
++        ->fields(array('changeset_id' => $changeset_to->changeset_id))
++        ->execute();
++    }
++    else {
++      cps_override_changeset($changeset_to->changeset_id);
++      // Otherwise save the entity in the new changeset - this will create a
++      // new revision tracked in the new changeset, leaving the old revision
++      // and cps_entity record intact.
++      entity_save($entity_type, $entity);
++      cps_override_changeset(NULL);
++    }
++  }
++  catch (Exception $e) {
++    drupal_set_message(nt('Unable to complete operation due to an internal error. Exception recorded to watchdog log.'));
++    $transaction->rollback();
++    watchdog_exception('cps', $e);
++    return;
++  }
++}
++
+ // -------------------------------------------------------------------------
+ // Database and general entity API functions
+ 
+diff --git a/includes/admin.inc b/includes/admin.inc
+index 866b012..f6dae61 100644
+--- a/includes/admin.inc
++++ b/includes/admin.inc
+@@ -336,38 +336,7 @@ function cps_changeset_move_form_submit($form, &$form_state) {
+     cps_override_changeset($changeset_from->changeset_id);
+     $entity = entity_load_single($entity_type, $entity_id);
+ 
+-    module_invoke_all('cps_move_changeset', $changeset_from, $changeset_to, $entity_type, $entity, $form_state['values']['type']);
+-    list($entity_id, $revision_id) = entity_extract_ids($entity_type, $entity);
+-
+-    $transaction = db_transaction();
+-    try {
+-      // When moving just update the record for the old changeset to point to
+-      // the new one.
+-      if ($form_state['values']['type'] == 'move') {
+-        db_update('cps_entity')
+-          ->condition('entity_type', $entity_type)
+-          ->condition('entity_id', $entity_id)
+-          ->condition('changeset_id', $changeset_from->changeset_id)
+-          ->fields(array('changeset_id' => $changeset_to->changeset_id))
+-          ->execute();
+-      }
+-      else {
+-        // Otherwise save the entity in the new changeset - this will create a
+-        // new revision tracked in the new changeset, leaving the old revision
+-        // and cps_entity record intact.
+-        cps_override_changeset($changeset_to->changeset_id);
+-        entity_save($entity_type, $entity);
+-      }
+-    }
+-    catch (Exception $e) {
+-      drupal_set_message(t('Unable to @type %entity from the site version %title to the current site version.', array(
+-        '%entity' => entity_label($entity_type, $entity),
+-        '%title' => $changeset_from->label(),
+-        '@type' => $form_state['values']['type'] == 'move' ? t('move') : t('copy')
+-      )));
+-      $transaction->rollback();
+-      watchdog_exception('CPS', $e);
+-    }
++    cps_move_changeset($changeset_from, $changeset_to, $entity_type, $entity, $form_state['values']['type']);
+     drupal_set_message(t('Changes to %entity have been @type from the site version %title to the current site version.', array(
+       '%entity' => entity_label($entity_type, $entity),
+       '%title' => $changeset_from->label(),
diff --git a/cps.install.orig b/cps.install.orig
new file mode 100644
index 0000000..e8a3cc6
--- /dev/null
+++ b/cps.install.orig
@@ -0,0 +1,598 @@
+<?php
+
+/**
+ * @file
+ * Installation specific hooks for CPS module.
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function cps_schema() {
+  $schema['cps_entity'] = array(
+    'description' => 'Stores the relationships between entity revisions and their containing changesets.',
+    'fields' => array(
+      'entity_type' => array(
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'entity_id' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'revision_id' => array(
+        'description' => 'The id of the entity revision which the changeset points to.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'changeset_id' => array(
+        'description' => 'The changeset id.',
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'published' => array(
+        'description' => 'True if this entity was added to the changeset when it was published.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('changeset_id', 'entity_type', 'entity_id'),
+    'indexes' => array(
+      'entity' => array('entity_type', 'entity_id'),
+      'published_changeset_id' => array('published', 'changeset_id'),
+    ),
+  );
+
+  $schema['cps_changeset'] = array(
+    'description' => 'Stores metadata about available changesets.',
+    'fields' => array(
+      'changeset_id' => array(
+        'description' => 'A machine name identifying the changeset.',
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'uid' => array(
+        'description' => 'The owner of the changeset.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'name' => array(
+        'description' => 'A name for the changeset.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'description' => array(
+        'description' => 'A brief description of this changeset.',
+        'type' => 'text',
+        'not null' => TRUE,
+        'size' => 'medium',
+        'translatable' => TRUE,
+      ),
+      'status' => array(
+        'description' => 'The current status of the changeset.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => 'unpublished',
+      ),
+      'published' => array(
+        'description' => 'Timestamp indicating when the changeset was published.',
+        'type' => 'int',
+      ),
+      'variables' => array(
+        'description' => 'A serialized array of variables staged in this changeset.',
+        'type' => 'blob',
+        'size' => 'big',
+        'serialize' => TRUE,
+      ),
+      'created' => array(
+        'description' => 'The Unix timestamp when the entity was created.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'changed' => array(
+        'description' => 'The Unix timestamp when the entity was most recently saved.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'lock_in_select' => array(
+        'description' => 'True if this changeset should be locked into all users select bars when open.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('changeset_id'),
+  );
+
+  $schema['cps_changeset_history'] = array(
+    'description' => 'Stores the history of changeset status changes.',
+    'fields' => array(
+      'changeset_id' => array(
+        'description' => 'A machine name identifying the changeset.',
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'uid' => array(
+        'description' => 'The user who made the change.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'previous_status' => array(
+        'description' => 'The previous status.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'new_status' => array(
+        'description' => 'The new status.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'message' => array(
+        'description' => 'An optional message accompanying the change',
+        'type' => 'text',
+        'size' => 'big',
+      ),
+      'timestamp' => array(
+        'description' => 'Timestamp indicating when the status was changed.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'indexes' => array(
+      'changeset_id' => array('changeset_id')
+    ),
+  );
+
+  return $schema;
+}
+
+/**
+ * Implements hook_install().
+ */
+function cps_install() {
+  // Create the special live/published changeset.
+  db_insert('cps_changeset')
+    ->fields(array(
+      'changeset_id' => CPS_PUBLISHED_CHANGESET,
+      'name' => 'Live',
+      'description' => 'The live changeset. This changeset cannot be edited or deleted.',
+      'published' => 0,
+      'uid' => 0,
+      'status' => CPS_LIVE_STATUS,
+    ))
+    ->execute();
+
+  cps_create_installed_changeset();
+}
+
+/**
+ * Create the special changeset used to mark content when CPS is installed.
+ */
+function cps_create_installed_changeset() {
+  // Make sure this is safe to run so that update hooks can call this to make
+  // sure the changeset exists and not worry about dependencies. So we insert
+  // this directly rather than using the entity system.
+  $result = db_query('SELECT changeset_id FROM {cps_changeset} WHERE changeset_id = :changeset_id', array(':changeset_id' => CPS_INSTALLED_CHANGESET))->fetchField();
+  if (!$result) {
+    db_insert('cps_changeset')
+      ->fields(array(
+        'changeset_id' => CPS_INSTALLED_CHANGESET,
+        'name' => 'Live content at install',
+        'description' => 'The installed changeset. This is only used to mark the state of live content at the time of install.',
+        'published' => REQUEST_TIME,
+        'uid' => 0,
+        'status' => 'archived',
+      ))
+      ->execute();
+  }
+}
+
+/**
+ * Mark content as part of the installed changeset.
+ *
+ * This method is used when an entity is newly added to CPS support to
+ * mark all content that is not currently in CPS as having gone live
+ * for the installed changeset. It is used to keep the state stable when
+ * CPS is introduced to an already existing system.
+ *
+ * @param string $entity_type
+ *   The entity type.
+ *
+ * @param mixed $sandbox
+ *   If provided, this function won't loop, and you're expected to
+ *   use a sandbox in a batch or update hook in order to use this in
+ *   a long run. However install hooks don't do sandboxes, so in that
+ *   case it should just loop.
+ *
+ * @return bool
+ *   FALSE if finished, TRUE if there might be more records.
+ */
+function cps_mark_current_content($entity_type, &$sandbox = NULL) {
+  cps_create_installed_changeset();
+
+  $entity_info = entity_get_info($entity_type);
+  $table = $entity_info['base table'];
+  $id_key = $entity_info['entity keys']['id'];
+  $revision_key = $entity_info['entity keys']['revision'];
+
+  $limit = 500;
+
+  if (isset($sandbox)) {
+    if (!isset($sandbox['current'])) {
+      $sandbox['current'] = 0;
+    }
+
+    $current = &$sandbox['current'];
+  }
+  else {
+    $current = 0;
+  }
+
+  // The do {} while () loop ensures this runs once if using sandbox, or until
+  // completion if not using sandbox.
+  do {
+    $query = db_select($table, 'base');
+    $query->addJoin('LEFT', 'cps_entity', 'ce', "ce.entity_type = :entity_type AND ce.entity_id = base.$id_key", array(':entity_type' => $entity_type));
+    // Only have results if there is no link to a changeset already.
+    $result = $query->fields('base', array($id_key, $revision_key))
+      ->isNull('ce.changeset_id')
+      ->range($current, $limit)
+      ->execute();
+
+    if ($result->rowCount() == 0) {
+      return FALSE;
+    }
+
+    $current += $limit;
+
+    // Use a multi-insert here for performance.
+    $insert_query = db_insert('cps_entity')
+      ->fields(array('entity_type', 'entity_id', 'revision_id', 'changeset_id', 'published'));
+
+    foreach ($result as $record) {
+      $insert_query->values(array(
+        'entity_type' => $entity_type,
+        'entity_id' => $record->{$id_key},
+        'revision_id' => $record->{$revision_key},
+        'changeset_id' => CPS_INSTALLED_CHANGESET,
+        'published' => 1, // Not part of the changeset but was live when changeset was published.
+      ));
+    }
+
+    $insert_query->execute();
+  } while (!isset($sandbox));
+
+  return TRUE;
+}
+
+/**
+ * Add status, uid fields to changesets. Add changeset history table.
+ */
+function cps_update_7001() {
+  $tables['cps_changeset_history'] = array(
+    'description' => 'Stores the history of changeset status changes.',
+    'fields' => array(
+      'changeset_id' => array(
+        'description' => 'A machine name identifying the changeset.',
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'uid' => array(
+        'description' => 'The user who made the change.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'previous_status' => array(
+        'description' => 'The previous status.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'new_status' => array(
+        'description' => 'The new status.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'message' => array(
+        'description' => 'An optional message accompanying the change',
+        'type' => 'text',
+        'size' => 'big',
+      ),
+      'timestamp' => array(
+        'description' => 'Timestamp indicating when the status was changed.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'indexes' => array(
+      'changeset_id' => array('changeset_id')
+    ),
+  );
+
+  foreach ($tables as $name => $schema) {
+    db_create_table($name, $schema);
+  }
+
+  $fields = array(
+    'cps_changeset' => array(
+      'uid' => array(
+        'description' => 'The owner of the changeset.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'status' => array(
+        'description' => 'The current status of the changeset.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'created' => array(
+        'description' => 'The Unix timestamp when the entity was created.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'changed' => array(
+        'description' => 'The Unix timestamp when the entity was most recently saved.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'cps_entity' => array(
+      'published' => array(
+        'description' => 'True if this entity was added to the changeset when it was published.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+      ),
+    ),
+  );
+
+  foreach ($fields as $table => $table_fields) {
+    foreach ($table_fields as $name => $field) {
+      db_add_field($table, $name, $field);
+    }
+  }
+
+  // Update all pre-existing published changesets to the published state.
+  db_update('cps_changeset')
+    ->fields(array(
+      'status' => CPS_ARCHIVED_STATUS,
+    ))
+    ->condition('published', 0, '>')
+    ->execute();
+
+  $result = db_query("SELECT changeset_id FROM {cps_changeset} WHERE changeset_id = 'published'")->fetchCol();
+  if (!$result) {
+    // Create our special live changeset.
+    db_insert('cps_changeset')
+      ->fields(array(
+        'changeset_id' => CPS_PUBLISHED_CHANGESET,
+        'name' => 'Live',
+        'description' => 'The live changeset. This changeset cannot be edited or deleted.',
+        'published' => 0,
+        'uid' => 1,
+        'status' => CPS_LIVE_STATUS,
+      ))
+      ->execute();
+  }
+  else {
+    $live = CPS_LIVE_STATUS;
+    db_query("UPDATE {cps_changeset} SET status = '$live', uid = 1 WHERE changeset_id = 'published'");
+  }
+
+  db_update('cps_changeset')
+    ->fields(array(
+      'status' => 'unpublished'
+    ))
+    ->condition('status', '')
+    ->execute();
+
+}
+
+/**
+ * Add index on {cps_entity}.changeset_id.
+ */
+function cps_update_7100() {
+  if (!db_index_exists('cps_entity', 'changeset_id')) {
+    db_add_index('cps_entity', 'changeset_id', array('changeset_id'));
+  }
+}
+
+/**
+ * Add indexes, remove field_collection_item records.
+ */
+function cps_update_7101() {
+   if (!db_index_exists('cps_entity', 'published_changeset_id')) {
+    db_add_index('cps_entity', 'published_changeset_id', array('published', 'changeset_id'));
+  }
+  db_delete('cps_entity')
+    ->condition('entity_type', 'field_collection_item')
+    ->execute();
+}
+
+/**
+ * Enable drafty module.
+ */
+function cps_update_7102() {
+  if (!module_enable(array('drafty_enforce'))) {
+    throw new Exception('Drafty module must be available to update to this version of CPS.');
+  }
+}
+
+/**
+ * Enable entity_status module.
+ */
+function cps_update_7103() {
+  if (!module_enable(array('entity_status'))) {
+    throw new Exception('Entity status module must be available to update to this version of CPS.');
+  }
+}
+
+/**
+ * Change {cps_entity}.published to default to 0.
+ */
+function cps_update_7104() {
+  db_update('cps_entity')
+    ->isNull('published')
+    ->fields(array('published' => 0))
+    ->execute();
+  db_drop_index('cps_entity', 'published_changeset_id');
+
+  $field = array(
+    'description' => 'True if this entity was added to the changeset when it was published.',
+    'type' => 'int',
+    'unsigned' => TRUE,
+    'not null' => TRUE,
+    'default' => 0,
+  );
+  $indexes = array(
+    'indexes' => array(
+      'published_changeset_id' => array('published', 'changeset_id'),
+    ),
+  );
+  db_change_field('cps_entity', 'published', 'published', $field, $indexes);
+}
+
+/**
+ * Create the 'installed' changeset.
+ */
+function cps_update_7105() {
+  cps_create_installed_changeset();
+}
+
+/**
+ * Change cps_entity indexes.
+ */
+function cps_update_7106() {
+  db_drop_index('cps_entity', 'changeset_id');
+  db_drop_primary_key('cps_entity');
+  db_add_primary_key('cps_entity', array('changeset_id', 'entity_type', 'entity_id'));
+  db_add_index('cps_entity', 'entity', array('entity_type', 'entity_id'));
+}
+
+/**
+ * Add historical information for changeset affected by #2428323.
+ */
+function cps_update_7107() {
+  $types = cps_get_supported();
+  // Get an array of entity types where there has never been an entity of that
+  // type edited in a changeset. This saves unnecessary queries later on.
+  $exclude_types = [];
+  foreach ($types as $type) {
+    if (!db_query_range('SELECT 1 FROM {cps_entity} WHERE entity_type = :entity_type AND published = 0', 0, 1, array(':entity_type' => $type))->fetchField()) {
+      $exclude_types[] = $type;
+    }
+  }
+  $entity_info = entity_get_info();
+  $changeset_result = db_query('SELECT changeset_id, published FROM {cps_changeset} WHERE status = :status', array(':status' => 'archived'));
+
+  $transaction = db_transaction();
+  try {
+    foreach ($changeset_result as $changeset) {
+      foreach ($types as $type) {
+        $base_table = $entity_info[$type]['base table'];
+        $id_key = $entity_info[$type]['entity keys']['id'];
+        $revision_key = $entity_info[$type]['entity keys']['revision'];
+        // Find all entities missing from this changeset.
+        $from_query = db_select('cps_entity', 'cps_entity')
+          ->fields('cps_entity', ['entity_id'])
+          ->condition('changeset_id', $changeset->changeset_id)
+          ->condition('entity_type', $type);
+        $result = db_select($base_table, 'base')
+          ->fields('base', [$id_key, $revision_key])
+          ->condition($id_key, $from_query, 'NOT IN')
+          ->execute();
+
+        foreach ($result as $record) {
+          $vid = NULL;
+          $previous_args = array(
+            ':id' => $record->{$id_key},
+            ':published' => $changeset->published,
+            ':type' => $type,
+            ':status' => 'archived',
+          );
+          // If this entity type has never been used in a changeset, just use
+          // the current version ID.
+          if (in_array($type, $exclude_types)) {
+            $vid = $record->{$revision_key};
+          }
+          // If the entity was edited in a changeset published prior to this
+          // one, then the version ID from that changeset should be used. This
+          // ensures that entities that have been edited in multiple changesets
+          // have the correct historical state - just taking the most recent vid
+          // would not be correct in this case.
+          else if ($vid = db_query_range('SELECT revision_id FROM {cps_entity} ce INNER JOIN cps_changeset cc ON ce.changeset_id = cc.changeset_id WHERE ce.entity_id = :id AND ce.entity_type = :type AND ce.published = 0 AND cc.published < :published AND cc.status = :status ORDER BY cc.published DESC', 0, 1, $previous_args)->fetchField()) {
+            // Assignment happens in the if statement, nothing else to do here.
+          }
+          // Otherwise use the current revision ID.
+          else {
+            $vid = $record->{$revision_key};
+          }
+
+          db_insert('cps_entity')
+            ->fields(array(
+              'entity_type' => $type,
+              'entity_id' => $record->{$id_key},
+              'revision_id' => $vid,
+              'changeset_id' => $changeset->changeset_id,
+              'published' => TRUE,
+            ))->execute();
+        }
+      }
+    }
+  }
+  catch (Exception $e) {
+    $transaction->rollback();
+    throw $e;
+  }
+}
+
+/**
+ * Add lock_in_select field.
+ */
+function cps_update_7108() {
+  $field = array(
+    'description' => 'True if this changeset should be locked into all users select bars when open.',
+    'type' => 'int',
+    'unsigned' => TRUE,
+    'not null' => TRUE,
+    'default' => 0,
+  );
+
+  db_add_field('cps_changeset', 'lock_in_select', $field);
+}
diff --git a/cps.module.orig b/cps.module.orig
new file mode 100644
index 0000000..2787d77
--- /dev/null
+++ b/cps.module.orig
@@ -0,0 +1,1676 @@
+<?php
+
+/**
+ * @file
+ * Main module file for the Content Preview System.
+ */
+
+define('CPS_PUBLISHED_CHANGESET', 'published');
+define('CPS_INSTALLED_CHANGESET', 'installed');
+
+define('CPS_LIVE_STATUS', 'live');
+define('CPS_ARCHIVED_STATUS', 'archived');
+
+require_once __DIR__ . '/includes/query.inc';
+
+// -----------------------------------------------------------------------
+// Drupal Core Hooks
+
+/**
+ * Implements hook_boot().
+ */
+function cps_boot() {
+  // If we are looking at a changeset, load overridden variables and add
+  // them into $conf.
+
+  // This isn't using cps_changeset_load() because that loads a bunch of things
+  // that don't exist during hook_boot.
+  if (variable_get('cps_override_variables', FALSE)) {
+    $changeset_id = cps_get_current_changeset();
+    if ($changeset_id != CPS_PUBLISHED_CHANGESET) {
+      $vars = db_query("SELECT variables FROM {cps_changeset} WHERE changeset_id = :changeset_id", array(':changeset_id' => $changeset_id))->fetchField();
+      if ($vars) {
+        global $conf;
+        global $cps_conf;
+        $cps_conf = unserialize($vars);
+
+        if ($cps_conf) {
+          foreach ($cps_conf as $name => $value) {
+            $conf[$name] = $value;
+          }
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_exit().
+ */
+function cps_exit() {
+  if (variable_get('cps_override_variables', FALSE)) {
+    global $cps_conf_changed;
+    if (!empty($cps_conf_changed)) {
+      $changeset_id = cps_get_current_changeset(TRUE);
+      if ($changeset_id != CPS_PUBLISHED_CHANGESET) {
+        $changeset = cps_changeset_load($changeset_id);
+        // Only write if $changeset->published is empty.
+        if (empty($changeset->published)) {
+          global $cps_conf;
+          $changeset->variables = $cps_conf;
+          $changeset->save();
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_module_implements_alter().
+ */
+function cps_module_implements_alter(&$implementations, $hook) {
+  if (in_array($hook, array('entity_query_alter', 'views_query_alter'))) {
+    // Move CPS's query alter after entity reference's so we can remove the
+    // checks for published status.
+    $temp = $implementations['cps'];
+    unset($implementations['cps']);
+    $implementations['cps'] = $temp;
+  }
+}
+
+/**
+ * Implements of hook_permission()
+ */
+function cps_permission() {
+  return array(
+    'administer changesets' => array(
+      'title' => t('Administer site versions'),
+      'description' => t('Add, delete, edit and manipulate site versions.'),
+    ),
+    'edit all changesets' => array(
+      'title' => t('Edit all site versions'),
+      'description' => t('Can edit and delete site versions owned by other people.'),
+    ),
+    'view changesets' => array(
+      'title' => t('View site versions'),
+      'description' => t('View the site version widget and change the site preview accordingly.'),
+    ),
+    'preview changesets' => array(
+      'title' => t('Preview site versions'),
+      'description' => t('Preview site versions without being able to see the widget.'),
+    ),
+    'publish changesets' => array(
+      'title' => t('Publish site versions'),
+      'description' => t('Can publish site versions.'),
+    ),
+  );
+}
+
+/**
+ * Implements hook_entity_info().
+ */
+function cps_entity_info() {
+  $info = array();
+
+  $info['cps_changeset'] = array(
+    'label' => t('Site version'),
+    'entity class' => 'CPSChangeset',
+    'controller class' => 'CPSChangesetController',
+    'views controller class' => 'EntityDefaultViewsController',
+    'base table' => 'cps_changeset',
+    // After variants this will change to FALSE
+    'fieldable' => FALSE,
+    'entity keys' => array(
+      'id' => 'changeset_id',
+      'label' => 'name',
+    ),
+    'bundles' => array(
+      'cps_changeset' => array(
+        'label' => t('Site version'),
+      ),
+    ),
+    'view modes' => array(
+      'full' => array(
+        'label' => t('Full'),
+        'custom settings' => FALSE,
+      ),
+      'embed' => array(
+        'label' => t('Embedded'),
+        'custom settings' => FALSE,
+      ),
+      'mail' => array(
+        'label' => t('Mailed'),
+        'custom settings' => FALSE,
+      ),
+    ),
+    // entity module callbacks
+    'label callback' => 'entity_class_label',
+    'uri callback' => 'entity_class_uri',
+    'view callback' => 'entity_metadata_view_single',
+    'creation callback' => 'cps_changeset_create',
+    'access callback' => 'cps_changeset_access',
+    'save callback' => 'cps_changeset_save',
+    'module' => 'cps',
+  );
+
+  return $info;
+}
+
+/**
+ * Implements hook_menu().
+ */
+function cps_menu() {
+  $items = array();
+  $items['admin/structure/changesets/%cps_changeset'] = array(
+    'title callback' => 'entity_label',
+    'title arguments' => array('cps_changeset', 3),
+    'page callback' => 'cps_changeset_view_page',
+    'page arguments' => array(3, 'full'),
+    'access callback' => 'entity_access',
+    'access arguments' => array('view', 'cps_changeset', 3),
+    'file' => 'includes/admin.inc',
+  );
+
+  $items['admin/structure/changesets/%cps_changeset/view'] = array(
+    'title' => 'Status',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+    'file' => 'includes/admin.inc',
+  );
+
+  $items['admin/structure/changesets/%cps_changeset/edit'] = array(
+    'title' => 'Edit',
+    'type' => MENU_LOCAL_TASK,
+    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'page callback' => 'cps_changeset_edit_page',
+    'page arguments' => array(3),
+    'access callback' => 'entity_access',
+    'access arguments' => array('update', 'cps_changeset', 3),
+    'weight' => -9,
+    'file' => 'includes/admin.inc',
+  );
+
+  $items['admin/structure/changesets/%cps_changeset/view-site'] = array(
+    'title' => 'Switch to this version',
+    'type' => MENU_LOCAL_ACTION,
+    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'page callback' => 'cps_changeset_view_site_page',
+    'page arguments' => array(3),
+    'access callback' => 'cps_changeset_view_site_access',
+    'access arguments' => array(3),
+    'file' => 'includes/admin.inc',
+    'weight' => -10,
+  );
+
+  $items['admin/structure/changesets/%cps_changeset/publish'] = array(
+    'title' => 'Publish',
+    'type' => MENU_LOCAL_ACTION,
+    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'page callback' => 'cps_changeset_publish_page',
+    'page arguments' => array(3),
+    'access callback' => 'entity_access',
+    'access arguments' => array('publish', 'cps_changeset', 3),
+    'file' => 'includes/admin.inc',
+  );
+
+  $items['admin/structure/changesets/%cps_changeset/unpublish'] = array(
+    'title' => 'Unpublish',
+    'type' => MENU_LOCAL_ACTION,
+    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'page callback' => 'cps_changeset_unpublish_page',
+    'page arguments' => array(3),
+    'access callback' => 'entity_access',
+    'access arguments' => array('unpublish', 'cps_changeset', 3),
+    'file' => 'includes/admin.inc',
+  );
+
+  $items['admin/structure/changesets/%cps_changeset/delete'] = array(
+    'title' => 'Delete',
+    'type' => MENU_LOCAL_TASK,
+    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('cps_changeset_delete_form', 3),
+    'access callback' => 'entity_access',
+    'access arguments' => array('delete', 'cps_changeset', 3),
+    'weight' => -8,
+    'file' => 'includes/admin.inc',
+  );
+
+  $items['admin/structure/changesets/%cps_changeset/remove/%/%'] = array(
+    'title' => 'Remove',
+    'type' => MENU_CALLBACK,
+    'page callback' => 'cps_changeset_remove_page',
+    'page arguments' => array(3, 5, 6),
+    'access callback' => 'entity_access',
+    'access arguments' => array('update', 'cps_changeset', 3),
+    'file' => 'includes/admin.inc',
+  );
+
+  $items['admin/structure/changesets/%cps_changeset/move/%/%'] = array(
+    'title' => 'Move',
+    'type' => MENU_CALLBACK,
+    'page callback' => 'cps_changeset_move_page',
+    'page arguments' => array(3, 5, 6),
+    // Handle access checking in the callback as its a bit complex for here.
+    'access callback' => TRUE,
+    'file' => 'includes/admin.inc',
+  );
+
+  if (module_exists('devel')) {
+    $items['admin/structure/changesets/%cps_changeset/devel'] = array(
+      'title' => 'Devel',
+      'page callback' => 'devel_load_object',
+      'page arguments' => array('cps_changeset', 3),
+      'access arguments' => array('access devel information'),
+      'type' => MENU_LOCAL_TASK,
+      'weight' => 100,
+      'file' => 'devel.pages.inc',
+      'file path' => drupal_get_path('module', 'devel'),
+    );
+
+    $items['admin/structure/changesets/%cps_changeset/devel/load'] = array(
+      'title' => 'Load',
+      'type' => MENU_DEFAULT_LOCAL_TASK,
+    );
+
+    $items['admin/structure/changesets/%cps_changeset/devel/render'] = array(
+      'title' => 'Render',
+      'page callback' => 'devel_render_object',
+      // Normally this would be the name of the entity type, but slightly
+      // modified in order to call the right function.
+      'page arguments' => array('cps_changeset', 3),
+      'access arguments' => array('access devel information'),
+      'file' => 'devel.pages.inc',
+      'file path' => drupal_get_path('module', 'devel'),
+      'type' => MENU_LOCAL_TASK,
+      'weight' => 100,
+    );
+  }
+
+  $items['admin/structure/changesets'] = array(
+    'title' => 'Site versions',
+    'description' => 'Manage changesets.',
+    'page callback' => 'cps_changeset_list_page',
+    'page arguments' => array('administer_changesets'),
+    'file' => 'includes/admin.inc',
+    'access arguments' => array('administer changesets'),
+    'weight' => -10,
+  );
+
+  $items['admin/structure/changesets/open'] = array(
+    'title' => 'Open',
+    'weight' => -10,
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+  );
+
+  $items['admin/structure/changesets/archived'] = array(
+    'title' => 'Archive',
+    'page callback' => 'cps_changeset_list_page',
+    'page arguments' => array('archived_changesets'),
+    'file' => 'includes/admin.inc',
+    'access arguments' => array('administer changesets'),
+    'weight' => 10,
+    'type' => MENU_LOCAL_TASK,
+  );
+
+  $items['admin/structure/changesets/add'] = array(
+    'title' => 'Add site version',
+    'page callback' => 'cps_changeset_add_entity_page',
+    'page arguments' => array(),
+    'access arguments' => array('administer changesets'),
+    'type' => MENU_LOCAL_ACTION,
+    'file' => 'includes/admin.inc',
+  );
+
+  return $items;
+}
+
+/**
+ * Implements hook_entity_load().
+ */
+function cps_entity_load(&$entities, $entity_type) {
+  $changeset = cps_get_current_changeset();
+  if ($changeset == CPS_PUBLISHED_CHANGESET) {
+    return;
+  }
+
+  $entity_info = entity_get_info($entity_type);
+  if (empty($entity_info['entity keys']['revision'])) {
+    return;
+  }
+
+  // When a non-default revision is being loaded, don't overwrite any of the
+  // contents of that revision.
+  foreach ($entities as $entity) {
+    // If a revision was requested, then there is only going to be one entity
+    // in the list. We don't touch entity loading when a specific revision was
+    // requested.
+    // When the new drafty dependency is added, drafty module may not actually
+    // be enabled, and this can cause fatal errors on sites that load entities
+    // during bootstrap, so check for module existence.
+    if (module_exists('drafty') && drafty()->wasRevisionRequested($entity)) {
+      return;
+    }
+  }
+
+  $revision_key = $entity_info['entity keys']['revision'];
+  $tracked = cps_get_tracked_entities($changeset);
+  foreach ($entities as $entity) {
+    list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
+
+    // If this entity has a revision set for the current change set,
+    // we need to swap it out for the correct revision.
+    if (isset($tracked[$entity_type][$entity_id]) && $tracked[$entity_type][$entity_id] != $revision_id) {
+      $swap_revision_id = $tracked[$entity_type][$entity_id];
+      $draft_revisions = entity_load($entity_type, array($entity_id), array($revision_key => $swap_revision_id));
+
+      // Swap the entity!
+      if (!empty($draft_revisions[$entity_id])) {
+        $new_entity = $entities[$entity_id] = $draft_revisions[$entity_id];
+        // Store the current revision ID so we can tell later if we need to put it back.
+        $new_entity->published_revision_id = $revision_id;
+        $new_entity->published_revision = clone($entity);
+        // Tell entity API not to save this as the default revision:
+        $new_entity->default_revision = FALSE;
+        $entities[$entity_id] = $new_entity;
+      }
+    }
+  }
+}
+
+/**
+ * Get all unpublished changesets.
+ */
+function cps_get_unpublished_changesets() {
+  $changesets = &drupal_static(__FUNCTION__);
+  if (!isset($changesets)) {
+    $changesets = db_query('SELECT changeset_id FROM {cps_changeset} WHERE published = 0')->fetchCol();
+  }
+  return $changesets;
+}
+
+/**
+ * Implements hook_entity_presave().
+ */
+function cps_entity_presave($entity, $entity_type) {
+  if (cps_is_supported($entity_type)) {
+    $initial = FALSE;
+    $current_changeset = cps_get_current_changeset();
+    if (!empty($entity->cps_initial_unpublished)) {
+      $initial = TRUE;
+      cps_override_changeset('initial');
+    }
+    else if ($current_changeset != CPS_PUBLISHED_CHANGESET) {
+      list($id) = entity_extract_ids($entity_type, $entity);
+      // Force a draft revision to be created when updating entities within a
+      // changeset. However respect the state of $entity->is_draft_revision if
+      // it is already set since drafty uses this internally to re-save the
+      // published version, and other modules may need the ability to override
+      // this in specific circumstances.
+      if (isset($id) && !isset($entity->is_draft_revision)) {
+        $entity->is_draft_revision = TRUE;
+      }
+    }
+
+    cps_set_revision_log($entity);
+
+    if ($initial) {
+      // Ordinarily we should use NULL to reset the override, but
+      // we may have already been overridden when we got here so just
+      // put back what we found instead.
+      cps_override_changeset($current_changeset);
+    }
+  }
+}
+
+/**
+ * Implements hook_entity_insert().
+ */
+function cps_entity_insert($entity, $entity_type) {
+  if (!cps_is_supported($entity_type)) {
+    return;
+  }
+
+  $changeset = cps_get_current_changeset();
+  list($entity_id, $revision_id) = entity_extract_ids($entity_type, $entity);
+
+  // If a new entity is inserted with the live/published changeset active,
+  // it is immediately live. Mark it as such in the most recently published
+  // changeset. This handles making sure that there is a history for
+  // programmatically created entities outside of changesets.
+  if ($changeset == CPS_PUBLISHED_CHANGESET) {
+    $changeset_id = db_query("SELECT changeset_id FROM {cps_changeset} ORDER BY published DESC LIMIT 1")->fetchCol();
+    db_insert('cps_entity')
+      ->fields(array(
+        'entity_type' => $entity_type,
+        'entity_id' => $entity_id,
+        'revision_id' => $revision_id,
+        'changeset_id' => $changeset_id,
+        'published' => TRUE,
+      ))->execute();
+
+    return;
+  }
+
+  // When an new entity is inserted, we actually want two revisions to be saved.
+  //  - An unpublished revision in the 'published' changeset, since CPS treats
+  //    any entity that's never been published as 'unpublished'.
+  //  - A draft revision in the current changeset with the published flag set to
+  //    the same as on the node form.
+
+  db_insert('cps_entity')
+    ->fields(array(
+      'entity_type' => $entity_type,
+      'entity_id' => $entity_id,
+      'changeset_id' => $changeset,
+      'revision_id' => $revision_id,
+   ))->execute();
+  // Operate on a clone to avoid changing the entity prior to subsequent
+  // hook_entity_insert() implementations.
+  $live_revision = clone $entity;
+
+  // The entity is no longer new.
+  unset($live_revision->is_new);
+
+  entity_status_set($entity_type, $live_revision, ENTITY_UNPUBLISHED);
+
+  $live_revision->cps_initial_unpublished = TRUE;
+  module_invoke_all('cps_live_revision_create', $entity_type, $live_revision);
+  entity_save($entity_type, $live_revision);
+}
+
+/**
+ * Implements hook_entity_update().
+ */
+function cps_entity_update($entity, $entity_type) {
+  $changeset = cps_get_current_changeset();
+  // If we are not on a change set, do nothing.
+  if ($changeset == CPS_PUBLISHED_CHANGESET || !cps_is_supported($entity_type)) {
+    return;
+  }
+
+  // @see cps_entity_insert().
+  if (!empty($entity->cps_initial_unpublished)) {
+    $changeset = 'initial';
+  }
+
+  // Only the 'initial' revision and draft revisions should be tracked in
+  // changesets. Everything else is in the 'published' changeset which is
+  // untracked.
+  if ($changeset !== 'initial' && !drafty()->isDraftRevision($entity)) {
+    return;
+  }
+
+  list($entity_id, $revision_id) = entity_extract_ids($entity_type, $entity);
+  // Store state information.
+  db_merge('cps_entity')
+    ->key(array(
+      'entity_type' => $entity_type,
+      'entity_id' => $entity_id,
+      'changeset_id' => $changeset,
+    ))
+    ->fields(array(
+      'revision_id' => $revision_id,
+    ))
+    ->execute();
+}
+
+/**
+ * Implements hook_entity_delete().
+ */
+function cps_entity_delete($entity, $entity_type) {
+  list($entity_id) = entity_extract_ids($entity_type, $entity);
+  db_delete('cps_entity')
+    ->condition('entity_type', $entity_type)
+    ->condition('entity_id', $entity_id)
+    ->execute();
+}
+
+/**
+ * Implements hook_field_attach_delete_revision().
+ */
+function cps_field_attach_delete_revision($entity_type, $entity) {
+  list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
+  db_delete('cps_entity')
+    ->condition('entity_type', $entity_type)
+    ->condition('revision_id', $revision_id)
+    ->condition('entity_id', $entity_id)
+    ->execute();
+}
+
+function cps_form_views_exposed_form_alter(&$form, &$form_state, $form_id) {
+  if (isset($form_state['view']) && ($form_state['view']->tag == 'cps' || $form_state['view']->tag == 'only mine') && isset($form['uid_current'])) {
+    $form['uid_current']['#options'] = array(
+      'All' => t('All'),
+      '1' => t('Only mine'),
+    );
+  }
+}
+
+// -----------------------------------------------------------------------
+// Contrib Hooks
+
+/**
+ * Implements hook_ctools_plugin_directory().
+ */
+function cps_ctools_plugin_directory($module, $plugin) {
+  if ($module == 'cps') {
+    return 'plugins/' . $plugin;
+  }
+}
+
+/**
+ * Implements hook_iib_page_item().
+ */
+function cps_iib_page_item() {
+  if (user_access('view changesets')) {
+    $path = drupal_get_path('module', 'cps');
+    ctools_include('forms', 'cps');
+    $items['left'] = array(
+      '#weight' => -10,
+      'form' => drupal_get_form('cps_changeset_preview_form'),
+      '#attached' => array(
+        'css' => array("$path/css/cps.css"),
+        'js' => array(
+          "$path/js/cps.js",
+          array(
+            'data' => array(
+              'cps' => array('changeset_id' => cps_get_current_changeset(TRUE)),
+            ),
+            'type' => 'setting',
+          ),
+        ),
+      ),
+    );
+    return $items;
+  }
+}
+
+/**
+ * Implements hook_iib_hidden_alter().
+ */
+function cps_iib_hidden_alter(&$hidden) {
+  $hidden = FALSE;
+}
+
+/**
+ * Implements hook_ctools_plugin_api().
+ */
+function cps_ctools_plugin_api($module, $api) {
+  if ($module == 'cps' && $api == 'cps') {
+    return array('version' => 1);
+  }
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function cps_views_api() {
+  return array(
+    'path' => drupal_get_path('module', 'cps') . '/plugins/views',
+    'api' => 3,
+  );
+}
+
+/**
+ * Sets the revision log for an entity.
+ */
+function cps_set_revision_log($entity) {
+  $current_changeset = cps_get_current_changeset();
+  if ($current_changeset != CPS_PUBLISHED_CHANGESET) {
+    $changeset = cps_changeset_load($current_changeset);
+    if ($changeset) {
+      $name = $changeset->name;
+    }
+    else {
+      $name = $current_changeset;
+    }
+  }
+  else {
+    $name = 'Published';
+  }
+  $entity->log = t('Revision for site version @changeset', array('@changeset' => $name));
+}
+
+/**
+ * Implements hook_entity_property_info().
+ */
+function cps_entity_property_info() {
+  return entity_get_controller('cps_changeset')->hook_entity_property_info();
+}
+
+/**
+ * Implements hook_entitycache_load_alter().
+ */
+function cps_entitycache_load_alter(&$entities) {
+  if (cps_get_current_changeset() !== CPS_PUBLISHED_CHANGESET) {
+    // If the state is not empty, do not let entitycache load things from cache.
+    $entities = array();
+  }
+}
+
+/**
+ * Implements hook_entitycache_save_alter().
+ */
+function cps_entitycache_save_alter(&$entities) {
+  if (cps_get_current_changeset() !== CPS_PUBLISHED_CHANGESET) {
+    // If the state is not empty, do not let entitycache save things in cache.
+    $entities = array();
+  }
+}
+
+// -----------------------------------------------------------------------
+// Public API
+
+/**
+ * Fetch the currently in use changeset.
+ *
+ * @param $test
+ *   If TRUE, test to ensure the changeset is valid.
+ *
+ * @return string
+ *   The current changeset id.
+ */
+function cps_get_current_changeset($test = FALSE) {
+
+  if (isset($GLOBALS['cps_override_changeset'])) {
+    return $GLOBALS['cps_override_changeset'];
+  }
+  if (!function_exists('user_access')) {
+    drupal_load('module', 'user');
+  }
+
+  if (!drupal_is_cli() && !user_access('preview changesets') && !user_access('view changesets') && !user_access('administer changesets')) {
+    return CPS_PUBLISHED_CHANGESET;
+  }
+
+  if (isset($_GET['changeset_id'])) {
+    $changeset_id = $_GET['changeset_id'];
+    $_SESSION['changeset_id'] = $changeset_id;
+  }
+  elseif (isset($_SESSION['changeset_id'])) {
+    $changeset_id = $_SESSION['changeset_id'];
+  }
+  else {
+    $changeset_id = CPS_PUBLISHED_CHANGESET;
+  }
+
+  // Use a $test flag because there are phases where we do not want to invoke
+  // a load.
+  if ($test && $changeset_id != CPS_PUBLISHED_CHANGESET) {
+    $changeset = cps_changeset_load($changeset_id);
+
+    if (!$changeset) {
+      // If no changeset was loaded, default to published.
+      $changeset_id = CPS_PUBLISHED_CHANGESET;
+    }
+  }
+
+  if ($changeset_id != CPS_PUBLISHED_CHANGESET) {
+    // Record that we have touched this changeset.
+    $_SESSION['touched changesets'][$changeset_id] = REQUEST_TIME;
+  }
+
+  return $changeset_id;
+}
+
+/**
+ * Set the current changeset.
+ *
+ * @param string $changeset_id
+ *   The changeset id.
+ */
+function cps_set_current_changeset($changeset_id) {
+  if (isset($changeset_id)) {
+    $_SESSION['changeset_id'] = $changeset_id;
+  }
+  elseif (isset($_SESSION['changeset_id'])) {
+    unset($_SESSION['changeset_id']);
+  }
+}
+
+/**
+ * Set a global override for the current changeset.
+ *
+ * @param $changeset_id
+ */
+function cps_override_changeset($changeset_id) {
+  global $cps_override_changeset;
+  $cps_override_changeset = $changeset_id;
+
+  // We have to reset the entity static cache because changing this could poison
+  // the cache.
+  foreach (entity_get_info() as $entity_type => $entity_info) {
+    if (cps_is_supported($entity_type)) {
+      entity_get_controller($entity_type)->resetCache();
+    }
+  }
+}
+
+/**
+ * Check whether CPS is supported for an entit type.
+ *
+ * @param string $entity_type
+ */
+function cps_is_supported($entity_type) {
+  $info = entity_get_info($entity_type);
+  return !empty($info['cps']);
+}
+
+/**
+ * Get supported entity types.
+ */
+function cps_get_supported() {
+  $types = array();
+  $info = entity_get_info();
+  foreach ($info as $entity_type => $entity_info) {
+    if (!empty($entity_info['cps'])) {
+      $types[] =  $entity_type;
+    }
+  }
+  return $types;
+}
+
+/**
+ * @{inheritdoc}
+ */
+function cps_make_revision_published($entity_type, $entity_id, $changeset_id) {
+  cps_override_changeset(CPS_PUBLISHED_CHANGESET);
+  // Find the revision id.
+  $revision_id = db_query('SELECT revision_id FROM {cps_entity} WHERE entity_type = :entity_type AND entity_id = :entity_id AND changeset_id = :changeset',
+  array(
+    ':entity_type' => $entity_type,
+    ':entity_id' => $entity_id,
+    ':changeset' => $changeset_id,
+  ))->fetchField();
+
+  drafty()->publishRevision($entity_type, $entity_id, $revision_id);
+  cps_override_changeset(NULL);
+}
+
+/**
+ * Return TRUE if the entity is in the changeset.
+ *
+ * @param $entity
+ *   The entity to check.
+ * @param $changeset_id
+ *   A changeset ID.
+ *
+ * @return bool
+ */
+function cps_is_entity_in_changeset($entity_type, $entity, $changeset_id) {
+  list($id) = entity_extract_ids($entity_type, $entity);
+  $tracked = cps_get_tracked_entities($changeset_id);
+  return isset($tracked[$entity_type][$id]);
+}
+
+/**
+ * Get tracked entities in a changeset.
+ *
+ * @param $changeset_id
+ *   A changeset ID.
+ *
+ * @return
+ *   A nested array. First keyed by entity type, then by entity ID, with
+ *   revision IDs as values.
+ *   type.
+ */
+function cps_get_tracked_entities($changeset_id) {
+  $cache = &drupal_static(__FUNCTION__, array());
+  if (!isset($cache[$changeset_id])) {
+    $cache[$changeset_id] = [];
+    $result = db_query('SELECT entity_type, entity_id, revision_id  FROM {cps_entity} WHERE changeset_id = :changeset_id AND published = 0', array(':changeset_id' => $changeset_id));
+    foreach ($result as $record) {
+      $cache[$changeset_id][$record->entity_type][$record->entity_id] = $record->revision_id;
+    }
+  }
+  return $cache[$changeset_id];
+}
+
+/**
+ * Publish a changeset.
+ *
+ * @param $changeset_id
+ *   A changeset ID.
+ */
+function cps_publish_changeset_entities($changeset_id) {
+  // Get all tracked entities in the changeset.
+  $changed = cps_get_tracked_entities($changeset_id);
+  $transaction = db_transaction();
+  try {
+    foreach ($changed as $entity_type => $ids) {
+      foreach ($ids as $entity_id => $revision_id) {
+        cps_make_revision_published($entity_type, $entity_id, $changeset_id);
+      }
+    }
+  }
+  catch (Exception $e) {
+    $transaction->rollback();
+    trigger_error(t('Unable to publish entities in changeset'), E_USER_ERROR);
+    watchdog_exception('CPS', $e);
+  }
+}
+
+/**
+ * Unpublish a changeset.
+ *
+ * @param $changeset_id
+ *   A changeset ID.
+ */
+function cps_unpublish_changeset_entities($changeset_id, $previous_changeset_id) {
+  $changed = cps_get_tracked_entities($changeset_id);
+  $transaction = db_transaction();
+  try {
+    foreach ($changed as $entity_type => $ids) {
+      foreach ($ids as $entity_id => $revision_id) {
+        // If the entity existed in the previous changeset ID, use that version.
+        $vid = db_query('SELECT revision_id FROM {cps_entity} WHERE entity_type = :entity_type AND entity_id = :entity_id AND changeset_id = :changeset_id', array(
+          'entity_type' => $entity_type,
+          'entity_id' => $entity_id,
+          'changeset_id' => $previous_changeset_id,
+        ))->fetchField();
+        if ($vid) {
+          cps_make_revision_published($entity_type, $entity_id, $previous_changeset_id);
+        }
+        // Otherwise assume the entity was new in the changeset, and publish the
+        // 'initial' version.
+        else {
+          cps_make_revision_published($entity_type, $entity_id, 'initial');
+        }
+      }
+    }
+  }
+  catch (Exception $e) {
+    $transaction->rollback();
+    trigger_error(t('Unable to unpublish entities in changeset'), E_USER_ERROR);
+    watchdog_exception('CPS', $e);
+  }
+}
+
+/**
+ * Remove non-tracked items from a changeset.
+ *
+ * @param $changeset_id
+ *   A changeset ID.
+ */
+function cps_remove_untracked_entities($changeset_id) {
+  db_delete('cps_entity')
+    ->condition('changeset_id', $changeset_id)
+    ->condition('published', 1)
+    ->execute();
+}
+
+/**
+ * Record the published version ID for untracked entities.
+ *
+ * @param $changeset_id
+ *   The changeset ID.
+ * @param $entity_type
+ *   Then entity type.
+ * @param $start
+ *   The entity ID to start the query from, defaults to 0.
+ * @param $limit
+ *   The number of entities to process.
+ */
+function cps_add_untracked_entities($changeset_id, $entity_type, $start = 0, $limit = 100) {
+
+  // Ensure the EntityFieldQuery here does not get altered.
+  cps_override_changeset(CPS_PUBLISHED_CHANGESET);
+  $tracked = cps_get_tracked_entities($changeset_id);
+  $info = entity_get_info($entity_type);
+
+  $entity_query = new EntityFieldQuery();
+  if (!empty($tracked[$entity_type])) {
+    $entity_query->entityCondition('entity_id', array_keys($tracked[$entity_type]), 'NOT IN');
+  }
+  $results = $entity_query
+    ->entityCondition('entity_type', $entity_type)
+    ->entityOrderBy('entity_id', 'ASC')
+    ->range($start, $limit)
+    ->execute();
+
+  // Reset the changeset
+  cps_override_changeset(NULL);
+  if (!empty($results[$entity_type])) {
+    $query = db_insert('cps_entity')
+      ->fields(array(
+        'entity_type',
+        'entity_id',
+        'revision_id',
+        'changeset_id',
+        'published',
+      ));
+    foreach ($results[$entity_type] as $entity) {
+      $query->values(array(
+        'entity_type' => $entity_type,
+        'entity_id' => $entity->{$info['entity keys']['id']},
+        'revision_id' => $entity->{$info['entity keys']['revision']},
+        'changeset_id' => $changeset_id,
+        'published' => 1,
+      ));
+    }
+    $query->execute();
+    // Return the count of entities that were updated.
+    return min($limit, count($results[$entity_type]));
+  }
+}
+
+/**
+ * Determine if entity editing is allowable.
+ *
+ * We disallow the editing or creation of entities and anything else managed by
+ * changesets when viewing the published changeset (i.e, the live site) or when
+ * viewing a previously published changeset (i.e, an archive view).
+ *
+ * @param $op
+ *   The operation to check. Any $op which isn't view' is considered an edit,
+ *   so non-privileged non-view operations should be filtered by the caller.
+ * @param string $entity_type
+ *   The type of entity to test.
+ * @param $entity
+ *   The entity to check.
+ * @param $changeset_id
+ *   The changeset to check against. If not specified the currently active changeset
+ *   will be used.
+ *
+ * @return bool
+ */
+function cps_can_edit_entities($op, $entity_type, $entity, $changeset_id = NULL) {
+  // Deletions can occur outside of a changeset if the content was never published.
+  if ($op == 'delete') {
+    return cps_can_delete_entity($entity_type, $entity);
+  }
+
+  if (!isset($changeset_id)) {
+    $changeset_id = cps_get_current_changeset(TRUE);
+  }
+
+  if ($op == 'view') {
+    if ($changeset_id != CPS_PUBLISHED_CHANGESET) {
+      return TRUE;
+    }
+
+    if (entity_status_supported($entity_type)) {
+      return entity_status_get($entity_type, $entity);
+    }
+    else {
+      return TRUE;
+    }
+  }
+
+  // The live changeset can never be edited for any reason, and we just bounce
+  // it out early, without even letting other modules get involved.
+  if ($changeset_id == CPS_PUBLISHED_CHANGESET) {
+    return FALSE;
+  }
+
+  $changeset = cps_changeset_load($changeset_id);
+
+  // Only unpublished changesets allow editing.
+  $access = $changeset->status == 'unpublished';
+
+  // Unless the user has edit all changesets, they can only make changes in
+  // their own.
+  if (!user_access('edit all changesets') && $GLOBALS['user']->uid != $changeset->uid) {
+    $access = FALSE;
+  }
+
+  // If $op == 'move to' they cannot make this change if the entity already has
+  // a revision in the changeset.
+  if ($op == 'move to') {
+    if ($entity && cps_is_entity_in_changeset($entity_type, $entity, $changeset_id)) {
+      $access = FALSE;
+    }
+  }
+
+  // Allow other modules to change this if needed.
+  $context = array(
+    'op' => $op,
+    'entity_type' => $entity_type,
+    'entity' => $entity,
+    'changeset' => $changeset,
+  );
+  drupal_alter('cps_can_edit_entities', $access, $context);
+  return $access;
+}
+
+/**
+ * Determine if an entity can be deleted.
+ *
+ * Once an entity has appeared in a published changeset, the entity should
+ * not be deletable so that the site history is preserved. Instead, entities
+ * should be unpublished which will make them disappear from standard view.
+ *
+ * This method ONLY checks for CPS reasons, and the entity is expected
+ * to do its own access control.
+ *
+ * @param string $entity_type
+ *   The type of entity to test.
+ * @param $entity
+ *   The entity to check.
+ * @return bool
+ *   TRUE if the entity can be deleted, FALSE if not.
+ */
+function cps_can_delete_entity($entity_type, $entity) {
+  list($entity_id) = entity_extract_ids($entity_type, $entity);
+
+  // Fetch a list of revisions directly joining to the cps table to find
+  // revisions that point to published changesets.
+  $entity_info = entity_get_info($entity_type);
+  $revision_table = $entity_info['revision table'];
+  $id_field = $entity_info['entity keys']['id'];
+  $revision_field = $entity_info['entity keys']['revision'];
+
+  $result = db_query("SELECT DISTINCT base.$revision_field FROM " . "{" . $revision_table . "} base LEFT JOIN {cps_entity} c ON base.$id_field = c.entity_id AND base.$revision_field = c.revision_id AND c.entity_type = '$entity_type' LEFT JOIN {cps_changeset} cs ON cs.changeset_id = c.changeset_id WHERE base.$id_field = :entity_id AND cs.published <> 0", array(':entity_id' => $entity_id))->fetchCol();
+
+  if (!$result) {
+    return TRUE;
+  }
+
+  if (!entity_status_supported($entity_type)) {
+    return FALSE;
+  }
+
+  // If we do have a result, go through each revision and see if any are published. If any are,
+  // deny.
+  foreach ($result as $revision_id) {
+    $revision = entity_revision_load($entity_type, $revision_id);
+    if ($revision && entity_status_get($entity_type, $revision)) {
+      return FALSE;
+    }
+  }
+
+  return TRUE;
+}
+
+/**
+ * Gets the available changesets for the widget.
+ *
+ * @return string[]
+ *   The list of available changesets, keyed by ID.
+ */
+function cps_get_changesets() {
+  global $user;
+  try {
+    $query = new EntityFieldQuery();
+    $results = $query->entityCondition('entity_type', 'cps_changeset')
+      ->propertyCondition('status', CPS_ARCHIVED_STATUS, '<>')
+      // Note that uid and locked check are in a db_or() in a query alter
+      // because EFQ does not support OR.
+      ->propertyOrderBy('name')
+      // This tag allows another module to modify this list if necessary.
+      ->addTag('cps_active_changeset_list')
+      ->execute();
+  }
+  catch (Exception $e) {
+    return array();
+  }
+
+  $changesets = array();
+  if (!empty($results['cps_changeset'])) {
+    $entities = cps_changeset_load_multiple(array_keys($results['cps_changeset']));
+    foreach ($entities as $id => $entity) {
+      $changesets[$id] = $entity->name;
+      if ($entity->lock_in_select) {
+        $locked[$id] = $entity->name;
+      }
+      // Assume if it's not locked it gets listed under 'mine'.
+      else {
+        $mine[$id] = $entity->name;
+      }
+    }
+  }
+
+  $published = array(
+    0 => t('Live'),
+  );
+
+  $recent = array();
+  $my_published = array();
+  // Go through any changesets they have viewed within the last 24 hours
+  // and create a list of them.
+  if (!empty($_SESSION['touched changesets'])) {
+    $recent_ids = array();
+    foreach ($_SESSION['touched changesets'] as $id => $timestamp) {
+      if ($timestamp + variable_get('cps_recent_changeset_max_age', 86400) < REQUEST_TIME) {
+        unset($_SESSION['touched changesets'][$id]);
+      }
+      elseif (empty($changesets[$id])) {
+        $recent_ids[] = $id;
+      }
+    }
+
+    if ($recent_ids) {
+      $entities = cps_changeset_load_multiple($recent_ids);
+      $uids = array();
+      foreach ($entities as $entity) {
+        $uids[] = $entity->uid;
+      }
+
+      $accounts = user_load_multiple($uids);
+      foreach ($entities as $id => $entity) {
+        if ($entity->uid == $user->uid) {
+          $my_published[$id] = $entity->name;
+        }
+        else {
+          $recent[format_username($accounts[$entity->uid])][$id] = $entity->name;
+        }
+      }
+    }
+  }
+
+  // Live changeset is always on top.
+  $retval = $published;
+
+  // Now my open changesets.
+  if (!empty($locked)) {
+    $retval += array(t('Locked open site versions') => $locked);
+  }
+
+  if (!empty($mine)) {
+    $retval += array(t('My open site versions') => $mine);
+  }
+
+  // My published if I've looked at them recently.
+  if ($my_published) {
+    $retval += array(t('My published') => $my_published);
+  }
+
+  // And other people's changesets if I've been looking at them.
+  if ($recent) {
+    asort($recent);
+    $retval += $recent;
+  }
+
+  return $retval;
+}
+
+/**
+ * Implements hook_query_TAG_alter().
+ */
+function cps_query_cps_active_changeset_list_alter($query) {
+  // Because EFQ doesn't support OR, this hack adds a db_or() to the underlying
+  // query.
+  $query->condition(db_or()
+    ->condition('uid', $GLOBALS['user']->uid)
+    ->condition('lock_in_select', TRUE)
+  );
+}
+
+/**
+ * Fetch a list of possible state types for changesets.
+ *
+ * @return string[]
+ *   An array of state types named keyed by the internal name.
+ */
+function cps_changeset_get_state_types() {
+  $types = &drupal_static(__FUNCTION__);
+  if (!isset($types)) {
+    $types = array(
+      'open' => t('Open'),
+      'closed' => t('Closed'),
+    );
+
+    drupal_alter('cps_changeset_state_types', $types);
+  }
+
+
+  return $types;
+}
+
+/**
+ * Fetch a list of possible states for changesets.
+ *
+ * @return array[]
+ *   An array of state named keyed by the internal name.
+ */
+function cps_changeset_get_states() {
+  $states = &drupal_static(__FUNCTION__);
+  if (!isset($states)) {
+    $states = array(
+      'unpublished' => array(
+        'label' => t('Unpublished'),
+        'weight' => 0,
+        'type' => 'open',
+      ),
+      CPS_ARCHIVED_STATUS => array(
+        'label' => t('Archived'),
+        'weight' => 9,
+        'type' => 'closed',
+      ),
+    );
+    drupal_alter('cps_changeset_states', $states);
+
+    uasort($states, 'drupal_sort_weight');
+  }
+
+  return $states;
+}
+
+/**
+ * Fetch the labels for all changeset states.
+ *
+ * @return string[]
+ */
+function cps_changeset_get_state_labels() {
+  $labels = &drupal_static(__FUNCTION__);
+  if (!$labels) {
+    $states = cps_changeset_get_states();
+
+    $labels = array();
+    foreach ($states as $state => $info) {
+      $labels[$state] = $info['label'];
+    }
+  }
+
+  return $labels;
+}
+
+/**
+ * Provide a warning message if the entity has unpublished changes in another changeset.
+ *
+ * @param object $entity
+ *   The entity to test.
+ *
+ * @return string|null
+ *   A warning string or null.
+ */
+function cps_changeset_edit_warning($entity_type, $entity) {
+  if (cps_entity_in_unpublished_changeset($entity_type, $entity, TRUE)) {
+    // If anything else pops, put up a warning.
+    return t('Warning, this content has unpublished changes in another site version! Whichever site version is published LAST will overwrite the other. Please see the History tab for details to help coordinate your changes.');
+  }
+}
+
+/**
+ * Determine whether an entity is in any published changeset.
+ */
+function cps_entity_in_published_changeset($entity_type, $entity) {
+  list($id) = entity_extract_ids($entity_type, $entity);
+  // Treat a changeset as published if the status is 'live' or 'archived'.
+  $result = db_query('SELECT 1 FROM {cps_entity} ce INNER JOIN {cps_changeset} cc ON ce.changeset_id = cc.changeset_id AND cc.status IN (:published) WHERE ce.entity_type = :type AND ce.entity_id = :id', array(':published' => array(CPS_ARCHIVED_STATUS, 'published'), ':type' => $entity_type, ':id' => $id))->fetchField();
+
+  return $result;
+}
+
+/**
+ * Determine whether an entity is in any unpublished changeset.
+ *
+ * @param $entity_type
+ *   The entity type.
+ * @param $entity
+ *   The entity object.
+ * @param $exclude_current_changeset
+ *   Whether the query should exclude the current active changeset, useful if
+ *   checking whether an entity is in more than one pending changeset. Defaults
+ *   to FALSE.
+ *
+ *   @return
+ *     Boolean if the entity is in an unpublished changeset.
+ */
+function cps_entity_in_unpublished_changeset($entity_type, $entity, $exclude_current_changeset = FALSE) {
+  // Treat a changeset as published if the status is 'live' or 'archived'.
+  list($id) = entity_extract_ids($entity_type, $entity);
+  $changeset = cps_get_current_changeset(TRUE);
+  // Exclude the current changeset if requested, unless it's the published
+  // changeset which is never in the database anyway.
+  $query = db_select('cps_entity', 'ce');
+  $query->join('cps_changeset', 'cc', 'ce.changeset_id = cc.changeset_id');
+  $query->condition('cc.status', [CPS_ARCHIVED_STATUS, 'published'], 'NOT IN')
+    ->condition('ce.entity_type', $entity_type)
+    ->condition('ce.entity_id', $id)
+    ->range(0, 1)
+    ->addExpression(1);
+
+  if ($exclude_current_changeset && $changeset != CPS_PUBLISHED_CHANGESET) {
+    $query->condition('ce.changeset_id', $changeset, '<>');
+  }
+  return $query->execute()->fetchField();
+}
+
+
+/**
+ * Lock the CPS publishing system.
+ *
+ * @return bool
+ *   TRUE if the lock can be acquired. FALSE if not.
+ *
+ * @see cps_publish_lock_time()
+ */
+function cps_publish_lock() {
+  $time = cps_publish_lock_time();
+  if ($time) {
+    return FALSE;
+  }
+  // Set the lock to expire in 30 minutes.
+  // This seems like a long time, but some publishing runs can be quite long, and we want
+  // to make sure they finish and that a timeout truly represents an aborted publishing job,
+  // for example because the user navigated away from the batch job before it completed.
+  // Because this is intended to be used in batch processes (for which the lock needs to be
+  // held for more than one page request, until the batch job is complete) it cannot use the
+  // core lock API. Therefore it uses a variable instead.
+  variable_set('cps_publishing_lock', REQUEST_TIME + variable_get('cps_publishing_lock_expire', 60 * 30));
+  return TRUE;
+}
+
+/**
+ * Unlock the CPS publishing system.
+ */
+function cps_publish_unlock() {
+  variable_del('cps_publishing_lock');
+}
+
+/**
+ * Test the CPS publishing lock.
+ *
+ * @return int
+ *   The number of seconds remaining if publishing is locked, FALSE if it is not.
+ */
+function cps_publish_lock_time() {
+  $lock = variable_get('cps_publishing_lock', 0);
+  if ($lock > REQUEST_TIME) {
+    return $lock - REQUEST_TIME;
+  }
+
+  return FALSE;
+}
+
+// -------------------------------------------------------------------------
+// Database and general entity API functions
+
+/**
+ * Changeset entity loader.
+ *
+ * @param string $changeset_id
+ *   The changeset id.
+ *
+ * @return CPSChangeset
+ *   The changeset, if found.
+ *
+ * @see entity_load()
+ */
+function cps_changeset_load($changeset_id) {
+  $entities = cps_changeset_load_multiple(array($changeset_id));
+  if ($entities) {
+    return reset($entities);
+  }
+}
+
+/**
+ * Load multiple changesets.
+ *
+ * @param string[] $ids
+ *   An array of IDs to load.
+ * @param array $conditions
+ *   (optional) An array of conditions.
+ * @param bool $reset
+ *   (optional) TRUE to force a load from database.
+ *
+ * @see entity_load_multiple()
+ */
+function cps_changeset_load_multiple($ids, $conditions = array(), $reset = FALSE) {
+  return entity_load('cps_changeset', $ids, $conditions, $reset);
+}
+
+/**
+ * Save a changeset entity.
+ *
+ * @param CPSChangeset $entity
+ *   The entity to save.
+ *
+ * @see node_save()
+ */
+function cps_changeset_save(CPSChangeset $entity) {
+  return entity_get_controller('cps_changeset')->save($entity);
+}
+
+/**
+ * Delete a changeset entity.
+ *
+ * @param $changeset_id
+ *   A changeset ID.
+ */
+function cps_changeset_delete($changeset_id) {
+  cps_changeset_delete_multiple(array($changeset_id));
+}
+
+/**
+ * Delete a revision for a changeset entity.
+ *
+ * @param $vid
+ *   The revision id to delete.
+ */
+function cps_changeset_delete_revision($vid) {
+  return entity_get_controller('cps_changeset')->deleteRevision($vid);
+}
+
+/**
+ * Delete multiple changesets.
+ *
+ * @param string[] $changeset_ids
+ *   An array of changeset IDs.
+ */
+function cps_changeset_delete_multiple(array $changeset_ids) {
+  return entity_get_controller('cps_changeset')->delete($changeset_ids);
+}
+
+/**
+ * View a changeset entity.
+ *
+ * @param CPSChangeset $entity
+ *   The changeset entity.
+ * @param string $view_mode
+ *   The view mode to view.
+ * @param string $langcode
+ *   The language to use or NULL.
+ *
+ * @return array
+ *   A Drupal renderable array.
+ *
+ * @see node_view()
+ */
+function cps_changeset_view(CPSChangeset $entity, $view_mode = 'full', $langcode = NULL) {
+  return entity_get_controller('cps_changeset')->view($entity, $view_mode, $langcode);
+}
+
+/**
+ * Callback to create a new entity.
+ *
+ * @param array $values
+ *   The initial values to seed the new entity with.
+ *
+ * @return CPSChangeset
+ */
+function cps_changeset_create($values = array()) {
+  return entity_get_controller('cps_changeset')->create($values);
+}
+
+/**
+ * Determine if the operation for the entity can be accessed.
+ *
+ * @param string $op
+ *   The operation being performed. One of 'view', 'update', 'create' or
+ *   'delete'. It may also be any changeset operation such as 'publish' or
+ *   'unpublish'.
+ * @param string|CPSChangeset $entity
+ *   (optional) an entity to check access for. If no entity is given, it will be
+ *   determined whether access is allowed for all entities of the given type.
+ * @param stdClass|null $account
+ *   The user to check for. Leave it to NULL to check for the global user.
+ *
+ * @return boolean
+ *   Whether access is allowed or not. If the entity type does not specify any
+ *   access information, NULL is returned.
+ *
+ * @see entity_access()
+ */
+function cps_changeset_access($op, $entity = NULL, $account = NULL) {
+  return entity_get_controller('cps_changeset')->access($op, $entity, $account);
+}
+
+/**
+ * Callback for getting content properties.
+ *
+ * @param CPSChangeset $changeset
+ *   The changeset entity to get proprties for.
+ * @param array $options
+ *   The options provided by the caller.
+ * @param $name
+ *   The name of the property.
+ *
+ * @return mixed
+ *   The result of the property.
+ */
+function cps_changeset_metadata_get_properties(CPSChangeset $changeset, array $options, $name) {
+  switch ($name) {
+    case 'is_new':
+      return empty($changeset->changeset_id) || !empty($changeset->is_new);
+
+    case 'edit_url':
+      return url('admin/structure/changesets/' . $changeset->changeset_id . '/edit', $options);
+
+    case 'delete_url':
+      return url('admin/structure/changesets/' . $changeset->changeset_id . '/delete', $options);
+
+    case 'author':
+      return !empty($changeset->uid) ? $changeset->uid : drupal_anonymous_user();
+
+    case 'operations':
+      $build = cps_changeset_get_operations($changeset);
+      return drupal_render($build);
+  }
+}
+
+/**
+ * Fetch a renderable array of changeset operations available for the changeset.
+ *
+ * @param CPSChangeset $changeset
+ *   The changeset to get operations for.
+ *
+ * @return array
+ *   A drupal Renderable array.
+ */
+function cps_changeset_get_operations(CPSChangeset $changeset) {
+  $uri = $changeset->uri();
+  $operations = array(
+    '#type' => 'container',
+    '#attributes' => array('class' => 'cps-changeset-operations'),
+    'site' => array(
+      '#theme' => 'link',
+      '#text' => t('view site'),
+      '#path' => '<front>',
+      '#options' => array(
+        'query' => array('changeset_id' => $changeset->changeset_id),
+        'html' => TRUE,
+        'attributes' => array('class' => array('cps-changeset-operation'))
+      ),
+      '#access' => user_access('preview changesets') || user_access('view changesets') || user_access('administer changesets'),
+    ),
+    'view' => array(
+      '#theme' => 'link',
+      '#text' => t('status'),
+      '#path' => $uri['path'],
+      '#options' => array(
+        'html' => TRUE,
+        'attributes' => array('class' => array('cps-changeset-operation'))
+      ),
+      '#access' => entity_access('view', 'cps_changeset', $changeset) && request_path() != $uri['path'],
+    ),
+    'edit' => array(
+      '#theme' => 'link',
+      '#text' => t('edit'),
+      '#path' => $uri['path'] . '/edit',
+      '#options' => array(
+        'query' => drupal_get_destination(),
+        'html' => TRUE,
+        'attributes' => array('class' => array('cps-changeset-operation'))
+      ),
+      '#access' => entity_access('update', 'cps_changeset', $changeset),
+    ),
+    'delete' => array(
+      '#theme' => 'link',
+      '#text' => t('delete'),
+      '#path' => $uri['path'] . '/delete',
+      '#options' => array(
+        'query' => drupal_get_destination(),
+        'html' => TRUE,
+        'attributes' => array('class' => array('cps-changeset-operation'))
+      ),
+      '#access' => entity_access('delete', 'cps_changeset', $changeset),
+    ),
+    'publish' => array(
+      '#theme' => 'link',
+      '#text' => t('publish'),
+      '#path' => $uri['path'] . '/publish',
+      '#options' => array(
+        'query' => drupal_get_destination(),
+        'html' => TRUE,
+        'attributes' => array('class' => array('cps-changeset-operation'))
+      ),
+      '#access' => entity_access('publish', 'cps_changeset', $changeset),
+    ),
+    'unpublish' => array(
+      '#theme' => 'link',
+      '#text' => t('unpublish'),
+      '#path' => $uri['path'] . '/unpublish',
+      '#options' => array(
+        'query' => drupal_get_destination(),
+        'html' => TRUE,
+        'attributes' => array('class' => array('cps-changeset-operation'))
+      ),
+      '#access' => entity_access('unpublish', 'cps_changeset', $changeset),
+    ),
+  );
+
+  drupal_alter('cps_changeset_operations', $operations, $changeset);
+  return $operations;
+}
+
+/**
+ * Convenience function to test whether the current changeset is active.
+ *
+ * @return bool
+ */
+function cps_is_current_changeset_active() {
+  return cps_get_current_changeset(TRUE) != CPS_PUBLISHED_CHANGESET;
+}
+
+// -----------------------------------------------------------------------
+// Menu callbacks that must be in .module
+
+/**
+ * _to_arg callback to provide a default when one is not provided.
+ *
+ * This is present to make tabs work correctly, and has no real value other
+ * than ensuring that warnings aren't given.
+ */
+function cps_changeset_optional_to_arg() {
+  return cps_get_current_changeset();
+}
+
+/**
+ * Menu _load callback for when cps_changeset is optional.
+ *
+ * @param string $changeset_id
+ *   A changeset ID taken from the URL.
+ *
+ * @return CPSChangeset
+ *   A changest object or NULL.
+ */
+function cps_changeset_optional_load($changeset_id) {
+  if ($changeset_id) {
+    return cps_changeset_load($changeset_id);
+  }
+}
+
+/**
+ * Determine if the 'switch to this site version' action should be visible.
+ *
+ * @param CPSChangeset $changeset
+ *
+ * @return bool
+ */
+function cps_changeset_view_site_access($changeset) {
+  $current_changeset_id = cps_get_current_changeset();
+  return $changeset && $current_changeset_id != $changeset->changeset_id && cps_changeset_access('view', $changeset);
+}
diff --git a/cps.module.rej b/cps.module.rej
new file mode 100644
index 0000000..de3aaab
--- /dev/null
+++ b/cps.module.rej
@@ -0,0 +1,60 @@
+--- cps.module
++++ cps.module
+@@ -1113,7 +1113,8 @@
+     $query = new EntityFieldQuery();
+     $results = $query->entityCondition('entity_type', 'cps_changeset')
+       ->propertyCondition('status', CPS_ARCHIVED_STATUS, '<>')
+-      ->propertyCondition('uid', $user->uid)
++      // Note that uid and locked check are in a db_or() in a query alter
++      // because EFQ does not support OR.
+       ->propertyOrderBy('name')
+       // This tag allows another module to modify this list if necessary.
+       ->addTag('cps_active_changeset_list')
+@@ -1128,6 +1129,13 @@
+     $entities = cps_changeset_load_multiple(array_keys($results['cps_changeset']));
+     foreach ($entities as $id => $entity) {
+       $changesets[$id] = $entity->name;
++      if ($entity->lock_in_select) {
++        $locked[$id] = $entity->name;
++      }
++      // Assume if it's not locked it gets listed under 'mine'.
++      else {
++        $mine[$id] = $entity->name;
++      }
+     }
+   }
+ 
+@@ -1173,8 +1181,12 @@
+   $retval = $published;
+ 
+   // Now my open changesets.
+-  if ($changesets) {
+-    $retval += array(t('My open site versions') => $changesets);
++  if (!empty($locked)) {
++    $retval += array(t('Locked open site versions') => $locked);
++  }
++
++  if (!empty($mine)) {
++    $retval += array(t('My open site versions') => $mine);
+   }
+ 
+   // My published if I've looked at them recently.
+@@ -1192,6 +1204,18 @@
+ }
+ 
+ /**
++ * Implements hook_query_TAG_alter().
++ */
++function cps_query_cps_active_changeset_list_alter($query) {
++  // Because EFQ doesn't support OR, this hack adds a db_or() to the underlying
++  // query.
++  $query->condition(db_or()
++    ->condition('uid', $GLOBALS['user']->uid)
++    ->condition('lock_in_select', TRUE)
++  );
++}
++
++/**
+  * Fetch a list of possible state types for changesets.
+  *
+  * @return string[]
diff --git a/cps_2451021_2.patch b/cps_2451021_2.patch
new file mode 100644
index 0000000..ee019ca
--- /dev/null
+++ b/cps_2451021_2.patch
@@ -0,0 +1,244 @@
+diff --git a/cps.module b/cps.module
+index 5933f38..6e18eff 100644
+--- a/cps.module
++++ b/cps.module
+@@ -329,10 +329,16 @@ function cps_menu() {
+  * Implements hook_entity_load().
+  */
+ function cps_entity_load(&$entities, $entity_type) {
++  $changeset = cps_get_current_changeset();
++  if ($changeset == CPS_PUBLISHED_CHANGESET) {
++    return;
++  }
++
+   $entity_info = entity_get_info($entity_type);
+   if (empty($entity_info['entity keys']['revision'])) {
+     return;
+   }
++
+   // When a non-default revision is being loaded, don't overwrite any of the
+   // contents of that revision.
+   foreach ($entities as $entity) {
+@@ -347,44 +353,20 @@ function cps_entity_load(&$entities, $entity_type) {
+     }
+   }
+ 
+-  $changeset = cps_get_current_changeset();
+-  // Fetch entity state information for not published changesets OR the current
+-  // one. We ignore published changesets because those will accrete over time
+-  // and are generally irrelevant to our needs.
+-  $keys = array_keys($entities);
+-
+-  // @todo: should this include the actual 'published' changeset, that has
+-  // published = 0 so is included in the query currently.
+-  $changesets = cps_get_unpublished_changesets();
+-  if ($changeset != CPS_PUBLISHED_CHANGESET) {
+-    $changesets[] = $changeset;
+-  }
+-  $result = db_query('SELECT * FROM {cps_entity} WHERE entity_type = :entity_type AND entity_id IN (:ids) AND changeset_id IN (:changesets)', array(':entity_type' => $entity_type, ':ids' => $keys, ':changesets' => $changesets));
+-  foreach ($result as $entity_state) {
+-    $entities[$entity_state->entity_id]->changeset[$entity_state->changeset_id] = $entity_state;
+-  }
+-
+-  // We only care further if the current changeset is not the published one.
+-  if (!$changeset) {
+-    return;
+-  }
+-
+   $revision_key = $entity_info['entity keys']['revision'];
+-
++  $tracked = cps_get_tracked_entities($changeset);
+   foreach ($entities as $entity) {
+     list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
+ 
+     // If this entity has a revision set for the current change set,
+     // we need to swap it out for the correct revision.
+-    if (!empty($entity->changeset[$changeset]) && $revision_id != $entity->changeset[$changeset]->revision_id) {
+-
+-      $swap_revision_id = $entity->changeset[$changeset]->revision_id;
++    if (isset($tracked[$entity_type][$entity_id]) && $tracked[$entity_type][$entity_id] != $revision_id) {
++      $swap_revision_id = $tracked[$entity_type][$entity_id];
+       $draft_revisions = entity_load($entity_type, array($entity_id), array($revision_key => $swap_revision_id));
+ 
+       // Swap the entity!
+       if (!empty($draft_revisions[$entity_id])) {
+         $new_entity = $entities[$entity_id] = $draft_revisions[$entity_id];
+-        $new_entity->changeset = $entity->changeset;
+         // Store the current revision ID so we can tell later if we need to put it back.
+         $new_entity->published_revision_id = $revision_id;
+         $new_entity->published_revision = clone($entity);
+@@ -392,10 +374,6 @@ function cps_entity_load(&$entities, $entity_type) {
+         $new_entity->default_revision = FALSE;
+         $entities[$entity_id] = $new_entity;
+       }
+-      else {
+-        // This state is invalid, and all we can do is remove the changeset from the list.
+-        unset($entity->changeset[$changeset]);
+-      }
+     }
+   }
+ }
+@@ -820,8 +798,10 @@ function cps_make_revision_published($entity_type, $entity_id, $changeset_id) {
+  *
+  * @return bool
+  */
+-function cps_is_entity_in_changeset($entity, $changeset_id) {
+-  return isset($entity->changesets[$changeset_id]);
++function cps_is_entity_in_changeset($entity_type, $entity, $changeset_id) {
++  list($id) = entity_extract_ids($entity_type, $entity);
++  $tracked = cps_get_tracked_entities($changeset_id);
++  return isset($tracked[$entity_type][$id]);
+ }
+ 
+ /**
+@@ -831,15 +811,17 @@ function cps_is_entity_in_changeset($entity, $changeset_id) {
+  *   A changeset ID.
+  *
+  * @return
+- *   An array with entity type as keys and array of entity IDs as values.
++ *   A nested array. First keyed by entity type, then by entity ID, with
++ *   revision IDs as values.
+  *   type.
+  */
+ function cps_get_tracked_entities($changeset_id) {
+   $cache = &drupal_static(__FUNCTION__, array());
+   if (!isset($cache[$changeset_id])) {
+-    $result = db_query('SELECT entity_type, entity_id  FROM {cps_entity} WHERE changeset_id = :changeset_id AND published = 0', array(':changeset_id' => $changeset_id));
++    $cache[$changeset_id] = [];
++    $result = db_query('SELECT entity_type, entity_id, revision_id  FROM {cps_entity} WHERE changeset_id = :changeset_id AND published = 0', array(':changeset_id' => $changeset_id));
+     foreach ($result as $record) {
+-      $cache[$changeset_id][$record->entity_type][$record->entity_id] = $record->entity_id;
++      $cache[$changeset_id][$record->entity_type][$record->entity_id] = $record->revision_id;
+     }
+   }
+   return $cache[$changeset_id];
+@@ -856,8 +838,8 @@ function cps_publish_changeset_entities($changeset_id) {
+   $changed = cps_get_tracked_entities($changeset_id);
+   $transaction = db_transaction();
+   try {
+-    foreach ($changed as $entity_type => $entity_ids) {
+-      foreach ($entity_ids as $entity_id) {
++    foreach ($changed as $entity_type => $ids) {
++      foreach ($ids as $entity_id => $revision_id) {
+         cps_make_revision_published($entity_type, $entity_id, $changeset_id);
+       }
+     }
+@@ -879,8 +861,8 @@ function cps_unpublish_changeset_entities($changeset_id, $previous_changeset_id)
+   $changed = cps_get_tracked_entities($changeset_id);
+   $transaction = db_transaction();
+   try {
+-    foreach ($changed as $entity_type => $entity_ids) {
+-      foreach ($entity_ids as $entity_id) {
++    foreach ($changed as $entity_type => $ids) {
++      foreach ($ids as $entity_id => $revision_id) {
+         // If the entity existed in the previous changeset ID, use that version.
+         $vid = db_query('SELECT revision_id FROM {cps_entity} WHERE entity_type = :entity_type AND entity_id = :entity_id AND changeset_id = :changeset_id', array(
+           'entity_type' => $entity_type,
+@@ -939,7 +921,7 @@ function cps_add_untracked_entities($changeset_id, $entity_type, $start = 0, $li
+ 
+   $entity_query = new EntityFieldQuery();
+   if (!empty($tracked[$entity_type])) {
+-    $entity_query->entityCondition('entity_id', $tracked[$entity_type], 'NOT IN');
++    $entity_query->entityCondition('entity_id', array_keys($tracked[$entity_type]), 'NOT IN');
+   }
+   $results = $entity_query
+     ->entityCondition('entity_type', $entity_type)
+@@ -1036,7 +1018,7 @@ function cps_can_edit_entities($op, $entity_type, $entity, $changeset_id = NULL)
+   // If $op == 'move to' they cannot make this change if the entity already has
+   // a revision in the changeset.
+   if ($op == 'move to') {
+-    if ($entity && !empty($entity->changeset[$changeset_id])) {
++    if ($entity && cps_is_entity_in_changeset($entity_type, $entity, $changeset_id)) {
+       $access = FALSE;
+     }
+   }
+@@ -1269,18 +1251,8 @@ function cps_changeset_get_state_labels() {
+  * @return string|null
+  *   A warning string or null.
+  */
+-function cps_changeset_edit_warning($entity) {
+-  // $entity->changeset will only contain unpublished revisions.
+-  if (empty($entity->changeset)) {
+-    return;
+-  }
+-
+-  foreach (array_keys($entity->changeset) as $id) {
+-    // Ignore the current changeset.
+-    if ($id == cps_get_current_changeset()) {
+-      continue;
+-    }
+-
++function cps_changeset_edit_warning($entity_type, $entity) {
++  if (cps_entity_in_unpublished_changeset($entity_type, $entity, TRUE)) {
+     // If anything else pops, put up a warning.
+     return t('Warning, this content has unpublished changes in another site version! Whichever site version is published LAST will overwrite the other. Please see the History tab for details to help coordinate your changes.');
+   }
+@@ -1298,6 +1270,42 @@ function cps_entity_in_published_changeset($entity_type, $entity) {
+ }
+ 
+ /**
++ * Determine whether an entity is in any unpublished changeset.
++ *
++ * @param $entity_type
++ *   The entity type.
++ * @param $entity
++ *   The entity object.
++ * @param $exclude_current_changeset
++ *   Whether the query should exclude the current active changeset, useful if
++ *   checking whether an entity is in more than one pending changeset. Defaults
++ *   to FALSE.
++ *
++ *   @return
++ *     Boolean if the entity is in an unpublished changeset.
++ */
++function cps_entity_in_unpublished_changeset($entity_type, $entity, $exclude_current_changeset = FALSE) {
++  // Treat a changeset as published if the status is 'live' or 'archived'.
++  list($id) = entity_extract_ids($entity_type, $entity);
++  $changeset = cps_get_current_changeset(TRUE);
++  // Exclude the current changeset if requested, unless it's the published
++  // changeset which is never in the database anyway.
++  $query = db_select('cps_entity', 'ce');
++  $query->join('cps_changeset', 'cc', 'ce.changeset_id = cc.changeset_id');
++  $query->condition('cc.status', [CPS_ARCHIVED_STATUS, 'published'], 'NOT IN')
++    ->condition('ce.entity_type', $entity_type)
++    ->condition('ce.entity_id', $id)
++    ->range(0, 1)
++    ->addExpression(1);
++
++  if ($exclude_current_changeset && $changeset != CPS_PUBLISHED_CHANGESET) {
++    $query->condition('ce.changeset_id', $changeset, '<>');
++  }
++  return $query->execute()->fetchField();
++}
++
++
++/**
+  * Lock the CPS publishing system.
+  *
+  * @return bool
+diff --git a/modules/cps_file_entity/cps_file_entity.module b/modules/cps_file_entity/cps_file_entity.module
+index f74814f..5eaa040 100644
+--- a/modules/cps_file_entity/cps_file_entity.module
++++ b/modules/cps_file_entity/cps_file_entity.module
+@@ -59,7 +59,7 @@ function cps_file_entity_form_file_entity_edit_alter(&$form, &$form_state) {
+   $form['options']['new_revision']['#value'] = empty($file->changeset[$changeset_id]);
+ 
+   if (empty($form_state['input'])) {
+-    $warning = cps_changeset_edit_warning($file);
++    $warning = cps_changeset_edit_warning('file', $file);
+     if ($warning) {
+       drupal_set_message($warning, 'warning');
+     }
+diff --git a/modules/cps_node/cps_node.module b/modules/cps_node/cps_node.module
+index 373cdde..dfdf31c 100644
+--- a/modules/cps_node/cps_node.module
++++ b/modules/cps_node/cps_node.module
+@@ -66,7 +66,7 @@ function cps_node_form_node_form_alter(&$form, &$form_state, &$form_id) {
+   $form['actions']['submit']['#submit'][] = 'cps_node_node_form_submit';
+ 
+   if (empty($form_state['input'])) {
+-    $warning = cps_changeset_edit_warning($form_state['node']);
++    $warning = cps_changeset_edit_warning('node', $form_state['node']);
+     if ($warning) {
+       drupal_set_message($warning, 'warning');
+     }
diff --git a/includes/CPSChangeset.class.php b/includes/CPSChangeset.class.php
index e389ea9..ba81689 100644
--- a/includes/CPSChangeset.class.php
+++ b/includes/CPSChangeset.class.php
@@ -225,4 +225,90 @@ class CPSChangeset extends Entity {
     $this->status = $status;
     $this->statusMessage = $message;
   }
+
+  /**
+   * Generate a Drupal build array of changed entities for rendering.
+   *
+   * @return array
+   *   A Drupal render array.
+   */
+  function renderChangedEntities() {
+    $build = array(
+      '#attached' => array(
+        'js' => array(ctools_attach_js('collapsible-div')),
+        'css' => array(ctools_attach_css('collapsible-div')),
+      )
+    );
+
+    // Display a list of changed entities to make it easy to examine what
+    // changes this set contains.
+    $changes = $this->getChangedEntities();
+
+    if (!empty($changes)) {
+      $entity_info = entity_get_info();
+      foreach ($changes as $entity_type => $changed_entities) {
+        $build[$entity_type] = array(
+          '#type' => 'item',
+          '#title' => t('Changed %type', array('%type' => $entity_info[$entity_type]['label'])),
+          '#prefix' => '<table class="cps-changed-entities">',
+          '#suffix' => '</table>',
+        );
+
+        foreach ($changed_entities as $entity_id => $change) {
+          ctools_include('diff', 'cps');
+          $build[$entity_type][$entity_id] = cps_render_changed_entity($entity_type, $change, $this);
+        }
+      }
+    }
+    else {
+      $build['#markup'] = '<p class="cps-changeset-no-changes">' . t('This site version has no changes.') . '</p>';
+    }
+
+    return $build;
+  }
+
+  /**
+   * Render a table for the changeset's history.
+   *
+   * @return array
+   */
+  function renderHistory() {
+    $statuses = cps_changeset_get_state_labels();
+    $account = user_load($this->uid);
+    $items = array(
+      array(
+        t('Created'),
+        format_date($this->created),
+        format_username($account),
+        '',
+      )
+    );
+
+    foreach ($this->history as $item) {
+      $status = isset($statuses[$item->new_status]) ? $statuses[$item->new_status] : '';
+      // Special status for unpublishing.
+      if ($item->new_status == 'unpublished' && $item->previous_status == CPS_ARCHIVED_STATUS) {
+        $status = t('Unpublished');
+      }
+
+      drupal_alter('cps_changeset_history_status', $status, $this, $item);
+      $account = user_load($item->uid);
+      $items[] = array(
+        $status,
+        format_date($item->timestamp),
+        format_username($account),
+        $item->message,
+      );
+    }
+
+    $build = array(
+      '#title' => t('Site version history'),
+      '#type' => 'item',
+      '#theme' => 'table',
+      '#header' => array(t('Status'), t('Date'), t('Author'), t('Message')),
+      '#rows' => $items
+    );
+
+    return $build;
+  }
 }
diff --git a/includes/CPSChangesetController.class.php b/includes/CPSChangesetController.class.php
index ad73944..b117e83 100644
--- a/includes/CPSChangesetController.class.php
+++ b/includes/CPSChangesetController.class.php
@@ -293,84 +293,10 @@ class CPSChangesetController extends EntityAPIController {
       '#suffix' => '</div>',
     );
 
-    $build['changes'] = array(
-      '#type' => 'item',
-      '#title' => t('Changes in this site version')
-    );
-
-    // Display a list of changed entities to make it easy to examine what
-    // changes this set contains.
-    $changes = $entity->getChangedEntities();
-
-    $build['changes'] = array(
-      '#attached' => array(
-        'js' => array(ctools_attach_js('collapsible-div')),
-        'css' => array(ctools_attach_css('collapsible-div')),
-      )
-    );
-
-    if (empty($changes)) {
-      $build['changes']['#markup'] = t('This site version contains no changes.');
-    }
-    $entity_info = entity_get_info();
-    foreach ($changes as $entity_type => $changed_entities) {
-      // Do not display field collection items as they are, as far as the user is
-      // concerned, part of their parent entity.
-      if ($entity_type == 'field_collection_item') {
-        continue;
-      }
-
-      $build['changes'][$entity_type] = array(
-        '#type' => 'item',
-        '#title' => t('Changed %type', array('%type' => $entity_info[$entity_type]['label'])),
-        '#prefix' => '<table class="cps-changed-entities">',
-        '#suffix' => '</table>',
-      );
-
-      foreach ($changed_entities as $entity_id => $change) {
-        ctools_include('diff', 'cps');
-        $build['changes'][$entity_type][$entity_id] = cps_render_changed_entity($entity_type, $change, $entity);
-      }
-    }
-
-    if (empty($build['changes'])) {
-      $build['changes']['#markup'] = '<p class="cps-changeset-no-changes">' . t('This site version has no changes.') . '</p>';
-    }
-
-    // In the full view mode, display a history at the bottom.
+    // In the full view mode, display changes and history at the bottom.
     if ($view_mode == 'full') {
-      $statuses = cps_changeset_get_state_labels();
-      $account = user_load($entity->uid);
-      $items = array(array(
-        t('Created'),
-        format_date($entity->created),
-        format_username($account),
-        '',
-      ));
-      foreach ($entity->history as $item) {
-        $status = isset($statuses[$item->new_status]) ? $statuses[$item->new_status] : '';
-        // Special status for unpublishing.
-        if ($item->new_status == 'unpublished' && $item->previous_status == CPS_ARCHIVED_STATUS) {
-          $status = t('Unpublished');
-        }
-
-        drupal_alter('cps_changeset_history_status', $status, $entity, $item);
-        $account = user_load($item->uid);
-        $items[] = array(
-          $status,
-          format_date($item->timestamp),
-          format_username($account),
-          $item->message,
-        );
-      }
-
-      $build['history'] = array(
-        '#title' => t('Site version history'),
-        '#type' => 'item',
-        '#theme' => 'table',
-        '#header' => array(t('Status'), t('Date'), t('Author'), t('Message')),
-        '#rows' => $items
-      );
+      $build['changes'] = $entity->renderChangedEntities();
+      $build['history'] = $entity->renderHistory();
     }
 
     return $build;
diff --git a/includes/diff.inc b/includes/diff.inc
index 065f8db..b9c39d2 100644
--- a/includes/diff.inc
+++ b/includes/diff.inc
@@ -42,6 +42,10 @@ function cps_render_diff($entity_type, $old, $new, $state = 'raw', $context = ar
     '#changes' => 0,
   );
 
+  if ($theme = variable_get('diff_theme', 'default')) {
+    $build['#attached']['css'][] = ctools_attach_css("diff.{$theme}.css", 'diff');
+  }
+
   foreach ($rows as $key => $row) {
     if (strpos($key, 'diff-row-') === 0) {
       // The Diff module sometimes adds empty rows which are keyed like a
diff --git a/includes/forms.inc.orig b/includes/forms.inc.orig
new file mode 100644
index 0000000..532ab04
--- /dev/null
+++ b/includes/forms.inc.orig
@@ -0,0 +1,442 @@
+<?php
+/**
+ * @file
+ * Form definitions for admin forms.
+ */
+
+/**
+ * Form callback; changeset preview form.
+ */
+function cps_changeset_preview_form($form, &$form_state) {
+  $changeset_id = cps_get_current_changeset(TRUE);
+
+  if ($changeset_id == CPS_PUBLISHED_CHANGESET) {
+    $changeset_state = CPS_LIVE_STATUS;
+  }
+  else {
+    $changeset = cps_changeset_load($changeset_id);
+    $changeset_state = $changeset->status;
+    $changeset_mine = $changeset->uid == $GLOBALS['user']->uid;
+  }
+
+  $form['#attached']['css'][] =  drupal_get_path('module', 'cps') . '/css/cps.css';
+
+  $form['#type'] = 'fieldset';
+  $form['#title'] = t('Site version:');
+  $form['#description'] = t('Preview the site in the context of this site version.');
+
+  $form['changeset_id'] = array(
+    '#type' => 'select',
+    '#title' => t('Site version'),
+    '#options' => cps_get_changesets(),
+    '#default_value' => $changeset_id,
+  );
+
+  if (!empty($changeset) && entity_access('view', 'cps_changeset', $changeset)) {
+    $uri = $changeset->uri();
+    $form['configure'] = array(
+      '#markup' => l(t('Site version'), $uri['path'], array(
+        'attributes' => array(
+          'class' => array('configure'),
+          'title' => t('Manage site version'),
+        ))),
+    );
+  }
+
+  if ($changeset_state == CPS_LIVE_STATUS) {
+    $form['new'] = array(
+      '#markup' => l(t('Create'), 'admin/structure/changesets/add', array(
+        'query' => drupal_get_destination(),
+        'attributes' => array(
+          'class' => array('new'),
+          'title' => t('Add new site version'),
+        ))),
+    );
+  }
+
+  $state_class = 'cps-preview-form--' . drupal_html_class($changeset_state);
+  $mine_class = !empty($changeset_mine) ? 'cps-changeset--mine' : 'cps-changeset--not-mine';
+  $form['#prefix'] = '<div class="cps-preview-form ' . $state_class . ' ' . $mine_class . ' clearfix">';
+  $form['#suffix'] = '<span class="cps-preview-form-status">' . t('Status:  @changeset_status', array('@changeset_status' => ucfirst($changeset_state))) . '</span></div>';
+  return $form;
+}
+
+/**
+ * Basic edit form for the content entity.
+ *
+ * The entity being edited should be stored in $form_state['entity']
+ * when this form is built.
+ *
+ * The name of this form is autogenerated as $entity_type . '_edit_form'.
+ */
+function cps_changeset_edit_form($form, &$form_state) {
+  $entity = $form_state['entity'];
+
+  $form['info']['name'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Name'),
+    '#description' => t('This will appear in the administrative interface to easily identify it.'),
+    '#default_value' => $entity->name,
+    '#required' => TRUE,
+  );
+
+  $form['info']['description'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Description'),
+    '#default_value' => $entity->description,
+  );
+  
+  $form['info']['lock_in_select'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Lock in select'),
+    '#description' => t('If checked all users will see this site version in the site version selector bar while unpublished.'),
+    '#default_value' => $entity->lock_in_select,
+  );
+
+  $form['actions'] = array(
+    '#type' => 'actions',
+  );
+
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * Validate callback for the content entity.
+ */
+function cps_changeset_edit_form_validate($form, &$form_state) {
+}
+
+/**
+ * Submit callback for the content entity.
+ */
+function cps_changeset_edit_form_submit($form, &$form_state) {
+  $entity = $form_state['entity'];
+  $entity->name = $form_state['values']['name'];
+  $entity->description = $form_state['values']['description'];
+  $entity->lock_in_select = $form_state['values']['lock_in_select'];
+
+  if (!empty($entity->is_new)) {
+    // Automatically move us to the new changeset context.
+    cps_set_current_changeset($entity->changeset_id);
+
+    // If there is a destination, we have to remove the changeset ID from it.
+    if (!empty($_GET['destination'])) {
+      // We used drupal_get_destination() before because it may not have been
+      // calculated; to directly modify it we will then need to get the
+      // (now cached) static.
+      $_GET['destination'] = preg_replace("/changeset_id=[^&]+/", 'changeset_id=' . $entity->changeset_id, $_GET['destination']);
+    }
+  }
+}
+
+/**
+ * Form callback to display the publish changeset form.
+ */
+function cps_changeset_publish_changeset_form($form, &$form_state) {
+  $form['warning'] = array(
+    '#markup' => '<div class="publish-warning">' . t('Are you sure you want to publish this? This action cannot be undone!') . '</div>',
+  );
+
+  $form['actions']['publish'] = array(
+    '#type' => 'submit',
+    '#value' => t('Publish'),
+  );
+
+  $form['actions']['cancel'] = array(
+    '#type' => 'submit',
+    '#value' => t('Cancel'),
+    '#limit_validation_errors' => array(),
+    '#submit' => array('cps_changeset_publish_changeset_form_cancel'),
+  );
+
+  return $form;
+}
+
+/**
+ * Submit handler to cancel the form.
+ */
+function cps_changeset_publish_changeset_form_cancel($form, &$form_state) {
+  $form_state['redirect'] = $form_state['entity']->uri();
+}
+
+/**
+ * Submit handler for the publish changeset form.
+ */
+function cps_changeset_publish_changeset_form_submit($form, &$form_state) {
+  // This is handled as a traditional submit method rather than my usual
+  // preference of divorcing work from the form itself due to the nature
+  // of batch API.
+  $batch = array(
+    'operations' => array(
+      array('cps_changeset_publish_batch_lock', array()),
+      array('cps_changeset_publish_batch_entities', array($form_state['entity'])),
+    ),
+    'finished' => 'cps_changeset_publish_batch_finished',
+    'title' => t('Publishing %changeset', array('%changeset' => $form_state['entity']->name)),
+    'file' => drupal_get_path('module', 'cps') . '/includes/forms.inc',
+  );
+
+  if (variable_get('cps_override_variables', FALSE)) {
+    $batch['operations'][] = array('cps_changeset_publish_batch_variables', array());
+  }
+
+  foreach (cps_get_supported() as $entity_type) {
+    // For every entity type we support, add another operation for that entity type.
+    $batch['operations'][] = array('cps_changeset_publish_batch_update', array($form_state['entity'], $entity_type));
+  }
+
+  drupal_alter('cps_publish_changeset_batch', $batch, $form_state);
+
+  batch_set($batch);
+
+  $form_state['redirect'] = $form_state['entity']->uri();
+}
+
+
+/**
+ * Batch API callback to acquire a lock for publishing.
+ *
+ * @param $entity_ids
+ *   An array of arrays. Each entry in the array is first entity_type, then entity_id.
+ * @param $changeset
+ * @param $context
+ */
+function cps_changeset_publish_batch_lock(&$context) {
+  if (cps_publish_lock()) {
+    $context['finished'] = TRUE;
+  }
+  else {
+    sleep(1);
+    $context['finished'] = FALSE;
+    $context['message'] = t('Waiting for lock: @time', array('@time' => format_interval(cps_publish_lock_time())));
+  }
+}
+
+/**
+ * Batch API callback to publish the entities in a changeset.
+ *
+ * @param $changeset
+ * @param $context
+ */
+function cps_changeset_publish_batch_entities($changeset, &$context) {
+  cps_publish_changeset_entities($changeset->changeset_id);
+  $context['results']['changeset'] = $changeset;
+  $context['message'] = t('Publishing entities');
+  $context['finished'] = TRUE;
+  return $context;
+}
+
+/**
+ * Batch API callback to publish the variables in a changeset.
+ */
+function cps_changeset_publish_batch_variables(&$context) {
+  $item = $context['results']['entity'];
+  foreach ($item->variables as $name => $value) {
+    db_merge('variable')->key(array('name' => $name))->fields(array('value' => serialize($value)))->execute();
+  }
+  cache_clear_all('variables', 'cache_bootstrap');
+  $context['message'] = t('Publishing variables');
+}
+
+/**
+ * Batch API callback to put in changeset markers for items that exist but didn't change.
+ * @param $entity_type
+ * @param $context
+ */
+function cps_changeset_publish_batch_update($changeset, $entity_type, &$context) {
+  $changeset_id = $changeset->changeset_id;
+
+  // Use the $context['sandbox'] at your convenience to store the
+  // information needed to track progression between successive calls.
+  if (empty($context['sandbox'])) {
+    $entity_info = entity_get_info($entity_type);
+    $context['sandbox'] = array();
+    $context['sandbox']['progress'] = 0;
+    $context['sandbox']['current_item'] = 0;
+    $context['message'] = t('Marking unchanged @entity_type content', array('@entity_type' => $entity_info['label']));
+  }
+
+  $limit = 100;
+  $count = cps_add_untracked_entities($changeset_id, $entity_type, $context['sandbox']['current_item'], $limit);
+
+  $context['results']['changeset'] = $changeset;
+
+  if (!$count) {
+    $context['finished'] = TRUE;
+  }
+  else {
+    $context['finished'] = FALSE;
+    $context['sandbox']['current_item'] = $context['sandbox']['current_item'] + $count;
+  }
+}
+
+/**
+ * Batch API callback to finish the submission.
+ *
+ * @param $success
+ * @param $results
+ * @param $operations
+ */
+function cps_changeset_publish_batch_finished($success, $results, $operations) {
+  if ($success) {
+    $changeset = $results['changeset'];
+    // Set the publication time on the item. We have to GET the item somehow.
+    $changeset->published = REQUEST_TIME;
+    $changeset->setStatus(CPS_ARCHIVED_STATUS);
+    if (variable_get('cps_override_variables', FALSE)) {
+
+      // Rebuild the variables in the same manner that the cache is built from variable_initialize.
+      $changeset->variables = array_map('unserialize', db_query('SELECT name, value FROM {variable}')->fetchAllKeyed());
+      // Go through and unset any whitelisted variables so that they aren't stored in the archive.
+      foreach ($changeset->variables as $name => $value) {
+        if (strpos($name, 'cache') !== FALSE) {
+          unset($results['entity']->variables[$name]);
+        }
+      }
+    }
+
+    $changeset->save();
+
+    module_invoke_all('cps_changeset_published', $changeset, 'published');
+
+    // Switch them to the published changeset.
+    cps_set_current_changeset(NULL);
+
+    drupal_set_message(t('The site version %changeset has been published.', array('%changeset' => $changeset->name)));
+  }
+
+  cps_publish_unlock();
+}
+
+// -----------------------------------------------------------------------
+// Unpublish form and handlers.
+
+/**
+ * Form callback to display the unpublish changeset form.
+ */
+function cps_changeset_unpublish_changeset_form($form, &$form_state) {
+  $entity = $form_state['entity'];
+  $form_state['previous'] = cps_changeset_load($entity->getPreviousChangeset());
+
+  $form['warning'] = array(
+    '#markup' => '<div class="unpublish-warning">' . t('<p>Are you sure you want to unpublish this?</p><p> This will revert content to the previous site version "%changeset".</p>', array('%changeset' => $form_state['previous']->name)) . '</div>',
+  );
+
+  $form['actions']['unpublish'] = array(
+    '#type' => 'submit',
+    '#value' => t('Revert'),
+  );
+
+  $form['actions']['cancel'] = array(
+    '#type' => 'submit',
+    '#value' => t('Cancel'),
+    '#limit_validation_errors' => array(),
+    // We re-use the publish_changeset_form_cancel here because all it does is redirect.
+    '#submit' => array('cps_changeset_publish_changeset_form_cancel'),
+  );
+
+  return $form;
+}
+
+/**
+ * Submit handler for the unpublish changeset form.
+ */
+function cps_changeset_unpublish_changeset_form_submit($form, &$form_state) {
+  // We have to tell batch where to include this file.
+  $batch = array(
+    'operations' => array(
+      array('cps_changeset_publish_batch_lock', array()),
+      array('cps_changeset_unpublish_batch_variables', array($form_state['entity'], $form_state['previous'])),
+      array('cps_changeset_unpublish_batch_entity', array()),
+    ),
+    'finished' => 'cps_changeset_unpublish_batch_finished',
+    'title' => t('Reverting %changeset', array('%changeset' => $form_state['entity']->name)),
+    'file' => drupal_get_path('module', 'cps') . '/includes/forms.inc',
+  );
+
+  drupal_alter('cps_unpublish_changeset_batch', $batch, $form_state);
+
+  batch_set($batch);
+
+  $form_state['redirect'] = $form_state['entity']->uri();
+}
+
+/**
+ * Batch API callback to unpublish the entities in a changeset.
+ */
+function cps_changeset_unpublish_batch_variables($item, $previous, &$context) {
+  // Use the $context['sandbox'] at your convenience to store the
+  // information needed to track progression between successive calls.
+  if (empty($context['sandbox'])) {
+    $context['sandbox'] = array();
+    $context['sandbox']['progress'] = 0;
+    $context['sandbox']['current_item'] = 0;
+    $context['results']['entity'] = $item;
+    $context['results']['previous'] = $previous;
+  }
+
+  if (variable_get('cps_override_variables', FALSE)) {
+
+    $result = db_query("SELECT name, value FROM {variable}");
+    $all_variables = array();
+    while ($var = $result->fetchObject()) {
+      $all_variables[$var->name] = unserialize($var->value);
+    }
+
+    foreach ($all_variables as $name => $value) {
+      if (isset($previous->variables[$name]) && $previous->variables[$name] != $value) {
+        db_merge('variable')->key(array('name' => $name))->fields(array('value' => serialize($previous->variables[$name])))->execute();
+      }
+      else {
+        // If a variable is unchanged, unset it so we won't mistakenly report it as a change in the
+        // reverted changeset.
+        if (isset($item->variables[$name])) {
+          unset($item->variables[$name]);
+        }
+      }
+    }
+    cache_clear_all('variables', 'cache_bootstrap');
+  }
+
+  $context['message'] = t('Reverting variables');
+}
+
+/**
+ * Batch API callback to put in changeset markers for items that exist but didn't change.
+ * @param $entity_type
+ * @param $context
+ */
+function cps_changeset_unpublish_batch_entity(&$context) {
+  $changeset_id = $context['results']['entity']->changeset_id;
+  $previous = $context['results']['previous']->changeset_id;
+  cps_unpublish_changeset_entities($changeset_id, $previous);
+  cps_remove_untracked_entities($changeset_id);
+}
+
+/**
+ * Batch API callback to finish the submission.
+ *
+ * @param $success
+ * @param $results
+ * @param $operations
+ */
+function cps_changeset_unpublish_batch_finished($success, $results, $operations) {
+  if ($success) {
+    // Set the publication time on the item. We have to GET the item somehow.
+    $results['entity']->published = NULL;
+    $results['entity']->setStatus('unpublished');
+    $results['entity']->save();
+
+    drupal_set_message(t('The site version %changeset has been unpublished.', array('%changeset' => $results['entity']->name)));
+  }
+
+  module_invoke_all('cps_changeset_published', $results['entity'], 'unpublished');
+
+  cps_publish_unlock();
+}
+
diff --git a/includes/forms.inc.rej b/includes/forms.inc.rej
new file mode 100644
index 0000000..ddec6e2
--- /dev/null
+++ b/includes/forms.inc.rej
@@ -0,0 +1,24 @@
+--- includes/forms.inc
++++ includes/forms.inc
+@@ -85,6 +85,13 @@
+     '#title' => t('Description'),
+     '#default_value' => $entity->description,
+   );
++  
++  $form['info']['lock_in_select'] = array(
++    '#type' => 'checkbox',
++    '#title' => t('Lock in select'),
++    '#description' => t('If checked all users will see this site version in the site version selector bar while unpublished.'),
++    '#default_value' => $entity->lock_in_select,
++  );
+ 
+   $form['actions'] = array(
+     '#type' => 'actions',
+@@ -111,6 +118,7 @@
+   $entity = $form_state['entity'];
+   $entity->name = $form_state['values']['name'];
+   $entity->description = $form_state['values']['description'];
++  $entity->lock_in_select = $form_state['values']['lock_in_select'];
+ 
+   if (!empty($entity->is_new)) {
+     // Automatically move us to the new changeset context.
diff --git a/lock-in-select.patch b/lock-in-select.patch
new file mode 100644
index 0000000..170df26
--- /dev/null
+++ b/lock-in-select.patch
@@ -0,0 +1,125 @@
+diff --git a/docroot/sites/all/modules/contrib/cps/cps.install b/docroot/sites/all/modules/contrib/cps/cps.install
+index 93170aa..e8a3cc6 100644
+--- a/docroot/sites/all/modules/contrib/cps/cps.install
++++ b/docroot/sites/all/modules/contrib/cps/cps.install
+@@ -110,6 +110,13 @@ function cps_schema() {
+         'not null' => TRUE,
+         'default' => 0,
+       ),
++      'lock_in_select' => array(
++        'description' => 'True if this changeset should be locked into all users select bars when open.',
++        'type' => 'int',
++        'unsigned' => TRUE,
++        'not null' => TRUE,
++        'default' => 0,
++      ),
+     ),
+     'primary key' => array('changeset_id'),
+   );
+@@ -574,3 +581,18 @@ function cps_update_7107() {
+     throw $e;
+   }
+ }
++
++/**
++ * Add lock_in_select field.
++ */
++function cps_update_7108() {
++  $field = array(
++    'description' => 'True if this changeset should be locked into all users select bars when open.',
++    'type' => 'int',
++    'unsigned' => TRUE,
++    'not null' => TRUE,
++    'default' => 0,
++  );
++
++  db_add_field('cps_changeset', 'lock_in_select', $field);
++}
+diff --git a/docroot/sites/all/modules/contrib/cps/cps.module b/docroot/sites/all/modules/contrib/cps/cps.module
+index a562ebc..e15a2cd 100644
+--- a/docroot/sites/all/modules/contrib/cps/cps.module
++++ b/docroot/sites/all/modules/contrib/cps/cps.module
+@@ -1113,7 +1113,8 @@ function cps_get_changesets() {
+     $query = new EntityFieldQuery();
+     $results = $query->entityCondition('entity_type', 'cps_changeset')
+       ->propertyCondition('status', CPS_ARCHIVED_STATUS, '<>')
+-      ->propertyCondition('uid', $user->uid)
++      // Note that uid and locked check are in a db_or() in a query alter
++      // because EFQ does not support OR.
+       ->propertyOrderBy('name')
+       // This tag allows another module to modify this list if necessary.
+       ->addTag('cps_active_changeset_list')
+@@ -1128,6 +1129,13 @@ function cps_get_changesets() {
+     $entities = cps_changeset_load_multiple(array_keys($results['cps_changeset']));
+     foreach ($entities as $id => $entity) {
+       $changesets[$id] = $entity->name;
++      if ($entity->lock_in_select) {
++        $locked[$id] = $entity->name;
++      }
++      // Assume if it's not locked it gets listed under 'mine'.
++      else {
++        $mine[$id] = $entity->name;
++      }
+     }
+   }
+ 
+@@ -1173,8 +1181,12 @@ function cps_get_changesets() {
+   $retval = $published;
+ 
+   // Now my open changesets.
+-  if ($changesets) {
+-    $retval += array(t('My open site versions') => $changesets);
++  if (!empty($locked)) {
++    $retval += array(t('Locked open site versions') => $locked);
++  }
++
++  if (!empty($mine)) {
++    $retval += array(t('My open site versions') => $mine);
+   }
+ 
+   // My published if I've looked at them recently.
+@@ -1192,6 +1204,18 @@ function cps_get_changesets() {
+ }
+ 
+ /**
++ * Implements hook_query_TAG_alter().
++ */
++function cps_query_cps_active_changeset_list_alter($query) {
++  // Because EFQ doesn't support OR, this hack adds a db_or() to the underlying
++  // query.
++  $query->condition(db_or()
++    ->condition('uid', $GLOBALS['user']->uid)
++    ->condition('lock_in_select', TRUE)
++  );
++}
++
++/**
+  * Fetch a list of possible state types for changesets.
+  *
+  * @return string[]
+diff --git a/docroot/sites/all/modules/contrib/cps/includes/forms.inc b/docroot/sites/all/modules/contrib/cps/includes/forms.inc
+index 076c889..532ab04 100644
+--- a/docroot/sites/all/modules/contrib/cps/includes/forms.inc
++++ b/docroot/sites/all/modules/contrib/cps/includes/forms.inc
+@@ -85,6 +85,13 @@ function cps_changeset_edit_form($form, &$form_state) {
+     '#title' => t('Description'),
+     '#default_value' => $entity->description,
+   );
++  
++  $form['info']['lock_in_select'] = array(
++    '#type' => 'checkbox',
++    '#title' => t('Lock in select'),
++    '#description' => t('If checked all users will see this site version in the site version selector bar while unpublished.'),
++    '#default_value' => $entity->lock_in_select,
++  );
+ 
+   $form['actions'] = array(
+     '#type' => 'actions',
+@@ -111,6 +118,7 @@ function cps_changeset_edit_form_submit($form, &$form_state) {
+   $entity = $form_state['entity'];
+   $entity->name = $form_state['values']['name'];
+   $entity->description = $form_state['values']['description'];
++  $entity->lock_in_select = $form_state['values']['lock_in_select'];
+ 
+   if (!empty($entity->is_new)) {
+     // Automatically move us to the new changeset context.
diff --git a/modules/cps_review/cps_review.info b/modules/cps_review/cps_review.info
new file mode 100644
index 0000000..e6a7bfc
--- /dev/null
+++ b/modules/cps_review/cps_review.info
@@ -0,0 +1,10 @@
+name = Content preview system - Review and Commenting
+description = Allows editors to leave reviews on documents managed by CPS.
+core = 7.x
+
+dependencies[] = cps
+dependencies[] = entity
+dependencies[] = entity_status
+
+files[] = includes/CPSReview.class.php
+files[] = includes/CPSReviewController.class.php
diff --git a/modules/cps_review/cps_review.install b/modules/cps_review/cps_review.install
new file mode 100644
index 0000000..e13a47d
--- /dev/null
+++ b/modules/cps_review/cps_review.install
@@ -0,0 +1,138 @@
+<?php
+
+/**
+ * @file
+ * Installation specific hooks for CPS review module.
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function cps_review_schema() {
+  $schema['cps_review'] = array(
+    'description' => 'Stores metadata about available CPS reviews.',
+    'fields' => array(
+      'review_id' => array(
+        'description' => 'The primary identifier for the entity.',
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'parent_review_id' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'description' => 'The parent of this CPS review for threading, if 0 there is no parent.',
+      ),
+      'container_entity_type' => array(
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'container_entity_id' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'changeset_id' => array(
+        'description' => 'A machine name identifying the changeset this review is part of.',
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'uid' => array(
+        'description' => 'The owner of the CPS review.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'title' => array(
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'The CPS review title.',
+      ),
+      'status' => array(
+        'description' => 'Boolean indicating whether the CPS review is published (visible to non-administrators).',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 1,
+      ),
+      'created' => array(
+        'description' => 'The Unix timestamp when the entity was created.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'changed' => array(
+        'description' => 'The Unix timestamp when the entity was most recently saved.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('review_id'),
+  );
+
+  return $schema;
+}
+
+/**
+ * Implements hook_install().
+ */
+function cps_review_install() {
+  $fields[] = array(
+    'field_name' => 'cps_review_body',
+    'type' => 'text_long',
+    'module' => 'text',
+    'locked' => TRUE,
+    'cardinality' => 1,
+    'translatable' => FALSE,
+  );
+
+  // Create fields.
+  foreach ($fields as $field) {
+    if (!field_read_field($field['field_name'])) {
+      field_create_field($field);
+    }
+  }
+
+  $instances[] = array (
+    'entity_type' => 'cps_review',
+    'bundle' => 'cps_review',
+    'field_name' => 'cps_review_body',
+    'label' => st('Comment'),
+    'settings' => array(
+      'text_processing' => 0,
+    ),
+    'widget' => array(
+      'type' => 'text_textarea',
+    ),
+    'display' => array(
+      'default' => array(
+        'label' => 'hidden',
+        'type' => 'text_default',
+      ),
+    ),
+  );
+
+  // Create all instances
+  foreach ($instances as $instance) {
+    if (!field_read_instance($instance['entity_type'], $instance['field_name'], $instance['bundle'])) {
+      field_create_instance($instance);
+    }
+  }
+}
+
+/**
+ * Set comment text fields to plain text.
+ */
+function cps_review_update_7000() {
+  if ($instance = field_read_instance('cps_review', 'cps_review_body', 'cps_review')) {
+    $instance['settings']['text_processing'] = 0;
+    field_update_instance($instance);
+  }
+}
diff --git a/modules/cps_review/cps_review.module b/modules/cps_review/cps_review.module
new file mode 100644
index 0000000..b8fe5e5
--- /dev/null
+++ b/modules/cps_review/cps_review.module
@@ -0,0 +1,600 @@
+<?php
+
+/**
+ * @file
+ * Main module file for the Content Preview System - Review Entity.
+ */
+
+/**
+ * Implements of hook_permission()
+ */
+function cps_review_permission() {
+  return array(
+    'administer cps reviews' => array(
+      'title' => t('Administer CPS review entities'),
+      'description' => t('Add, delete, edit and manipulate review entities.'),
+    ),
+    'edit all cps reviews' => array(
+      'title' => t('Edit all CPS review entities'),
+      'description' => t('Can edit CPS reviews owned by other people.'),
+    ),
+    'view unpublished cps reviews' => array(
+      'title' => t('View CPS review entities'),
+      'description' => t('View unpublished CPS review entities on the admin page.'),
+    ),
+    'publish cps reviews' => array(
+      'title' => t('Publish CPS review entities'),
+      'description' => t('Can publish CPS review entities.'),
+    ),
+  );
+}
+
+/**
+ * Implements hook_entity_info().
+ */
+function cps_review_entity_info() {
+  $info = array();
+
+  $info['cps_review'] = array(
+    'label' => t('CPS Review'),
+    'entity class' => 'CPSReview',
+    'controller class' => 'CPSReviewController',
+    'views controller class' => 'EntityDefaultViewsController',
+    'base table' => 'cps_review',
+    'fieldable' => TRUE,
+    'entity keys' => array(
+      'id' => 'review_id',
+      'label' => 'name',
+    ),
+    'bundles' => array(
+      'cps_review' => array(
+        'label' => t('CPS Review'),
+      ),
+    ),
+    'view modes' => array(
+      'full' => array(
+        'label' => t('Full'),
+        'custom settings' => FALSE,
+      ),
+      'embed' => array(
+        'label' => t('Embedded'),
+        'custom settings' => FALSE,
+      ),
+      'mail' => array(
+        'label' => t('Mailed'),
+        'custom settings' => FALSE,
+      ),
+    ),
+    // entity module callbacks
+    'label callback' => 'entity_class_label',
+    'uri callback' => 'entity_class_uri',
+    'view callback' => 'cps_review_view',
+    'creation callback' => 'cps_review_create',
+    'access callback' => 'cps_review_access',
+    'save callback' => 'cps_review_save',
+    'module' => 'cps_review',
+  );
+
+  return $info;
+}
+
+/**
+ * Implements hook_menu().
+ */
+function cps_review_menu() {
+  $base = 'cps-review';
+
+  $items = array();
+  $items[$base . '/%cps_review'] = array(
+    'title callback' => 'entity_label',
+    'title arguments' => array('cps_review', 1),
+    'page callback' => 'cps_review_view_page',
+    'page arguments' => array(1, 'full'),
+    'access callback' => 'entity_access',
+    'access arguments' => array('view', 'cps_review', 1),
+    'file' => 'includes/admin.inc',
+  );
+
+  $items[$base . '/%cps_review/view'] = array(
+    'title' => 'View',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+    'file' => 'includes/admin.inc',
+  );
+
+  $items[$base . '/%cps_review/edit'] = array(
+    'title' => 'Edit',
+    'type' => MENU_LOCAL_TASK,
+    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'page callback' => 'cps_review_edit_page',
+    'page arguments' => array(1),
+    'access callback' => 'entity_access',
+    'access arguments' => array('update', 'cps_review', 1),
+    'weight' => 9,
+    'file' => 'includes/admin.inc',
+  );
+
+  $items[$base . '/%cps_review/reply'] = array(
+    'title' => 'Reply',
+    'type' => MENU_LOCAL_TASK,
+    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'page callback' => 'cps_review_reply_entity_page',
+    'page arguments' => array(1),
+    'access callback' => 'entity_access',
+    'access arguments' => array('view', 'cps_review', 1),
+    'weight' => 9,
+    'file' => 'includes/admin.inc',
+  );
+
+  $items[$base . '/%cps_review/publish'] = array(
+    'title' => 'Publish',
+    'type' => MENU_LOCAL_TASK,
+    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'page callback' => 'cps_review_publish_page',
+    'page arguments' => array(1),
+    'access callback' => 'entity_access',
+    'access arguments' => array('publish', 'cps_review', 1),
+    'file' => 'includes/admin.inc',
+    'weight' => 10,
+  );
+
+  $items[$base . '/%cps_review/unpublish'] = array(
+    'title' => 'Unpublish',
+    'type' => MENU_LOCAL_TASK,
+    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'page callback' => 'cps_review_unpublish_page',
+    'page arguments' => array(1),
+    'access callback' => 'entity_access',
+    'access arguments' => array('unpublish', 'cps_review', 1),
+    'file' => 'includes/admin.inc',
+    'weight' => 11,
+  );
+
+  $items[$base . '/%cps_review/delete'] = array(
+    'title' => 'Delete',
+    'type' => MENU_LOCAL_TASK,
+    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('cps_review_delete_form', 1),
+    'access callback' => 'entity_access',
+    'access arguments' => array('delete', 'cps_review', 1),
+    'file' => 'includes/admin.inc',
+    'weight' => 12,
+  );
+
+  if (module_exists('devel')) {
+    $items[$base . '/%cps_review/devel'] = array(
+      'title' => 'Devel',
+      'page callback' => 'devel_load_object',
+      'page arguments' => array('cps_review', 1),
+      'access arguments' => array('access devel information'),
+      'type' => MENU_LOCAL_TASK,
+      'weight' => 100,
+      'file' => 'devel.pages.inc',
+      'file path' => drupal_get_path('module', 'devel'),
+    );
+
+    $items[$base . '/%cps_review/devel/load'] = array(
+      'title' => 'Load',
+      'type' => MENU_DEFAULT_LOCAL_TASK,
+    );
+
+    $items[$base . '/%cps_review/devel/render'] = array(
+      'title' => 'Render',
+      'page callback' => 'devel_render_object',
+      // Normally this would be the name of the entity type, but slightly
+      // modified in order to call the right function.
+      'page arguments' => array('cps_review', 1),
+      'access arguments' => array('access devel information'),
+      'file' => 'devel.pages.inc',
+      'file path' => drupal_get_path('module', 'devel'),
+      'type' => MENU_LOCAL_TASK,
+      'weight' => 100,
+    );
+  }
+
+  $items['admin/structure/cps-reviews'] = array(
+    'title' => 'CPS reviews',
+    'description' => 'Manage CPS reviews.',
+    'page callback' => 'cps_review_list_page',
+    'page arguments' => array('administer_cps_reviews'),
+    'file' => 'includes/admin.inc',
+    'access arguments' => array('administer cps reviews'),
+    'weight' => -10,
+  );
+
+  return $items;
+}
+
+/**
+ * Implements hook_field_extra_fields().
+ */
+function cps_review_field_extra_fields() {
+  $info = array();
+
+  // Add the CPS review field to all supported CPS entities.
+
+  foreach (cps_get_supported() as $entity_type) {
+    // Add the extra field to all bundles of the entity.
+    $entity_info = entity_get_info($entity_type);
+    foreach ($entity_info['bundles'] as $bundle => $bundle_info) {
+      $info[$entity_type][$bundle]['display']['cps_review_widget'] = array(
+        'label' => nt('CPS Review: Widget'),
+        'description' => nt('Provides the CPS review widget'),
+        'weight' => 100,
+        // Note: For this to take effect, you will need a core patch:
+        //       https://www.drupal.org/node/1256368
+        // Use hook_field_extra_fields_display_alter() to prevent having to
+        // update all view modes with this extra key.
+        'visible' => FALSE,
+      );
+    }
+  }
+
+  return $info;
+}
+
+/**
+ * Implements hook_entity_view().
+ */
+function cps_review_entity_view($entity, $entity_type, $view_mode, $langcode) {
+  if (!cps_is_supported($entity_type)) {
+    return;
+  }
+  list(,,$bundle) = entity_extract_ids($entity_type, $entity);
+  $extra_fields = field_extra_fields_get_display($entity_type, $bundle, $view_mode);
+
+  // Get the extra fields visibility.
+  if (!empty($extra_fields['cps_review_widget']['visible'])) {
+    module_load_include('inc', 'cps_review', 'includes/admin');
+    $entity->content['cps_review_widget'] = cps_review_widget($entity_type, $entity);
+  }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function cps_review_theme() {
+  $theme = array();
+  $theme['cps_review'] = array(
+    'render element' => 'elements',
+    'base hook' => 'entity',
+    'template' => 'cps-review',
+    'path' => drupal_get_path('module', 'cps_review') . '/templates',
+   );
+
+  return $theme;
+}
+
+// -----------------------------------------------------------------------
+// Contrib Hooks
+
+/**
+ * Implements hook_ctools_plugin_directory().
+ */
+function cps_review_ctools_plugin_directory($module, $plugin) {
+  if ($module == 'cps_review') {
+    return 'plugins/' . $plugin;
+  }
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function cps_review_views_api() {
+  return array(
+    'path' => drupal_get_path('module', 'cps_review') . '/plugins/views',
+    'api' => 3,
+  );
+}
+
+/**
+ * Implements hook_entity_property_info().
+ */
+function cps_review_entity_property_info() {
+  return entity_get_controller('cps_review')->hook_entity_property_info();
+}
+
+// -----------------------------------------------------------------------
+// Theme hooks
+
+/**
+ * Implements hook_preprocess_entity().
+ */
+function cps_review_preprocess_entity(&$variables) {
+  if (isset($variables['cps_review'])) {
+    $entity = $variables['cps_review'];
+    $variables['attributes_array']['id'] = $entity->htmlId();
+    if (variable_get('cps_review_use_title', 0) != 1) {
+      // Hide the title.
+      $variables['title'] = '';
+    }
+  }
+}
+
+// -----------------------------------------------------------------------
+// Public API
+
+// @todo
+
+// -------------------------------------------------------------------------
+// Database and general entity API functions
+
+/**
+ * CPS review entity loader.
+ *
+ * @param string $review_id
+ *   The CPS review id.
+ *
+ * @return CPSReview
+ *   The CPS review, if found.
+ *
+ * @see entity_load()
+ */
+function cps_review_load($review_id) {
+  $entities = cps_review_load_multiple(array($review_id));
+  if ($entities) {
+    return reset($entities);
+  }
+}
+
+/**
+ * Load multiple CPS reviews.
+ *
+ * @param string[] $ids
+ *   An array of IDs to load.
+ * @param array $conditions
+ *   (optional) An array of conditions.
+ * @param bool $reset
+ *   (optional) TRUE to force a load from database.
+ *
+ * @see entity_load_multiple()
+ */
+function cps_review_load_multiple($ids, $conditions = array(), $reset = FALSE) {
+  return entity_load('cps_review', $ids, $conditions, $reset);
+}
+
+/**
+ * Save a CPS review entity.
+ *
+ * @param CPSReview $entity
+ *   The entity to save.
+ *
+ * @see node_save()
+ */
+function cps_review_save(CPSReview $entity) {
+  return entity_get_controller('cps_review')->save($entity);
+}
+
+/**
+ * Delete a CPS review entity.
+ *
+ * @param $review_id
+ *   A CPS review ID.
+ */
+function cps_review_delete($review_id) {
+  cps_review_delete_multiple(array($review_id));
+}
+
+/**
+ * Delete a revision for a CPS review entity.
+ *
+ * @param $vid
+ *   The revision id to delete.
+ */
+function cps_review_delete_revision($vid) {
+  return entity_get_controller('cps_review')->deleteRevision($vid);
+}
+
+/**
+ * Delete multiple CPS reviews.
+ *
+ * @param string[] $review_ids
+ *   An array of review IDs.
+ */
+function cps_review_delete_multiple(array $review_ids) {
+  return entity_get_controller('cps_review')->delete($review_ids);
+}
+
+/**
+ * View a CPS review entity.
+ *
+ * @param CPSReview[] $entity
+ *   The CPS review entities.
+ * @param string $view_mode
+ *   The view mode to view.
+ * @param string $langcode
+ *   The language to use or NULL.
+ *
+ * @return array
+ *   A Drupal renderable array.
+ *
+ * @see node_view()
+ */
+function cps_review_view($entities, $view_mode = 'full', $langcode = NULL) {
+  return entity_get_controller('cps_review')->view($entities, $view_mode, $langcode);
+}
+
+/**
+ * Callback to create a new entity.
+ *
+ * @param array $values
+ *   The initial values to seed the new entity with.
+ *
+ * @return CPSReview
+ */
+function cps_review_create($values = array()) {
+  return entity_get_controller('cps_review')->create($values);
+}
+
+/**
+ * Determine if the operation for the entity can be accessed.
+ *
+ * @param string $op
+ *   The operation being performed. One of 'view', 'update', 'create' or
+ *   'delete'. It may also be any CPS review operation such as 'publish' or
+ *   'unpublish'.
+ * @param string|CPSReview $entity
+ *   (optional) an entity to check access for. If no entity is given, it will be
+ *   determined whether access is allowed for all entities of the given type.
+ * @param stdClass|null $account
+ *   The user to check for. Leave it to NULL to check for the global user.
+ *
+ * @return boolean
+ *   Whether access is allowed or not. If the entity type does not specify any
+ *   access information, NULL is returned.
+ *
+ * @see entity_access()
+ */
+function cps_review_access($op, $entity = NULL, $account = NULL) {
+  return entity_get_controller('cps_review')->access($op, $entity, $account);
+}
+
+/**
+ * Callback for getting content properties.
+ *
+ * @param CPSReview $review
+ *   The CPS review entity to get properties for.
+ * @param array $options
+ *   The options provided by the caller.
+ * @param $name
+ *   The name of the property.
+ *
+ * @return mixed
+ *   The result of the property.
+ */
+function cps_review_metadata_get_properties(CPSReview $review, array $options, $name) {
+  switch ($name) {
+    case 'is_new':
+      return empty($review->review_id) || !empty($review->is_new);
+
+    case 'edit_url':
+      $options['query'] += drupal_get_destination();
+      return url('cps-review/' . $review->review_id. '/edit', $options);
+
+    case 'delete_url':
+      $options['query'] += drupal_get_destination();
+      return url('cps-review/' . $review->review_id . '/delete', $options);
+
+    case 'author':
+      return !empty($review->uid) ? $review->uid : drupal_anonymous_user();
+
+    case 'operations':
+      $build = cps_review_get_operations($review);
+      return drupal_render($build);
+  }
+}
+
+/**
+ * Fetch a renderable array of operations available for the CPS review.
+ *
+ * @param CPSReview $review
+ *   The review to get operations for.
+ *
+ * @return array
+ *   A drupal Renderable array.
+ */
+function cps_review_get_operations(CPSReview $review) {
+  $uri = $review->uri();
+  $operations = array(
+    '#type' => 'container',
+    '#attributes' => array('class' => 'cps-review-operations'),
+    'site' => array(
+      '#theme' => 'link',
+      '#text' => t('view site'),
+      '#path' => '<front>',
+      '#options' => array(
+        'query' => array('review_id' => $review->review_id),
+        'html' => TRUE,
+        'attributes' => array('class' => array('cps-review-operation'))
+      ),
+      '#access' => user_access('view CPS reviews') || user_access('administer CPS reviews'),
+    ),
+    'view' => array(
+      '#theme' => 'link',
+      '#text' => t('view'),
+      '#path' => $uri['path'],
+      '#options' => array(
+        'html' => TRUE,
+        'attributes' => array('class' => array('cps-review-operation'))
+      ),
+      '#access' => entity_access('view', 'cps_review', $review) && request_path() != $uri['path'],
+    ),
+    'edit' => array(
+      '#theme' => 'link',
+      '#text' => t('edit'),
+      '#path' => $uri['path'] . '/edit',
+      '#options' => array(
+        'query' => drupal_get_destination(),
+        'html' => TRUE,
+        'attributes' => array('class' => array('cps-review-operation'))
+      ),
+      '#access' => entity_access('update', 'cps_review', $review),
+    ),
+    'delete' => array(
+      '#theme' => 'link',
+      '#text' => t('delete'),
+      '#path' => $uri['path'] . '/delete',
+      '#options' => array(
+        'query' => drupal_get_destination(),
+        'html' => TRUE,
+        'attributes' => array('class' => array('cps-review-operation'))
+      ),
+      '#access' => entity_access('delete', 'cps_review', $review),
+    ),
+    'publish' => array(
+      '#theme' => 'link',
+      '#text' => t('publish'),
+      '#path' => $uri['path'] . '/publish',
+      '#options' => array(
+        'query' => drupal_get_destination(),
+        'html' => TRUE,
+        'attributes' => array('class' => array('cps-review-operation'))
+      ),
+      '#access' => entity_access('publish', 'cps_review', $review),
+    ),
+    'unpublish' => array(
+      '#theme' => 'link',
+      '#text' => t('unpublish'),
+      '#path' => $uri['path'] . '/unpublish',
+      '#options' => array(
+        'query' => drupal_get_destination(),
+        'html' => TRUE,
+        'attributes' => array('class' => array('cps-review-operation'))
+      ),
+      '#access' => entity_access('unpublish', 'cps_review', $review),
+    ),
+  );
+  $operations['#attached']['css'][] = drupal_get_path('module', 'cps_review') . '/css/cps-review.css';
+
+  drupal_alter('cps_review_operations', $operations, $review);
+  return $operations;
+}
+
+/**
+ * Shows threaded reviews.
+ *
+ * @param CPSReview $entity
+ *   The entity to get threaded reviews for.
+ *
+ * @return array
+ *   Renderable array with the threaded reviews - if any.
+ */
+function cps_review_get_thread(CPSReview $entity) {
+  $entity_query = new EntityFieldQuery();
+  $results = $entity_query
+    ->entityCondition('entity_type', 'cps_review')
+    ->propertyCondition('parent_review_id', $entity->review_id)
+    ->propertyCondition('status', 1)
+    ->propertyOrderBy('created', 'ASC')
+    ->execute();
+
+  if (empty($results['cps_review'])) {
+    return array();
+  }
+
+  $entities = entity_load('cps_review', array_keys($results['cps_review']));
+  $build = entity_view('cps_review', $entities);
+
+  return $build;
+}
diff --git a/modules/cps_review/css/cps-review.css b/modules/cps_review/css/cps-review.css
new file mode 100644
index 0000000..86c2a4b
--- /dev/null
+++ b/modules/cps_review/css/cps-review.css
@@ -0,0 +1,7 @@
+.cps-review-operation {
+  margin-left: 15px;
+}
+
+.entity-cps-review__thread {
+  margin-left: 50px;
+}
diff --git a/modules/cps_review/includes/CPSReview.class.php b/modules/cps_review/includes/CPSReview.class.php
new file mode 100644
index 0000000..911ffdb
--- /dev/null
+++ b/modules/cps_review/includes/CPSReview.class.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * @file
+ * Contains the class for the CPS review entity.
+ */
+
+/**
+ * Class to handle CPS review entities.
+ */
+class CPSReview extends Entity {
+
+  /**
+   * The database identifier of this CPS review.
+   *
+   * @var int
+   */
+  public $review_id = NULL;
+
+  /**
+   * The database identifier of the associated changeset, which will be a sha256.
+   *
+   * @var string
+   */
+  public $changeset_id = NULL;
+
+  /**
+   * The user ID of the author/owner of the CPS review.
+   *
+   * @var int
+   */
+  public $uid = NULL;
+
+  /**
+   * The current status of this CPS review.
+   *
+   * @var bool
+   */
+  public $status = NULL;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function defaultLabel() {
+    return check_plain($this->title);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function defaultUri() {
+    return array('path' => 'cps-review/' . $this->identifier());
+  }
+
+  /**
+   * Returns the HTML ID used to identify the review on the page.
+   *
+   * @return string
+   *   Returns the build HTML ID.
+   */
+  public function htmlId() {
+    return 'cps-review-' . $this->identifier();
+  }
+
+  /**
+   * Returns the permalink, where this entity can be found in context.
+   *
+   * @return array
+   *   Returns an array with 'path' and 'options' keys.
+   *
+   * @see entity_uri()
+   */
+  public function permalink() {
+    $container_entity = entity_load_single($this->container_entity_type, $this->container_entity_id);
+    $uri = entity_uri($this->container_entity_type, $container_entity);
+    $uri['options']['fragment'] = $this->htmlId();
+    drupal_alter('cps_review_permalink', $uri, $this);
+
+    return $uri;
+  }
+}
diff --git a/modules/cps_review/includes/CPSReviewController.class.php b/modules/cps_review/includes/CPSReviewController.class.php
new file mode 100644
index 0000000..b9b9656
--- /dev/null
+++ b/modules/cps_review/includes/CPSReviewController.class.php
@@ -0,0 +1,250 @@
+<?php
+/**
+ * @file
+ * Contains the controller class for the CPS review entity.
+ */
+
+/**
+ * CPS review entity controller class.
+ */
+class CPSReviewController extends EntityAPIController {
+  /**
+   * {@inheritdoc}
+   */
+  public function create(array $values = array()) {
+    global $user;
+    $values += array(
+      'changeset_id' => cps_get_current_changeset(),
+      'uid' => $user->uid,
+      'title' => '',
+      'created' => REQUEST_TIME,
+      'changed' => REQUEST_TIME,
+      'status' => TRUE,
+    );
+    return parent::create($values);
+  }
+
+  /**
+   * Determine if the operation for the entity can be accessed.
+   *
+   * @param string $op
+   *   The operation being performed. One of 'view', 'update', 'create' or
+   *   'delete'. It may also be any changeset operation such as 'publish' or
+   *   'unpublish'.
+   * @param string|CPSReview $entity
+   *   (optional) entity to check access for. If no entity is given, it will be
+   *   determined whether access is allowed for all entities of the given type.
+   * @param stdClass|null $account
+   *   The user to check for. Leave it to NULL to check for the global user.
+   *
+   * @return boolean
+   *   Whether access is allowed or not. If the entity type does not specify any
+   *   access information, NULL is returned.
+   *
+   * @see entity_access()
+   */
+  public function access($op, $entity = NULL, $account = NULL) {
+    if (!isset($account)) {
+      $account = $GLOBALS['user'];
+    }
+    $access = FALSE;
+
+    // This is just invalid.
+    if ($op !== 'create' && !$entity) {
+      return FALSE;
+    }
+
+    // No operations are valid on this one.
+    // None are valid without the administer reviews permission.
+    if (user_access('administer cps reviews', $account)) {
+      switch ($op) {
+        case 'create':
+          $access = TRUE;
+          break;
+        case 'view':
+          $access = $entity->status == ENTITY_PUBLISHED || user_access('view unpublished cps reviews', $account);
+          break;
+
+        case 'update':
+          $access = user_access('edit all cps reviews', $account) || $entity->uid == $account->uid;
+          break;
+
+        case 'delete':
+          // Reviews in published changesets may not be deleted.
+          $changeset = cps_changeset_load($entity->changeset_id);
+          $access = $changeset && empty($changeset->published);
+          break;
+
+        case 'publish':
+          $access = user_access('publish cps reviews', $account) && $entity->status == ENTITY_UNPUBLISHED;
+          break;
+
+        case 'unpublish':
+          $access = user_access('publish cps reviews', $account) && $entity->status == ENTITY_PUBLISHED;
+          $access = $access || ($account->uid == $entity->uid);
+          break;
+      }
+    }
+
+    // Let other modules get involved,
+    drupal_alter('cps_review_access', $access, $op, $entity, $account);
+    return $access;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save($entity, DatabaseTransaction $transaction = NULL) {
+    $entity->changed = REQUEST_TIME;
+    return parent::save($entity, $transaction);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
+    parent::attachLoad($queried_entities, $revision_id);
+  }
+
+  /**
+   * Implements delegated hook_entity_property_info().
+   */
+  public function hook_entity_property_info() {
+    $info = array();
+    // Add meta-data about the basic CPS review properties.
+    $properties = &$info['cps_review']['properties'];
+    $properties['changeset_id'] = array(
+      'label' => t("Associated Changeset ID"),
+      'description' => t("The ID of the associated changeset."),
+      'setter callback' => 'entity_property_verbatim_set',
+      'schema field' => 'changeset_id',
+    );
+    $properties['title'] = array(
+      'label' => t("Title"),
+      'description' => t("The title of the CPS review."),
+      'setter callback' => 'entity_property_verbatim_set',
+      'schema field' => 'title',
+    );
+    $properties['is_new'] = array(
+      'label' => t("Is new"),
+      'type' => 'boolean',
+      'description' => t("Whether the CPS review is new and not saved to the database yet."),
+      'getter callback' => 'entity_metadata_entity_get_properties',
+    );
+    $properties['url'] = array(
+      'label' => t("URL"),
+      'description' => t("The URL of the CPS review."),
+      'getter callback' => 'entity_metadata_entity_get_properties',
+      'type' => 'uri',
+      'computed' => TRUE,
+      'entity views field' => TRUE,
+    );
+    $properties['edit_url'] = array(
+      'label' => t("Edit URL"),
+      'description' => t("The URL of the CPS review's edit page."),
+      'getter callback' => 'cps_review_metadata_get_properties',
+      'type' => 'uri',
+      'computed' => TRUE,
+      'entity views field' => TRUE,
+    );
+    $properties['delete_url'] = array(
+      'label' => t("Delete URL"),
+      'description' => t("The URL of the CPS review's delete page."),
+      'getter callback' => 'cps_review_metadata_get_properties',
+      'type' => 'uri',
+      'computed' => TRUE,
+      'entity views field' => TRUE,
+    );
+    $properties['status'] = array(
+      'label' => t("Status"),
+      'type' => 'boolean',
+      'description' => t("The current state of the CPS review."),
+      'getter callback' => 'entity_property_verbatim_get',
+      'setter callback' => 'entity_property_verbatim_set',
+      'auto creation' => TRUE,
+      'schema field' => 'status',
+    );
+    $properties['created'] = array(
+      'label' => t("Date created"),
+      'type' => 'date',
+      'description' => t("The date the CPS review was posted."),
+      'setter callback' => 'entity_property_verbatim_set',
+      'setter permission' => 'administer cps reviews',
+      'schema field' => 'created',
+    );
+    $properties['changed'] = array(
+      'label' => t("Date changed"),
+      'type' => 'date',
+      'schema field' => 'changed',
+      'description' => t("The date the CPS review was most recently updated."),
+    );
+    $properties['author'] = array(
+      'label' => t("Author"),
+      'type' => 'user',
+      'description' => t("The author of the CPS review."),
+      'getter callback' => 'cps_review_metadata_get_properties',
+      'setter callback' => 'entity_property_verbatim_set',
+      'setter permission' => 'administer cps reviews',
+      'required' => TRUE,
+      'schema field' => 'uid',
+    );
+    $properties['operations'] = array(
+      'label' => t("Operations"),
+      'description' => t("The available operations for a CPS review."),
+      'getter callback' => 'cps_review_metadata_get_properties',
+      'computed' => TRUE,
+      'entity views field' => TRUE,
+      'type' => 'text',
+      'sanitized' => TRUE,
+    );
+
+    return $info;
+  }
+
+  /**
+   * @{inheritdoc}
+   */
+  public function buildContent($entity, $view_mode = 'full', $langcode = NULL, $content = array()) {
+    /** @var CPSReview $entity */
+    $build = array();
+    $build['body'] = parent::buildContent($entity, $view_mode, $langcode, $content);
+
+    $account = user_load($entity->uid);
+    $build['title'] = array(
+      '#markup' => check_plain($entity->title),
+      '#access' => variable_get('cps_review_use_title', 0) == 1,
+      '#weight' => -10,
+    );
+    $uri = $entity->permalink();
+    $build['submitted'] = array(
+      '#markup' => t('Submitted by !username on !datetime - !permalink', array(
+        '!username' => l(format_username($account), 'user/' . $account->uid),
+        '!datetime' => format_date($entity->created),
+        '!permalink' => l(t('Permalink'), $uri['path'], $uri['options']),
+        )
+      ),
+      '#weight' => -5,
+    );
+    $build['links'] = array(
+      '#type' => 'container',
+      '#weight' => 10,
+      '#attributes' => array(
+        'class' => array(
+          'entity-cps-review__operations',
+        ),
+      ),
+      'operations' => cps_review_get_operations($entity),
+    );
+    $build['thread'] = array(
+      '#type' => 'container',
+      '#attributes' => array(
+        'class' => array(
+          'entity-cps-review__thread',
+        ),
+      ),
+      '#weight' => 20,
+      'thread' => cps_review_get_thread($entity),
+    );
+    return $build;
+  }
+}
diff --git a/modules/cps_review/includes/admin.inc b/modules/cps_review/includes/admin.inc
new file mode 100644
index 0000000..aa675f9
--- /dev/null
+++ b/modules/cps_review/includes/admin.inc
@@ -0,0 +1,302 @@
+<?php
+/**
+ * @file
+ * CPS review administration pages.
+ */
+
+/**
+ * List all entities for the given type.
+ *
+ * @param $view_name
+ *   The view to render the reviews admin screen.
+ *
+ * @return string
+ *   A rendered page.
+ */
+function cps_review_list_page($view_name) {
+  $view = views_get_view($view_name);
+  if (!$view || !$view->access('default')) {
+    return MENU_NOT_FOUND;
+  }
+
+  drupal_set_title(t('All CPS reviews'));
+
+  $view->override_path = $_GET['q'];
+  return $view->preview('default');
+}
+
+/**
+ * Field callback to add a new review and list existing reviews.
+ *
+ * @param string $container_entity_type
+ *   The entity type of the containing entity.
+ * @param int $container_entity_id
+ *   The entity ID this review is part of.
+ *
+ * @return array
+ *   The rendered output of the extra field with keys 'form' and 'reviews'.
+ */
+function cps_review_widget($container_entity_type, $container_entity) {
+  $entity = cps_review_create();
+
+  list($container_entity_id,,) = entity_extract_ids($container_entity_type, $container_entity);
+  $entity->container_entity_type = $container_entity_type;
+  $entity->container_entity_id = $container_entity_id;
+
+  $form_state = array(
+    'entity' => $entity,
+    'no_redirect' => TRUE,
+    'op' => 'add',
+    'args' => array(),
+  ) + form_state_defaults();
+
+  form_load_include($form_state, 'inc', 'cps_review', 'includes/forms');
+  $output = drupal_build_form('cps_review_edit_form', $form_state);
+
+  if (!empty($form_state['executed'])) {
+    // Use the one on $form_state because form caching can change the object.
+    $form_state['entity']->save();
+    call_user_func_array('drupal_goto', $entity->permalink());
+  }
+
+  $build = array(
+    '#type' => 'container',
+    '#attributes' => array(
+      'class' => array(
+        'cps-review'
+      ),
+    ),
+  );
+  $build['reviews'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Reviews'),
+    '#attributes' => array(
+      'class' => array(
+        'cps-review__reviews'
+      ),
+    ),
+    '#collapsible' => TRUE,
+    '#collapsed' => FALSE,
+    'reviews' => cps_review_list_reviews($container_entity_type, $container_entity_id),
+  );
+  $build['form'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Add review'),
+    '#attributes' => array(
+      'class' => array(
+        'cps-review__form'
+      ),
+    ),
+    '#collapsible' => TRUE,
+    '#collapsed' => FALSE,
+    'form' => $output,
+  );
+
+  // Prevent the review form from being displayed.
+  $changeset_id = cps_get_current_changeset();
+  if ($changeset_id == CPS_PUBLISHED_CHANGESET) {
+    $build['form']['#access'] = FALSE;
+  }
+  else {
+    $changeset = cps_changeset_load($changeset_id);
+    if (!empty($changeset->published)) {
+      $build['form']['#access'] = FALSE;
+    }
+  }
+
+  // Do not show the reviews part if there are no reviews.
+  if (empty($build['reviews']['reviews'])) {
+    hide($build['reviews']);
+  }
+
+  // Give other modules a chance to allow a different policy.
+  drupal_alter('cps_review_widget', $build);
+
+  return $build;
+}
+
+/**
+ * List all reviews for this entity in order of creation.
+ *
+ * @param string $container_entity_type
+ *   The entity type of the containing entity.
+ * @param int $container_entity_id
+ *   The entity ID this review is part of.
+ *
+ * @return array
+ *   Render Array with all matching reviews.
+ *
+ */
+function cps_review_list_reviews($container_entity_type, $container_entity_id) {
+  $build = array();
+
+  $changeset_id = cps_get_current_changeset();
+
+  $entity_query = new EntityFieldQuery();
+  $results = $entity_query
+    ->entityCondition('entity_type', 'cps_review')
+    ->propertyCondition('container_entity_type', $container_entity_type)
+    ->propertyCondition('container_entity_id', $container_entity_id)
+    ->propertyCondition('changeset_id', $changeset_id)
+    ->propertyCondition('status', 1)
+    ->propertyCondition('parent_review_id', 0)
+    ->propertyOrderBy('created', 'ASC')
+    ->execute();
+
+  if (empty($results['cps_review'])) {
+    return array();
+  }
+
+  $entities = entity_load('cps_review', array_keys($results['cps_review']));
+  $build = entity_view('cps_review', $entities);
+
+  return $build;
+}
+
+/**
+ * Page callback to reply to an entity.
+ *
+ * @param CPSReview $review
+ *   The review to reply to.
+ *
+ * @return array
+ *   A drupal renderable array.
+ */
+function cps_review_reply_entity_page(CPSReview $review) {
+  $entity = cps_review_create();
+
+  $entity->container_entity_type = $review->container_entity_type;
+  $entity->container_entity_id = $review->container_entity_id;
+  $entity->parent_review_id = $review->review_id;
+
+  drupal_set_title(t('Reply to a CPS review'));
+
+  $form_state = array(
+    'entity' => $entity,
+    'no_redirect' => TRUE,
+    'op' => 'add',
+    'args' => array(),
+  ) + form_state_defaults();
+
+  form_load_include($form_state, 'inc', 'cps_review', 'includes/forms');
+  $output = drupal_build_form('cps_review_edit_form', $form_state);
+
+  if (!empty($form_state['executed'])) {
+    // Use the one on $form_state because form caching can change the object.
+    $form_state['entity']->save();
+    call_user_func_array('drupal_goto', $entity->permalink());
+  }
+
+  return $output;
+}
+
+/**
+ * Page callback to edit a entity.
+ *
+ * @param CPSReview $entity
+ *   The entity to edit.
+ *
+ * @return array
+ *   A Drupal renderable array.
+ */
+function cps_review_edit_page(CPSReview $entity) {
+  $form_state = array(
+    'entity' => $entity,
+    'no_redirect' => TRUE,
+    'op' => 'edit',
+  ) + form_state_defaults();
+
+  form_load_include($form_state, 'inc', 'cps_review', 'includes/forms');
+  $output = drupal_build_form('cps_review_edit_form', $form_state);
+
+  if (!empty($form_state['executed'])) {
+    // Use the one on $form_state because form caching can actually change the object.
+    /** @var CPSReview $entity */
+    $entity = $form_state['entity'];
+    $entity->save();
+    call_user_func_array('drupal_goto', $entity->permalink());
+  }
+
+  return $output;
+}
+
+/**
+ * Page callback to view a entity.
+ *
+ * @param CPSReview $entity
+ *   The entity to view.
+ * @param string $view_mode
+ *   (optional) The view mode to render. Defaults to 'full'.
+ *
+ * @return string
+ *   A rendered page.
+ */
+function cps_review_view_page(CPSReview $entity, $view_mode = 'full') {
+  $build = $entity->view($view_mode, NULL, TRUE);
+  return $build;
+}
+
+/**
+ * Menu callback via drupal_get_form() -- ask for confirmation of entity deletion
+ */
+function cps_review_delete_form($form, &$form_state, CPSReview $entity) {
+  $form_state['entity'] = $entity;
+  $uri = $entity->permalink();
+  return confirm_form($form,
+    t('Are you sure you want to delete %title?', array('%title' => $entity->label())),
+    $uri['path'],
+    t('This action cannot be undone.'),
+    t('Delete'),
+    t('Cancel')
+  );
+}
+
+/**
+ * Form submit handler to execute entity deletion
+ */
+function cps_review_delete_form_submit($form, &$form_state) {
+  if ($form_state['values']['confirm']) {
+    /** @var CPSReview $entity */
+    $entity = $form_state['entity'];
+    $entity->delete();
+    drupal_set_message(t('CPS review "%title" has been deleted.', array('%title' => $entity->label())));
+  }
+
+  $form_state['redirect'] = 'admin/structure/cps_reviews';
+}
+
+/**
+ * Page callback to display the publish CPS review form.
+ *
+ * @param CPSReview $entity
+ *
+ * @return array
+ *   A Drupal renderable array.
+ */
+function cps_review_publish_page(CPSReview $entity) {
+  $form_state = array(
+    'entity' => $entity,
+  ) + form_state_defaults();
+
+  form_load_include($form_state, 'inc', 'cps_review', 'includes/forms');
+  $output = drupal_build_form('cps_review_publish_form', $form_state);
+  return $output;
+}
+
+/**
+ * Page callback to display the unpublish CPS review form.
+ *
+ * @param CPSReview $entity
+ *
+ * @return array
+ *   A Drupal renderable array.
+ */
+function cps_review_unpublish_page(CPSReview $entity) {
+  $form_state = array(
+    'entity' => $entity,
+  ) + form_state_defaults();
+
+  form_load_include($form_state, 'inc', 'cps_review', 'includes/forms');
+  $output = drupal_build_form('cps_review_unpublish_form', $form_state);
+  return $output;
+}
diff --git a/modules/cps_review/includes/forms.inc b/modules/cps_review/includes/forms.inc
new file mode 100644
index 0000000..83c600b
--- /dev/null
+++ b/modules/cps_review/includes/forms.inc
@@ -0,0 +1,172 @@
+<?php
+/**
+ * @file
+ * Form definitions for admin forms.
+ */
+
+/**
+ * Basic edit form for the content entity.
+ *
+ * The entity being edited should be stored in $form_state['entity']
+ * when this form is built.
+ *
+ * The name of this form is autogenerated as $entity_type . '_edit_form'.
+ */
+function cps_review_edit_form($form, &$form_state) {
+  $entity = $form_state['entity'];
+
+  $form['entity'] = array();
+  $form['entity']['title'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Subject'),
+    '#description' => t('Review Title'),
+    '#default_value' => $entity->title,
+    '#access' => variable_get('cps_review_use_title', 0) == 1,
+    '#weight' => -10,
+  );
+  $form['entity']['body'] = array();
+
+  $entity_type = $entity->entityType();
+  field_attach_form($entity_type, $entity, $form['entity']['body'], $form_state, entity_language($entity_type, $entity));
+
+  $form['actions'] = array(
+    '#type' => 'actions',
+  );
+
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * Validate callback for the content entity.
+ */
+function cps_review_edit_form_validate($form, &$form_state) {
+  $entity = $form_state['entity'];
+  field_attach_form_validate($entity->entityType(), $entity, $form, $form_state);
+}
+
+/**
+ * Submit callback for the content entity.
+ */
+function cps_review_edit_form_submit($form, &$form_state) {
+  $entity = $form_state['entity'];
+  entity_form_submit_build_entity('cps_review', $entity, $form, $form_state);
+  _cps_review_populate_title($entity);
+}
+
+/**
+ * Ensures a title is always set and possibly calculated from the body.
+ *
+ * @param $entity
+ *   The entity to populate the title property on if empty.
+ */
+function _cps_review_populate_title($entity) {
+  if (trim($entity->title) != '') {
+    return;
+  }
+
+  // The body may be in any format, so:
+  // 1) Filter it into HTML
+  // 2) Strip out all HTML tags
+  // 3) Convert entities back to plain-text.
+  $field = field_info_field('cps_review_body');
+  $langcode = field_is_translatable('cps_review_body', $field) ? entity_language('cps_review', $comment) : LANGUAGE_NONE;
+  $body = $entity->cps_review_body[$langcode][0];
+  if (isset($body['format'])) {
+    $text = check_markup($body['value'], $body['format']);
+  }
+  else {
+    $text = check_plain($body['value']);
+  }
+  $entity->title = truncate_utf8(trim(decode_entities(strip_tags($text))), 29, TRUE);
+
+  // Edge cases where the entity body is populated only by HTML tags will
+  // require a default title.
+  if ($entity->title == '') {
+    $entity->title = t('(No title)');
+  }
+}
+
+
+/**
+ * Form callback to display the publish form.
+ */
+function cps_review_publish_form($form, &$form_state) {
+  $form['warning'] = array(
+    '#markup' => '<div class="publish-warning">' . t('Are you sure you want to publish this?') . '</div>',
+  );
+
+  $form['actions']['publish'] = array(
+    '#type' => 'submit',
+    '#value' => t('Publish'),
+  );
+
+  $form['actions']['cancel'] = array(
+    '#type' => 'submit',
+    '#value' => t('Cancel'),
+    '#limit_validation_errors' => array(),
+    '#submit' => array('cps_review_publish_form_cancel'),
+  );
+
+  return $form;
+}
+
+/**
+ * Submit handler to cancel the form.
+ */
+function cps_review_publish_form_cancel($form, &$form_state) {
+  $form_state['redirect'] = $form_state['entity']->uri();
+}
+
+/**
+ * Submit handler for the publish form.
+ */
+function cps_review_publish_form_submit($form, &$form_state) {
+  $entity = $form_state['entity'];
+  $entity->status = ENTITY_PUBLISHED;
+  $entity->save();
+  $form_state['redirect'] = $form_state['entity']->uri();
+}
+
+// -----------------------------------------------------------------------
+// Unpublish form and handlers.
+
+/**
+ * Form callback to display the unpublish form.
+ */
+function cps_review_unpublish_form($form, &$form_state) {
+  $entity = $form_state['entity'];
+
+  $form['warning'] = array(
+    '#markup' => '<div class="unpublish-warning">' . t('Are you sure you want to unpublish this?') . '</div>',
+  );
+
+  $form['actions']['unpublish'] = array(
+    '#type' => 'submit',
+    '#value' => t('Unpublish'),
+  );
+
+  $form['actions']['cancel'] = array(
+    '#type' => 'submit',
+    '#value' => t('Cancel'),
+    '#limit_validation_errors' => array(),
+    // We re-use the publish_form_cancel here because all it does is redirect.
+    '#submit' => array('cps_review_publish_form_cancel'),
+  );
+
+  return $form;
+}
+
+/**
+ * Submit handler for the unpublish form.
+ */
+function cps_review_unpublish_form_submit($form, &$form_state) {
+  $entity = $form_state['entity'];
+  $entity->status = ENTITY_UNPUBLISHED;
+  $entity->save();
+  $form_state['redirect'] = $form_state['entity']->uri();
+}
diff --git a/modules/cps_review/plugins/views/cps_review.views_default.inc b/modules/cps_review/plugins/views/cps_review.views_default.inc
new file mode 100644
index 0000000..45986ef
--- /dev/null
+++ b/modules/cps_review/plugins/views/cps_review.views_default.inc
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * @file
+ * Bulk export of views_default objects generated by Bulk export module.
+ */
+
+/**
+ * Implements hook_views_default_views().
+ */
+function cps_review_views_default_views() {
+  $views = array();
+
+  $view = new view();
+  $view->name = 'administer_cps_reviews';
+  $view->description = '';
+  $view->tag = 'cps_review';
+  $view->base_table = 'cps_review';
+  $view->human_name = 'Administer CPS reviews';
+  $view->core = 7;
+  $view->api_version = '3.0';
+  $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+  
+  /* Display: Master */
+  $handler = $view->new_display('default', 'Master', 'default');
+  $handler->display->display_options['use_more_always'] = FALSE;
+  $handler->display->display_options['access']['type'] = 'none';
+  $handler->display->display_options['cache']['type'] = 'none';
+  $handler->display->display_options['query']['type'] = 'views_query';
+  $handler->display->display_options['exposed_form']['type'] = 'basic';
+  $handler->display->display_options['pager']['type'] = 'full';
+  $handler->display->display_options['pager']['options']['items_per_page'] = '25';
+  $handler->display->display_options['style_plugin'] = 'table';
+
+  /* No results behavior: Global: Text area */
+  $handler->display->display_options['empty']['area']['id'] = 'area';
+  $handler->display->display_options['empty']['area']['table'] = 'views';
+  $handler->display->display_options['empty']['area']['field'] = 'area';
+  $handler->display->display_options['empty']['area']['empty'] = TRUE;
+  $handler->display->display_options['empty']['area']['content'] = 'No CPS reviews found.';
+  $handler->display->display_options['empty']['area']['format'] = 'plain_text';
+  /* Relationship: CPS Review: Author uid */
+  $handler->display->display_options['relationships']['author']['id'] = 'author';
+  $handler->display->display_options['relationships']['author']['table'] = 'cps_review';
+  $handler->display->display_options['relationships']['author']['field'] = 'author';
+  $handler->display->display_options['relationships']['author']['label'] = 'Author';
+  $handler->display->display_options['relationships']['author']['required'] = TRUE;
+  /* Field: CPS Review: Associated Changeset ID */
+  $handler->display->display_options['fields']['changeset_id']['id'] = 'changeset_id';
+  $handler->display->display_options['fields']['changeset_id']['table'] = 'cps_review';
+  $handler->display->display_options['fields']['changeset_id']['field'] = 'changeset_id';
+  /* Field: CPS Review: Subject */
+  $handler->display->display_options['fields']['title']['id'] = 'title';
+  $handler->display->display_options['fields']['title']['table'] = 'cps_review';
+  $handler->display->display_options['fields']['title']['field'] = 'title';
+  /* Field: User: Name */
+  $handler->display->display_options['fields']['name']['id'] = 'name';
+  $handler->display->display_options['fields']['name']['table'] = 'users';
+  $handler->display->display_options['fields']['name']['field'] = 'name';
+  $handler->display->display_options['fields']['name']['relationship'] = 'author';
+  /* Field: CPS Review: Status */
+  $handler->display->display_options['fields']['status']['id'] = 'status';
+  $handler->display->display_options['fields']['status']['table'] = 'cps_review';
+  $handler->display->display_options['fields']['status']['field'] = 'status';
+  $handler->display->display_options['fields']['status']['label'] = 'Published';
+  $handler->display->display_options['fields']['status']['not'] = 0;
+  /* Field: CPS Review: Date created */
+  $handler->display->display_options['fields']['created']['id'] = 'created';
+  $handler->display->display_options['fields']['created']['table'] = 'cps_review';
+  $handler->display->display_options['fields']['created']['field'] = 'created';
+  /* Field: CPS Review: Operations */
+  $handler->display->display_options['fields']['operations']['id'] = 'operations';
+  $handler->display->display_options['fields']['operations']['table'] = 'views_entity_cps_review';
+  $handler->display->display_options['fields']['operations']['field'] = 'operations';
+  $views[$view->name] = $view;
+
+  return $views;
+}
diff --git a/modules/cps_review/templates/cps-review.tpl.php b/modules/cps_review/templates/cps-review.tpl.php
new file mode 100644
index 0000000..06ac7b1
--- /dev/null
+++ b/modules/cps_review/templates/cps-review.tpl.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Default theme implementation to display custom entities.
+ *
+ * Variables:
+ * - $css: An array of CSS files for the current page.
+ *
+ * @see template_preprocess()
+ * @see template_preprocess_entity()
+ * @see template_process()
+ */
+
+hide($content['submitted']);
+$rendered_content = render($content);
+?>
+<?php if (!empty($rendered_content)): ?>
+  <div class="<?php print $classes; ?> clearfix"<?php print $attributes; ?>>
+    <?php if ($title): ?>
+      <h3><?php print $title; ?></h3>
+    <?php endif; ?>
+    <div class="submitted"><?php print render($content['submitted']); ?></div>
+    <?php
+      print $rendered_content;
+    ?>
+  </div>
+<?php endif; ?>
