diff --git a/activity/project_activity_project.info b/activity/project_activity_project.info
new file mode 100644
index 0000000..3b1c7b0
--- /dev/null
+++ b/activity/project_activity_project.info
@@ -0,0 +1,7 @@
+name = Project Activity
+description = Part of the Google Summer of Code Project 'Development Activity logging, Activity Streams and Development Statistics'.
+package = Project Activity
+dependencies[] = project
+dependencies[] = trigger
+dependencies[] = activity
+core = 6.x
diff --git a/activity/project_activity_project.module b/activity/project_activity_project.module
new file mode 100644
index 0000000..522be99
--- /dev/null
+++ b/activity/project_activity_project.module
@@ -0,0 +1,288 @@
+<?php
+
+/**
+ * @file: Main module of Project Activity: Project.
+ */
+ 
+/**
+ * Implementation of hook_hook_info().
+ *
+ * Provides Trigger support for Project.
+ */
+function project_hook_info() {
+  $hooks = project_activity_project_hooks();
+  
+  return array('project' => array('project' => $hooks['project']));
+}
+
+/**
+ * Implementation of hook_activity_info().
+ *
+ * Provides Activity support for Project.
+ */
+function project_activity_info() {
+  static $cache;
+  
+  if (!isset($cache)) {
+    $cache = project_activity_project_hooks();
+  
+    foreach ($cache as $module => &$hooks) {
+      $hooks = array_keys($hooks);
+    }
+  }
+  
+  $info = new stdClass();
+  
+  $info->api = 2;
+  $info->name = 'project';
+  $info->object_type = 'project';
+  $info->eid_field = 'nid';
+  $info->objects = array('Actor' => 'project');
+  $info->hooks = array('project' => $cache['project']);
+  $info->type_options = array('sandbox' => 'Sandbox', 'project' => 'Projects');
+  $info->list_callback = 'project_activity_project_list_activity_actions';
+  $info->context_load_callback = 'project_activity_project_load_activity_context';
+  
+  return $info;
+}
+
+/**
+ * Implementation of hook_activity_type_check().
+ */
+function project_activity_type_check($token_objects, $types) {
+  $category = (($token_objects['project']->project['sandbox'] == '1') ? 'sandbox' : 'project');
+  
+  return (in_array($category, $types));
+}
+
+/**
+ * Returns a list of triggers for Project.
+ */
+function project_activity_project_hooks() {
+  return $cache = array(
+    'project' =>
+      array(
+        'promote_sandbox' => array(
+          'runs when' => t('When a sandbox gets promoted to a project'),
+        ),
+        'maintainer_new' => array(
+          'runs when' => t('When a new maintainer was added'),
+        ),
+        'insert' => array(
+          'runs when' => t('When a new project gets created'),
+        ),
+        'update' => array(
+          'runs when' => t('When an existing project gets updated'),
+        ),
+      ),
+  );
+}
+
+/**
+ * Implementation of hook_activity_objects_alter().
+ */
+function project_activity_project_activity_objects_alter(&$objects, $type) {
+  switch ($type) {
+    case 'project':
+      $objects['node'] = $objects['project'];
+      break;
+  }
+}
+
+/**
+ * Implementation of hook_activity_record_alter().
+ */
+function project_activity_project_activity_record_alter(&$record, $context) {
+  if (!empty($context['project'])) {
+    $record->nid = $context['pid'];
+  }
+}
+
+/**
+ * Following functions are to catch events from the Project module.
+ */
+
+/**
+ * Implementation of hook_project_promote_sandbox().
+ */
+function project_activity_project_project_promote_sandbox($node) {
+  global $user;
+  
+  $aids = _trigger_get_hook_aids('project', 'promote_sandbox');
+  $context = project_activity_project_context('promote_sandbox', $node, $user->uid);
+
+  actions_do(array_keys($aids), $project, $context);
+}
+
+/**
+ * Implementation of hook_project_maintainer_save().
+ *
+ * @todo Make this trigger behave like it should.
+ * @todo Implement hook_project_maintainer_new in Project, currently we can only work when maintainers get saved.
+ */
+function project_activity_project_project_maintainer_new($nid, $uid, $permissions) {
+  global $user;
+  
+  $node = node_load($nid);
+  
+  $aids = _trigger_get_hook_aids('project', 'maintainer_new');
+  $context = project_activity_project_context('maintainer_new', $node, $user->uid, array('maintainer_new' => user_load($uid)));
+
+  actions_do(array_keys($aids), $node, $context);
+}
+
+/**
+ * Implementation of hook_nodeapi().
+ */
+function project_activity_project_nodeapi(&$node, $op, $arg) {
+  if (!empty($node->project) && ($node->type == 'project_project')) {
+    switch ($op) {
+      case 'insert':
+      case 'update':
+        global $user;
+        
+        $aids = _trigger_get_hook_aids('project', $op);
+        $context = project_activity_project_context($op, $node, $user->uid);
+
+        actions_do(array_keys($aids), $node, $context);
+    }
+  }
+}
+
+/**
+ * Returns the context array for Project activity.
+ */
+function project_activity_project_context($op, $node, $uid = FALSE, $options = array()) {
+  if (empty($node)) {
+    return array();
+  } 
+  else {
+    $context = array();
+    
+    if ($uid !== FALSE) {
+      $context['actor'] = $uid;
+    }
+    
+    foreach ($options as $key => $value) {
+      $node->project[$key] = $value;
+    }
+    
+    $context += array(
+      'hook' => 'project',
+      'op' => $op,
+      'action' => $op,
+      'project' => $node,
+      'pid' => $node->nid,
+    );
+    
+    return $context;
+  }
+}
+
+/**
+ * Implementation of hook_activity_token_list().
+ */
+function project_activity_token_list($type = 'all') {
+  if ($type == 'project') {
+    $tokens = array_merge_recursive(project_token_list('node'), node_activity_token_list('node'));
+    $tokens = $tokens['node'];
+    
+    $tokens['project_maintainer'] = t('Name of the newly created maintainer');
+    $tokens['project_maintainer-link'] = t('Link to the newly created maintainer');
+
+    return array(
+      'project' => $tokens,
+    );
+  }
+}
+
+/**
+ * Implementation of hook_activity_token_values().
+ */
+function project_activity_token_values($type, $object = NULL, $options = array()) {
+  $values = array();
+
+  if ($type == 'project') {
+    $values['project_maintainer'] = '';
+    $values['project_maintainer-link'] = '';
+    
+    if ((!empty($object->project['maintainer_new'])) && (is_object($object->project['maintainer_new']))) {
+      $maintainer = $object->project['maintainer_new'];
+      
+      $values['project_maintainer'] = $maintainer->name;
+      $values['project_maintainer-link'] = theme('activity_username', $maintainer);;
+    }
+  }
+  
+  return $values;
+}
+
+/**
+ * List all the Activity Actions that match the hook and op.
+ *
+ * @param string $hook
+ *  The hook that is to be fired.
+ * @param string $op
+ *  The op to be used in the hook.
+ * @param int $max_age
+ *  The max age from now.
+ *
+ * @return array
+ *  An array of arrays with 'id', 'created' and 'actor' keys.
+ */
+function project_activity_project_list_activity_actions($hook, $op, $max_age, $from, $count) {
+  $actions = array();
+
+  if (!empty($max_age)) {
+    $min_time = time() - $max_age;
+  }
+  else {
+    $min_time = 0;
+  }
+  
+  $sql_strings = array(
+    'insert' => "SELECT nid as nid, created as created, uid as actor FROM {node} WHERE created > %d AND type = 'project_project'",
+    'update' => "SELECT nid as nid, changed as created, uid as actor FROM {node} WHERE created > %d AND type = 'project_project' AND NOT (created = changed)",
+    'maintainer_new' => "SELECT m.uid AS maintainer_uid, n.nid AS nid, n.uid AS actor, n.created AS created FROM {project_maintainer} m LEFT JOIN {node} n ON n.nid = m.nid WHERE created > %d",
+  );
+
+  if (!empty($sql_strings[$op])) {
+    $result = db_query_range($sql_strings[$op], $min_time, $from, $count);
+    while ($row = db_fetch_array($result)) {
+      $row['id'] = array('nid' => $row['nid'], 'options' => array());
+      
+      switch ($op) {
+        case 'maintainer_new':
+          $row['id']['options'] = array(
+            'maintainer_new' => user_load($row['maintainer_uid'])
+          );
+          
+          break;
+      }
+      
+      $actions[] = $row;
+    }
+  }
+
+  return $actions;
+}
+
+/**
+ * Load up the context array to pass to activity_record.
+ *
+ * @param string $hook
+ *  The hook that is being fired.
+ * @param string $op
+ *  The op for that hook.
+ * @param string $id
+ *  The id for the action.
+ *
+ * @return array
+ *   The context array for activity_record.
+ * @see trigger_nodeapi
+ */
+function project_activity_project_load_activity_context($hook, $op, $id) {
+  if ($node = node_load($id['nid'])) {
+    return project_activity_project_context($op, $node, FALSE, $id['options']);
+  }
+}
\ No newline at end of file
diff --git a/activity/project_activity_release.info b/activity/project_activity_release.info
new file mode 100644
index 0000000..f493aed
--- /dev/null
+++ b/activity/project_activity_release.info
@@ -0,0 +1,8 @@
+name = Project Release Activity
+description = Part of the Google Summer of Code Project 'Development Activity logging, Activity Streams and Development Statistics'.
+package = Project Activity
+dependencies[] = project_activity_project
+dependencies[] = project
+dependencies[] = trigger
+dependencies[] = activity
+core = 6.x
diff --git a/activity/project_activity_release.module b/activity/project_activity_release.module
new file mode 100644
index 0000000..30ddd8c
--- /dev/null
+++ b/activity/project_activity_release.module
@@ -0,0 +1,246 @@
+<?php
+
+/**
+ * @file: Main module of Project Activity: Release.
+ */
+ 
+/**
+ * Implementation of hook_hook_info().
+ *
+ * Provides Trigger support for Project release.
+ */
+function project_release_hook_info() {
+  $hooks = project_activity_release_hooks();
+  
+  return array('project_release' => array('project_release' => $hooks['project_release']));
+}
+
+/**
+ * Implementation of hook_activity_info().
+ *
+ * Provides Activity support for Project release.
+ */
+function project_release_activity_info() {
+  static $cache;
+  
+  if (!isset($cache)) {
+    $cache = project_activity_release_hooks();
+  
+    foreach ($cache as $module => &$hooks) {
+      $hooks = array_keys($hooks);
+    }
+  }
+  
+  $info = new stdClass();
+  
+  $info->api = 2;
+  $info->name = 'project_release';
+  $info->object_type = 'project_release';
+  $info->eid_field = 'nid';
+  $info->objects = array('Actor' => 'project_release');
+  $info->hooks = array('project_release' => $cache['project_release']);
+  $info->list_callback = 'project_release_activity_list_activity_actions';
+  $info->context_load_callback = 'project_release_activity_load_activity_context';
+  
+  return $info;
+}
+
+/**
+ * Implementation of hook_activity_objects_alter().
+ */
+function project_activity_release_activity_objects_alter(&$objects, $type) {
+  switch ($type) {
+    case 'project_release':
+      $objects['node'] = node_load($objects['project_release']->project_release['pid']);
+      
+      break;
+  }
+}
+
+/**
+ * Implementation of hook_activity_record_alter().
+ */
+function project_activity_release_activity_record_alter(&$record, $context) {
+  if (!empty($context['project_release'])) {
+    $record->nid = $context['project_release']->nid;
+  }
+}
+
+/**
+ * Implementation of hook_activity_token_list().
+ */
+function project_release_activity_token_list($type = 'all') {
+  if ($type == 'project_release') {
+    $tokens = project_release_token_list('node');
+    $tokens = $tokens['node'];
+    
+    $tokens['project_release'] = t('Link to project release');
+    
+    return array(
+      'project release' => $tokens,
+    );
+  }
+}
+
+/**
+ * Implementation of hook_activity_token_values().
+ */
+function project_release_activity_token_values($type, $object = NULL, $options = array()) {
+  if ($type == 'project_release') {
+    $values = project_release_token_values('node', $object);
+    
+    $values['project_release'] = l($object->title, "node/{$object->nid}");
+    
+    return $values;
+  }
+}
+
+/**
+ * Returns a list of triggers for Project.
+ */
+function project_activity_release_hooks() {
+  return array(
+    'project_release' =>
+      array(
+        'insert' => array(
+          'runs when' => t('When a new release gets created'),
+        ),
+        'update' => array(
+          'runs when' => t('When an existing release gets updated'),
+        ),
+        'delete' => array(
+          'runs when' => t('When a release gets deleted'),
+        ),
+        'create_package' => array(
+          'runs when' => t('When a release gets packaged'),
+        ),
+      ),
+  );
+}
+
+/**
+ * Implementation of hook_activity_charts_query_alter().
+ */
+function project_release_activity_charts_query_alter($query) {
+  $release_table = $query->add_table('project_release_nodes', 'node_activity');
+  
+  $join = new views_join;
+  $join->construct('node', $release_table, 'pid', 'nid');
+  $release_project_table = $query->add_relationship('project_issues_project', $join, $release_table);
+  
+  $query->add_field($release_table, 'pid', 'activity_id');
+  $query->add_field($release_project_table, 'title', 'activity_id_name');
+  
+  return $query;
+}
+
+/**
+ * Implementation of hook_nodeapi.
+ */
+function project_activity_release_nodeapi(&$node, $op, $arg) {
+  global $user;
+  
+  if (!empty($node->project_release) && ($node->type == 'project_release')) {
+    switch ($op) {
+      case 'insert':
+      case 'update':
+      case 'delete':
+        $aids = _trigger_get_hook_aids('project_release', $op);
+        $context = project_release_activity_context($op, $node, $user->uid);
+
+        actions_do(array_keys($aids), $node, $context);
+    }
+  }
+}
+
+/**
+ * Implementation of hook_project_release_create_package.
+ */
+function project_activity_release_project_release_create_package($project_node, $release_node) {   
+  $aids = _trigger_get_hook_aids('project_release', 'create_package');
+  $context = project_release_activity_context('create_package', $release_node, $release_node->uid);
+
+  actions_do(array_keys($aids), $node, $context);
+}
+
+/**
+ * Returns the context array for Project activity.
+ */
+function project_release_activity_context($op, $node, $uid = FALSE) {
+  if (empty($node)) {
+    return array();
+  } 
+  else {
+    $context = array(
+      'hook' => 'project_release',
+      'op' => $op,
+      'action' => $op,
+      'project_release' => $node,
+      'pid' => $node->project_release['pid'],
+    );
+    
+    if ($uid !== FALSE) {
+      $context['actor'] = $uid;
+    }
+    
+    return $context;
+  }
+}
+
+/**
+ * List all the Activity Actions that match the hook and op.
+ *
+ * @param string $hook
+ *  The hook that is to be fired.
+ * @param string $op
+ *  The op to be used in the hook.
+ * @param int $max_age
+ *  The max age from now.
+ *
+ * @return array
+ *  An array of arrays with 'id', 'created' and 'actor' keys.
+ */
+function project_release_activity_list_activity_actions($hook, $op, $max_age, $from, $count) {
+  $actions = array();
+
+  if (!empty($max_age)) {
+    $min_time = time() - $max_age;
+  }
+  else {
+    $min_time = 0;
+  }
+  
+  $sql_strings = array(
+    'insert' => "SELECT nid as id, created as created, uid as actor FROM {node} WHERE created > %d AND type = 'project_release'",
+    'update' => "SELECT nid as id, changed as created, uid as actor FROM {node} WHERE created > %d AND type = 'project_release' AND NOT (created = changed)",
+  );
+
+  if (!empty($sql_strings[$op])) {
+    $result = db_query_range($sql_strings[$op], $min_time, $from, $count);
+    while ($row = db_fetch_array($result)) {
+      $actions[] = $row;
+    }
+  }
+
+  return $actions;
+}
+
+/**
+ * Load up the context array to pass to activity_record.
+ *
+ * @param string $hook
+ *  The hook that is being fired.
+ * @param string $op
+ *  The op for that hook.
+ * @param string $id
+ *  The id for the action.
+ *
+ * @return array
+ *   The context array for activity_record.
+ * @see trigger_nodeapi
+ */
+function project_release_activity_load_activity_context($hook, $op, $id) {
+  if ($node = node_load($id)) {
+    return project_release_activity_context($op, $node);
+  }
+}
\ No newline at end of file
diff --git a/activity/tests/project_activity_project.test b/activity/tests/project_activity_project.test
new file mode 100644
index 0000000..aabf580
--- /dev/null
+++ b/activity/tests/project_activity_project.test
@@ -0,0 +1,262 @@
+<?php
+
+/**
+ * @file: Provide tests for Project Activity module.
+ */
+ 
+include_once drupal_get_path('module', 'project_activity_project') . '/tests/project_activity_testcase.test';
+ 
+class ProjectActivityWebTestCase extends BaseProjectActivityWebTestCase { 
+
+  /**
+   * Returns information about these tests.
+   *
+   * @return mixed
+   */
+  function getInfo() {
+    return array(
+      'name' => t('Project activity'),
+      'description' => t('Test the basic functionality of Project Activity module.'),
+      'group' => t('Project Activity'),
+    );
+  }
+
+  /**
+   * Sets up each test.
+   */
+  function setUp() {
+    parent::setUp('project_activity_project', 'project', 'activity', 'views', 'token', 'trigger');
+    
+    $this->projectUser = $this->drupalCreateUser(array('access content', 'create full projects', 'create sandbox projects', 'administer projects', 'create page content', 'edit any page content', 'administer nodes'));
+    $this->secondUser = $this->drupalCreateUser(array('access content', 'create full projects', 'create sandbox projects', 'administer projects'));
+  }
+
+  /**
+   * Test if the creation of a project is properly logged.
+   */
+  function testProjectCreate() {
+    $hash = $this->createActivityTemplate('project', 'insert');
+    
+    // Login the user that's going to create a new project.
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // Create a new project
+    $project = $this->createProject($this->randomName(8), $this->randomName(16));
+    
+    // Assert activity has happened.
+    $this->assertActivity($user, $hash, $project, 0, 1);
+  }
+  
+  /**
+   * Test if the updating of a project is properly logged.
+   */
+  function testProjectUpdate() {
+    $hash = $this->createActivityTemplate('project', 'update');
+
+    // Login the user that's going to create the project
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // Create the project
+    $title = $this->randomName(8);
+    $project = $this->createProject($title, $this->randomName(16));
+    
+    // Login the user that's going to edit the project
+    $this->drupalLogin($this->secondUser);
+    $user = $this->getUrl();
+    
+    // Update the project
+    $this->updateProject($project, $title, $this->randomName(16));
+    
+    // Assert activity has happened.
+    $this->assertActivity($user, $hash, $project, 0, 1);
+  }
+  
+  /**
+   * Test if the adding of maintainers to a project is properly logged.
+   */
+  function testProjectMaintainerNew() {
+    $hash = $this->createActivityTemplate('project', 'maintainer_new');
+    
+    // Create maintainer
+    $maintainer = $this->drupalCreateUser();
+    $user_maintainer = url('user/'. $maintainer->uid, array('absolute' => TRUE));
+
+    // Login the user that's going to make $maintainer a new maintainer.
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // Create the project
+    $project = $this->createProject($this->randomName(8), $this->randomName(16));
+    
+    // Add $maintainer as a new maintainer
+    $this->addMaintainer($project, $maintainer->name);
+    
+    // Assert activity has happened.
+    // When a new project gets created, the creator is automatically assigned as a maintainer.
+    $this->assertActivity($user, $hash, $project, 0, 2);
+    $this->assertActivity($user, $hash, $project, 1, 2);
+  }
+  
+  /**
+   * Test if promoting a sandbox project is properly logged.
+   */
+  function testProjectPromoteSandbox() {
+    $hash = $this->createActivityTemplate('project', 'promote_sandbox');
+
+    // Login the user that's going to create the project.
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // Create the project
+    $title = $this->randomName(8);
+    $project = $this->createProject($title, $this->randomName(16), TRUE);
+    
+    // Login the user that's going to promote the sandbox.
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // We promote the sandbox.
+    $this->promoteSandbox($project, $title);
+    
+    // Assert activity has happened.
+    $this->assertActivity($user, $hash, $project, 0, 1);
+  }
+  
+  /**
+   * Test the Project token integration (custom).
+   */
+  function testCustomTokens() {
+    // Create maintainer
+    $maintainer = $this->drupalCreateUser();
+    $user_maintainer = url('user/'. $maintainer->uid, array('absolute' => TRUE));
+
+    // Login the user that's going to create the project
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // Create the project
+    $project = $this->createProject($this->randomName(8), $this->randomName(16));
+    
+    $message = '[project_maintainer]';
+    $this->_createActivityTemplate('project', 'maintainer_new', $message);
+    
+    // Login the user that's going to make $maintainer a new maintainer.
+    $this->drupalLogin($this->projectUser);
+    
+    // Add $maintainer as a new maintainer
+    $this->addMaintainer($project, $maintainer->name);
+    
+    // Assert activity has happened.
+    $this->_assertActivity($maintainer->name, 0, 1);
+  }
+  
+  /**
+   * Following test is there to make sure that we ONLY track Projects and not just every regular node.
+   *
+   * To only check one action is enough, as they are all handled by the same function (hook_nodeapi).
+   */
+  function testNodeUpdate() {
+    $hash = $this->createActivityTemplate('project', 'update');
+    
+    // Login the user that's going to create a new page.
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // Create a new page node.
+    $title = $this->randomName(8);
+    $page = $this->createPage($title, $this->randomName(16));
+    
+    // Update the page node.
+    $this->updatePage($page, $title, $this->randomName(16));
+    
+    // Assert activity has NOT happened.
+    $this->assertActivity(NULL, $hash, $page, 0, 0);
+  }
+  
+  /**
+   * Test if the activity options are correctly handled.
+   */
+  function testProjectOption() {
+    $options = array('project', 'sandbox');
+    
+    // First we create all possible scenario's (powerset of $options).
+    $testcases = array(array());
+    foreach ($options as $option) {
+      foreach ($testcases as $combination) {
+        array_push($testcases, array_merge(array($option), $combination));
+      }
+    }
+    
+    // Now we try every possible scenario.
+    foreach ($testcases as $testcase) {
+      // First we create an activity template.
+      $hash = $this->createActivityTemplate('project', 'insert', $testcase);
+      
+      // Determine which options shouldn't be logged.
+      $anti_testcase = array_diff($options, $testcase);
+      
+      // Calling createActivityTemplate always logs in a user with only Activity creation permissions.
+      // We log in our regular user.
+      $this->drupalLogin($this->projectUser);
+      $user = $this->getUrl();
+      
+      // Variable to count amount of activity.
+      $activity_count = 0;
+      
+      // And now we perform actions.
+      foreach ($testcase as $option) {
+        // Produce some activity.
+        $project = $this->createProject($this->randomName(8), $this->randomName(16), ($option == 'sandbox'));
+        $activity_count ++;
+        
+        // Also create issues that shouldn't be logged.
+        foreach ($anti_testcase as $anti_option) {
+          $this->createProject($this->randomName(8), $this->randomName(16), ($anti_option == 'sandbox'));
+        }
+        
+        // Assert activity has happened.
+        $this->assertActivity($user, $hash, $project, 0, $activity_count);
+      }
+      
+      $this->clearAllActivityTemplates(1, TRUE);
+    }
+  }
+  
+  /**
+   * Tests if regeneration works correctly.
+   */
+  function testRegeneration() {
+    $this->createActivityTemplate('project', 'insert');
+    $this->createActivityTemplate('project', 'maintainer_new');
+    
+    // Login the user that's going to create a new project.
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // Create a new project
+    for ($i = 0; $i < 10; $i ++) {
+      $project_title = $this->randomName(8);
+      $project = $this->createProject($project_title, $this->randomName(16), ($i % 2));
+      
+      if ($i % 2) {
+        $this->promoteSandbox($project, $project_title);
+      }
+      
+      if ($i % 3) {
+        $this->updateProject($project, $project_title, $this->randomName(16));
+      }
+      
+      if ($i % 4) {
+        $maintainer = $this->drupalCreateUser();
+        
+        $this->addMaintainer($project, $maintainer->name);
+      }
+    }
+    
+    $this->regenerate();
+  }
+
+}
+
diff --git a/activity/tests/project_activity_release.test b/activity/tests/project_activity_release.test
new file mode 100644
index 0000000..3063381
--- /dev/null
+++ b/activity/tests/project_activity_release.test
@@ -0,0 +1,216 @@
+<?php
+
+/**
+ * @file: Provide tests for Project Activity module.
+ */
+ 
+include_once drupal_get_path('module', 'project_activity_project') . '/tests/project_activity_testcase.test';
+
+class ProjectReleaseActivityWebTestCase extends BaseProjectActivityWebTestCase {
+
+  /**
+   * Returns information about these tests.
+   *
+   * @return mixed
+   */
+  function getInfo() {
+    return array(
+      'name' => t('Project Release activity'),
+      'description' => t('Test the basic functionality of Project Release Activity module.'),
+      'group' => t('Project Activity'),
+    );
+  }
+
+  /**
+   * Sets up each test.
+   */
+  function setUp() {
+    parent::setUp('project_activity_release', 'project_activity_project', 'project_release', 'project', 'views', 'activity', 'token', 'trigger');
+    
+    // Create the user that's going to create the project
+    $this->projectUser = $this->drupalCreateUser(array('access content', 'create full projects', 'administer nodes', 'administer projects'));
+    
+    // Login the user
+    $this->drupalLogin($this->projectUser);
+    
+    // Create a new project
+    $this->project_title = $this->randomName(8);
+    $this->project = $this->createProject($this->project_title, $this->randomName(16));
+  }
+  
+  /**
+   * Test if the creation of a project release is properly logged.
+   */
+  function testProjectReleaseCreate() {
+    $hash = $this->createActivityTemplate('project_release', 'insert');
+    
+    // Login the user that's going to create a new release
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // Create a new project release
+    $this->createProjectRelease($this->project, $this->project_title, $this->randomName(16));
+    
+    // Assert activity has happened.
+    $this->assertActivity($user, $hash, $this->project, 0, 1);
+  }
+  
+  /**
+   * Test if the updating of a project release is properly logged.
+   */
+  function testProjectReleaseUpdate() {
+    $hash = $this->createActivityTemplate('project_release', 'update');
+    
+    // Login the user that's going to create a new release.
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // Create a new project release.
+    $release = $this->createProjectRelease($this->project, $this->project_title, $this->randomName(16));
+    
+    // Update the project release.
+    $this->updateProjectRelease($release, $this->project_title, $this->randomName(16));
+    
+    // Assert activity has happened.
+    $this->assertActivity($user, $hash, $this->project, 0, 1);
+  }
+  
+  /**
+   * Test if the deletion of a project release is properly logged.
+   */
+  function testProjectReleaseDelete() {
+    $hash = $this->createActivityTemplate('project_release', 'delete');
+    
+    // Login the user that's going to create a new release.
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // Create a new project release.
+    $release = $this->createProjectRelease($this->project, $this->project_title, $this->randomName(16));
+    
+    // Update the project release.
+    $this->deleteProjectRelease($release, $this->project_title);
+    
+    // Assert activity has happened.
+    $this->assertActivity($user, $hash, $this->project, 0, 1);
+  }
+  
+  /**
+   * Test if packaging of a project release is properly logged.
+   */
+  function testProjectReleaseCreatePackage() {
+    $hash = $this->createActivityTemplate('project_release', 'create_package');
+    
+    // Login the user that's going to create a new release.
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // Create a new project release.
+    $release = $this->createProjectRelease($this->project, $this->project_title, $this->randomName(16));
+    
+    // Update the project release.
+    $this->createPackageProjectRelease($release);
+    
+    // Assert activity has happened.
+    $this->assertActivity($user, $hash, $this->project, 0, 1);
+  }
+  
+  /**
+   * Test the Project Release token integration (standard).
+   */
+  function testProjectReleaseTokens() {
+    $message = '[project_release_pid]	[project_release_project_title]	[project_release_project_title-raw]	[project_release_project_shortname]	[project_release_version]	[project_release_version_major]	[project_release_version_minor]	[project_release_version_patch]	[project_release_version_extra]';
+    
+    $this->_createActivityTemplate('project_release', 'insert', $message);
+    
+    // Login the user that's going to create a new release.
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // Create a new project release.
+    $major = rand(1, 9);
+    $minor = rand(1, 9);
+    $patch = rand(1, 9);
+    
+    // Create a new project release with the randomly generated version number.
+    $release = $this->createProjectRelease($this->project, $this->project_title, $this->randomName(16), $major, $minor, $patch);
+    
+    // Load the release as a node and do token replacement.
+    $node = node_load($this->getIdentifier($release), NULL, TRUE);
+    $render = token_replace_multiple($message, array('node' => $node), '[', ']', array(), TRUE);
+    
+    // Assert activity has happened.
+    $this->_assertActivity($render, 0, 1);
+  }
+  
+  /**
+   * Test the Project Release token integration (custom).
+   */
+  function testCustomTokens() {
+    $message = '[project_release]';
+    
+    $this->_createActivityTemplate('project_release', 'insert', $message);
+    
+    // Login the user that's going to create a new release.
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // Create a new project release.
+    $major = rand(1, 9);
+    $minor = rand(1, 9);
+    $patch = rand(1, 9);
+    
+    // Create a new project release with the randomly generated version number.
+    $release = $this->createProjectRelease($this->project, $this->project_title, $this->randomName(16), $major, $minor, $patch);
+    
+    // Load the release as a node and do token replacement.
+    $node = node_load($this->getIdentifier($release), NULL, TRUE);
+    $render = token_replace_multiple($message, array('project_release' => $node), '[', ']', array(), TRUE);
+    
+    // Assert activity has happened.
+    $this->_assertActivity($render, 0, 1);
+  }
+  
+  /**
+   * Following test is there to make sure that we ONLY track Project Releases and not just every regular node.
+   *
+   * To only check one action is enough, as they are all handled by the same function (hook_nodeapi).
+   */
+  function testNodeUpdate() {
+    $hash = $this->createActivityTemplate('project_release', 'update');
+    
+    // Login the user that's going to create a new page.
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // Create a new page node.
+    $title = $this->randomName(8);
+    $page = $this->createPage($title, $this->randomName(16));
+    
+    // Update the page node.
+    $this->updatePage($page, $title, $this->randomName(16));
+    
+    // Assert activity has NOT happened.
+    $this->assertActivity(NULL, $hash, $page, 0, 0);
+  }
+  
+  /**
+   * Tests if regeneration works correctly.
+   */
+  function testRegeneration() {
+    $hash = $this->createActivityTemplate('project_release', 'insert');
+    
+    // Login the user that's going to create a new release.
+    $this->drupalLogin($this->projectUser);
+    $user = $this->getUrl();
+    
+    // Create a new release
+    for ($i = 0; $i < 10; $i ++) {
+      $release_title = $this->randomName(8);
+      $release = $this->createProjectRelease($this->project, $this->project_title, $release_title, $i);
+    }
+    
+    $this->regenerate();
+  }
+  
+}
\ No newline at end of file
diff --git a/activity/tests/project_activity_testcase.test b/activity/tests/project_activity_testcase.test
new file mode 100644
index 0000000..97c0895
--- /dev/null
+++ b/activity/tests/project_activity_testcase.test
@@ -0,0 +1,408 @@
+<?php
+
+/**
+ * @file: Provide common testmethods for Project Activity modules.
+ */
+
+class BaseProjectActivityWebTestCase extends DrupalWebTestCase {
+
+  /**
+   * Returns information about these tests.
+   *
+   * @return mixed
+   */
+  public function setUp() {
+    // Dissable Autoload if it was enabled before running tests.
+    spl_autoload_unregister('autoload_class');
+    spl_autoload_unregister('autoload_interface');
+    
+    node_load(NULL, NULL, TRUE);
+    
+    call_user_func_array(array('BaseProjectActivityWebTestCase', 'parent::setUp'), func_get_args());
+  }
+
+  /**
+   * Creates an activity template for the specified type and operation.
+   * Returns an unique hash that identifies the template.
+   *
+   * @param  $type  string  The type of the activity template.
+   * @param  $operation  string  The operation we need to create a template for.
+   * @param  $types  mixed  The activity types that have to be selected.
+   *
+   * @return  string  An unique hash to identify the activity template.
+   */
+  protected function createActivityTemplate($type, $operation, $types = array(), $object_type = NULL) {
+    $hash = md5(print_r($types, TRUE) . $type . $operation);
+    
+    $this->_createActivityTemplate($type, $operation, '[account-url]'. $hash .'[node-url]', $types, $object_type);
+    
+    return $hash;
+  }
+  
+  /**
+   * Creates an activity template for the specified type and operation.
+   *
+   * @param  $type  string  The type of the activity template.
+   * @param  $operation  string  The operation we need to create a template for.
+   * @param  $message  string  The message we want to record when the activity occurs.
+   * @param  $options  mixed  The activity options that have to be selected.
+   */
+  protected function _createActivityTemplate($type, $operation, $message, $options = array(), $object_type = NULL) {
+    $activityUser = $this->drupalCreateUser(array('administer activity'));
+    $this->drupalLogin($activityUser);
+    
+    $edit = array();
+    $edit['hook'] = $type;
+    $this->drupalPost('admin/build/activity/create', $edit, t('Continue'));
+    
+    // fill out our configurable action form and post
+    $edit = array();
+    $edit['operation'] = $operation;
+    
+    // activity options
+    foreach ($options as $option) {
+      $edit['activity_types['. $option .']'] = '1';
+    }
+    
+    if (empty($object_type)) {
+      $object_type = $type;
+    }
+    
+    $this->drupalPost(NULL, $edit, t('Continue'));
+    
+    // fill out our configurable action form and post
+    $edit = array();
+    $edit['everyone-pattern-en'] = $message;
+    $edit[$object_type .'-pattern-en'] = $edit['everyone-pattern-en'];    
+    $this->drupalPost(NULL, $edit, t('Save'));
+    
+    $this->assertText(t('Saved.'));
+  }
+  
+  /**
+   * Deletes all activity templates.
+   *
+   * @param  integer  The expected amount of templates.
+   * @param  boolean  Wheter or not the existing activity messages belonging to each template should be removed.
+   */
+  protected function clearAllActivityTemplates($expected_amount, $delete_existing = FALSE) {
+    $activityUser = $this->drupalCreateUser(array('administer activity'));
+    $this->drupalLogin($activityUser);
+    
+    $this->drupalGet('admin/build/activity');
+    
+    for ($activity_count = 1; $activity_count <= $expected_amount; $activity_count ++) {
+      $this->clickLink(t('delete'));
+      
+      $edit = array();
+      
+      if ($delete_existing) {
+        $edit['delete_existing'] = '1';
+      }
+      
+      $this->drupalPost(NULL, $edit, t('Delete'));
+    }
+    
+  }
+  
+  /**
+   * Asserts that a given activity message has been logged.
+   *
+   * @param  $user  string  Absolute URL to user profile.
+   * @param  $hash  string  Hash received from the createActivityTemplate function.
+   * @param  $node  string  Absolute URL to node.
+   * @param  $index  integer  The location of the activity in the activity log; lower is sooner.
+   * @param  $amount  integer  The total amount of activity messages expected.
+   * @param  $display  string  The display that needs to be shown.
+   *
+   * ... any additional parameters will be passed as arguments to the view.
+   */
+  protected function assertActivity($user, $hash, $node, $index = 0, $amount = 0, $display_id = NULL) {
+    $args = array_slice(func_get_args(), 6);
+    array_unshift($args, 'all_activity', $display_id);
+    
+    $activity = call_user_func_array('views_get_view_result', $args);
+    
+    if (!empty($activity)) {
+      $this->assertEqual($activity[$index]->activity_messages_message, $user . $hash . $node);
+    } 
+    else {
+      $this->assertEqual($user, NULL);
+    }
+    
+    if ($amount) {
+        $this->assertEqual(count($activity), $amount);
+    }
+  }
+  
+  /**
+   * Regenerates Activity and asserts that it's the same as before.
+   */
+  protected function regenerate() {
+    $activityUser = $this->drupalCreateUser(array('administer activity'));
+    $this->drupalLogin($activityUser);
+    
+    $before = views_get_view_result('all_activity');
+    
+    $this->drupalGet('admin/build/activity');
+    $urls = $this->xpath('//a[normalize-space(text())=:label]', array(':label' => t('regenerate')));
+    
+    for ($index = 0; $index < count($urls); $index ++) {
+      $this->drupalGet('admin/build/activity');
+      $this->clickLink(t('regenerate'), $index);
+    }
+    
+    $after = views_get_view_result('all_activity');
+    
+    $this->assertEqual(count($after), count($before));
+    
+    foreach ($after as &$activity) {
+      $activity = $activity->activity_messages_message;
+    }
+    
+    foreach ($before as &$activity) {
+      $activity = $activity->activity_messages_message;
+    }
+    
+    for ($i = 0; $i < count($before); $i ++) {
+      $x = array_search($before[$i], $after);
+      unset($after[$x]);
+      
+      $this->assertTrue(($x !== FALSE), t('Activity successfully regenerated'));
+    }
+  }
+  
+  /**
+   * Asserts that a given activity message has been logged.
+   *
+   * @param  $message  string  The message we need to assert.
+   * @param  $index  integer  The location of the activity in the activity log; lower is sooner.
+   * @param  $amount  integer  The total amount of activity messages expected.
+   * @param  $display  string  The display that needs to be shown.
+   *
+   * ... any additional parameters will be passed as arguments to the view.
+   */
+  protected function _assertActivity($message, $index = 0, $amount = 0, $display_id = NULL) {
+    $args = array_slice(func_get_args(), 4);
+    array_unshift($args, 'all_activity', $display_id);
+    
+    $activity = call_user_func_array('views_get_view_result', $args);
+    
+    if (!empty($activity)) {
+      $this->assertEqual($activity[$index]->activity_messages_message, $message);
+    } 
+    else {
+      $this->assertEqual($message, NULL);
+    }
+    
+    if ($amount) {
+        $this->assertEqual(count($activity), $amount);
+    }
+  }
+  
+  /**
+   * Creates a regular page node manually.
+   *
+   * @param  $title  string  The title of the node.
+   * @param  $body  string  The body of the node.
+   *
+   * @return  string  Absolute URL to the created project.
+   */
+  protected function createPage($title, $body) {
+    $edit = array();
+    $edit['title'] = $title;
+    $edit['body'] = $body;
+    
+    $this->drupalPost('node/add/page', $edit, t('Save'));
+    
+    $this->assertText(t('Page @title has been created.', array('@title' => $title)));
+    
+    return $this->getUrl();
+  }
+  
+  /**
+   * Updates a regular page node manually.
+   *
+   * @param  $page  string  URL to the page node.
+   * @param  $title  string  The title of the page.
+   * @param  $body  string  The new body of the page.
+   */
+  protected function updatePage($page, $title, $body) {
+    $edit = array();
+    $edit['body'] = $body;
+    
+    $this->drupalPost($page .'/edit', $edit, t('Save'));
+    
+    $this->assertText(t('Page @title has been updated.', array('@title' => $title)));
+  }
+  
+  /**
+   * Creates a project-project node manually.
+   *
+   * @param  $title  string  The title of the project.
+   * @param  $body  string  The body of the project.
+   * @param  $sandbox  boolean  Wheter or not the project should be a sandbox.
+   *
+   * @return  string  Absolute URL to the created project.
+   */
+  protected function createProject($title, $body, $sandbox = FALSE) {
+    $edit = array();
+    $edit['title'] = $title;
+    $edit['body'] = $body;
+    $edit['project[uri]'] = $edit['title'];
+    
+    if ($sandbox) {
+        $edit['project[sandbox]'] = '1';
+    }
+    
+    $this->drupalPost('node/add/project-project', $edit, t('Save'));
+    
+    $this->assertText(t('Project @title has been created.', array('@title' => $title)));
+    
+    return $this->getUrl();
+  }
+  
+  /**
+   * Updates a project-project node manually.
+   *
+   * @param  $project  string  URL to the project node.
+   * @param  $title  string  The title of the project.
+   * @param  $body  string  The new body of the project.
+   */
+  protected function updateProject($project, $title, $body) {
+    $edit = array();
+    $edit['body'] = $body;
+    
+    $this->drupalPost($project .'/edit', $edit, t('Save'));
+    
+    $this->assertText(t('Project @title has been updated.', array('@title' => $title)));
+  }
+  
+  /**
+   * Adds a maintainer to a project manually.
+   *
+   * @param  $project  string  URL to the project node.
+   * @param  $maintainer  string  The username of the maintainer.
+   */
+  protected function addMaintainer($project, $maintainer_name) {
+    $edit = array();
+    $edit['new_maintainer[user]'] = $maintainer_name;
+    
+    $this->drupalPost($project .'/maintainers', $edit, t('Update'));
+    
+    $this->assertText($maintainer_name);
+  }
+  
+  /**
+   * Promotes a sandbox manually.
+   *
+   * @param  $project  string  URL to the project node.
+   * @param  $title  string  The title of the project.
+   */
+  protected function promoteSandbox($project, $title) {
+    $edit = array();
+    $edit['confirm'] = '1';
+    
+    $this->drupalPost($project .'/edit/promote', $edit, t('Promote to full project'));
+    
+    $this->assertText(t('Once a project is promoted, it can not be turned back into a sandbox.'));
+    
+    $this->drupalPost(NULL, array(), t('Promote'));
+    
+    $this->assertText(t('The project @title has been promoted to a full project.', array('@title' => $title)));
+  }
+  
+  /**
+   * Returns the last crumb from a string seperated by '/'.
+   *
+   * @param  $url  string  The URL that needs to be parsed.
+   *
+   * @return  string  The last crumb.
+   */
+  protected function getIdentifier($url) {
+    $exploded = explode('/', $url);
+    
+    return array_pop($exploded);
+  }
+  
+  /**
+   * Creates a project-release node manually.
+   *
+   * @param  $project  string  URL to the project to which the release should belong.
+   * @param  $title  string  The title of the project.
+   * @param  $body  string  The body of the release.
+   * @param  $major  integer  The major version number of the release.
+   * @param  $minor  integer  The minor version number of the release.
+   * @param  $patch  integer  The patch version number of the release.
+   *
+   * @return  string  Absolute URL to the created release.
+   */
+  protected function createProjectRelease($project, $title, $body, $major = NULL, $minor = NULL, $patch = NULL) {
+    $edit = array();
+    $edit['body'] = $body;
+    
+    $version = '';
+    
+    if (!empty($major)) {
+      $edit['project_release[version_major]'] = $major;
+      $version = $major;
+    }
+    
+    if (!empty($minor)) {
+      $edit['project_release[version_minor]'] = $minor;
+      $version = $major .'.'. $minor;
+    }
+    
+    if (!empty($patch)) {
+      $edit['project_release[version_patch]'] = $patch;
+      $version = $major .'.'. $minor .'.'. $patch;
+    }
+    
+    $this->drupalPost('node/add/project-release/'. $this->getIdentifier($project), $edit, t('Save'));
+    
+    $this->assertText(t('Project release @title @version has been created.', array('@title' => $title, '@version' => $version)));
+    
+    return $this->getUrl();
+  }
+  
+  /**
+   * Updates a project-release node manually.
+   *
+   * @param  $project_release  string  URL to the project release.
+   * @param  $title  string  The title of the project release.
+   * @param  $body  string  The new body of the release.
+   */
+  protected function updateProjectRelease($project_release, $title, $body) {
+    $edit = array();
+    $edit['body'] = $this->randomName(16);
+    
+    $this->drupalPost($project_release .'/edit', $edit, t('Save'));
+    
+    $this->assertText(t('Project release @title  has been updated.', array('@title' => $title)));
+  }
+  
+  /**
+   * Deletes a project-release node manually.
+   *
+   * @param  $project_release  string  URL to the project release.
+   * @param  $title  string  The title of the project release.
+   */
+  protected function deleteProjectRelease($project_release, $title) {
+    $this->drupalPost($project_release .'/delete', array(), t('Delete'));
+    
+    $this->assertText(t('Project release @title  has been deleted.', array('@title' => $title)));
+  }
+  
+  /**
+   * Packages a project-release node manually.
+   *
+   * @param  $project_release  string  URL to the project release.
+   */
+  protected function createPackageProjectRelease($project_release) {
+    $project = node_load($this->getIdentifier($this->project));
+    $project_release = node_load($this->getIdentifier($project_release));
+    
+    project_activity_release_project_release_create_package($project, $project_release);
+  }
+  
+}
\ No newline at end of file
