? usage
Index: release/project-release-serve-history.php
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project/release/project-release-serve-history.php,v
retrieving revision 1.4
diff -u -r1.4 project-release-serve-history.php
--- release/project-release-serve-history.php	26 Jul 2007 22:28:33 -0000	1.4
+++ release/project-release-serve-history.php	1 Aug 2007 05:01:02 -0000
@@ -79,13 +79,29 @@
 echo file_get_contents($full_path);
 
 
-/**
- * @todo: Record usage statistics?
+// Record usage statistics.
 if (isset($_GET['sitekey'])) {
-  if (isset($_GET['version'])) {
+  include_once './includes/bootstrap.inc';
+  drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
+
+  // We can't call module_exists without bootstrapping to a higher level so 
+  // we'll settle for checking that the table exists.
+  if (db_table_exists('project_usage_raw')) {
+    $site_key = $_GET['sitekey'];
+    $project_version = isset($_GET['version']) ? $_GET['version'] : '';
+
+    // Compute a timestamp for the begining of the day.
+    $time_parts = getdate();
+    $timestamp = mktime(0, 0, 0, $time_parts['mon'], $time_parts['mday'], $time_parts['year']);
+
+    if (db_result(db_query("SELECT COUNT(*) FROM {project_usage_raw} WHERE project_uri = '%s' AND timestamp = %d AND site_key = '%s'", $project_name, $timestamp, $site_key))) { 
+      db_query("UPDATE {project_usage_raw} SET api_version = '%s', project_version = '%s' WHERE project_uri = '%s' AND timestamp = %d AND site_key = '%s'", $api_version, $project_version, $project_name, $timestamp, $site_key);
+    }
+    else {
+      db_query("INSERT INTO {project_usage_raw} (project_uri, timestamp, site_key, api_version, project_version) VALUES ('%s', %d, '%s', '%s', '%s')", $project_name, $timestamp, $site_key, $api_version, $project_version);
+    }
   }
 }
-*/
 
 
 /**
--- usage/project_usage.info
+++ usage/project_usage.info
@@ -0,0 +1,5 @@
+; $Id$
+name = Project usage
+description = "Provides data about installed usage of projects (requires update_status.module clients connecting to this site)."
+package = Project
+dependencies = project project_release

--- usage/project_usage.install
+++ usage/project_usage.install
@@ -0,0 +1,63 @@
+<?php
+// $Id$
+// $Name$
+
+function project_usage_install() {
+  switch ($GLOBALS['db_type']) {
+    case 'mysql':
+    case 'mysqli':
+      db_query("CREATE TABLE IF NOT EXISTS {project_usage_raw} (
+          project_uri varchar(50) NOT NULL default '',          
+          timestamp int unsigned NOT NULL default '0',
+          site_key varchar(32) NOT NULL default '',
+          api_version varchar(32) NOT NULL default '',
+          project_version varchar(255) NOT NULL default '',
+          pid int unsigned NOT NULL default '0',
+          nid int unsigned NOT NULL default '0',
+          tid int unsigned NOT NULL default '0',
+          PRIMARY KEY (timestamp, project_uri, site_key)
+        ) /*!40100 DEFAULT CHARACTER SET utf8 */;");
+      db_query("CREATE TABLE IF NOT EXISTS {project_usage_day} (
+          timestamp int unsigned NOT NULL default '0',
+          site_key varchar(32) NOT NULL default '',
+          pid int unsigned NOT NULL default '0',
+          nid int unsigned NOT NULL default '0',
+          tid int unsigned NOT NULL default '0',
+          PRIMARY KEY (timestamp, site_key, pid)
+        ) /*!40100 DEFAULT CHARACTER SET utf8 */;");
+      db_query("CREATE TABLE IF NOT EXISTS {project_usage_week_project} (
+          nid int unsigned NOT NULL default '0',
+          timestamp int unsigned NOT NULL default '0',
+          tid int unsigned NOT NULL default '0',
+          count int unsigned NOT NULL default '0',
+          PRIMARY KEY (nid, timestamp, tid)
+        ) /*!40100 DEFAULT CHARACTER SET utf8 */;");
+      db_query("CREATE TABLE IF NOT EXISTS {project_usage_week_release} (
+          nid int unsigned NOT NULL default '0',
+          timestamp int unsigned NOT NULL default '0',
+          count int unsigned NOT NULL default '0',
+          PRIMARY KEY (nid, timestamp)
+        ) /*!40100 DEFAULT CHARACTER SET utf8 */;");
+      break;
+  }
+}
+
+function project_usage_uninstall() {
+  $tables = array(
+    'project_usage_raw',
+    'project_usage_day',
+    'project_usage_week_project',
+    'project_usage_week_release',
+  );
+  foreach ($tables as $table) {
+    if (db_table_exists($table)) {
+      db_query("DROP TABLE {$table}");
+    }
+  }
+
+  variable_del('project_usage_last_daily');
+  variable_del('project_usage_last_weekly');
+  variable_del('project_usage_life_daily');
+  variable_del('project_usage_life_weekly_project');
+  variable_del('project_usage_life_weekly_release');
+}

--- usage/project_usage.module
+++ usage/project_usage.module
@@ -0,0 +1,221 @@
+<?php
+// $Id$
+// $Name$
+
+/**
+ * @file
+ *
+ * This module provides logging of the requests sent by the 
+ * update_status.module (contrib in 5.x) and update.module (core in 6.x) to the 
+ * project_release.module on updates.drupal.org. The 
+ * release/project-release-serve-history.php script inserts data into the 
+ * {project_usage_raw} table created by this module.
+ * 
+ * On a daily basis the usage data is matched to project and release nodes
+ * and moved into the {project_usage_day} table. On a weekly basis the daily
+ * usage data is tallied and stored in the {project_usage_week} table.
+ * 
+ * This data is then used to compute live usage statistics about all projects 
+ * hosted on drupal.org. In theory, another site could setup
+ * update_status.module-style checking to their own project.module-based 
+ * server, in which case, they might want to enable this module. Otherwise, 
+ * sites should just leave this disabled.
+ */
+
+// Number of seconds in a day.
+define('PROJECT_USAGE_DAY', 60 * 60 * 24);
+// Number of seconds in a week.
+define('PROJECT_USAGE_WEEK', PROJECT_USAGE_DAY * 7);
+// Number of seconds in a year.
+define('PROJECT_USAGE_YEAR', PROJECT_USAGE_DAY * 365);
+
+/**
+ * Implementation of hook_menu().
+ */
+function project_usage_menu($may_cache) {
+  $items = array();
+  if ($may_cache) {
+    $items[] = array(
+      'path' => 'admin/project/project-usage-settings',
+      'title' => t('Project usage settings'),
+      'callback' => 'drupal_get_form',
+      'callback arguments' => array('project_usage_settings_form'),
+      'description' => t('Configure how long usage data is retained.'),
+      'weight' => 1,
+    );
+  }
+  return $items;
+}
+
+/**
+ * Module settings form.
+ */
+function project_usage_settings_form() {
+  $times = array(
+     3 * PROJECT_USAGE_YEAR,
+     2 * PROJECT_USAGE_YEAR,
+     1 * PROJECT_USAGE_YEAR,
+    26 * PROJECT_USAGE_WEEK,
+    12 * PROJECT_USAGE_WEEK,
+     8 * PROJECT_USAGE_WEEK,
+     4 * PROJECT_USAGE_WEEK,
+  );
+  $age_options = drupal_map_assoc($times, 'format_interval');
+  $form['project_usage_life_daily'] = array(
+    '#type' => 'select',
+    '#title' => t('Daily usage data lifespan'),
+    '#default_value' => variable_get('project_usage_life_daily', 4 * PROJECT_USAGE_WEEK),
+    '#options' => $age_options,
+    '#description' => t("Discard the daily usage data after this amount of time has passed."),
+  );
+  $form['project_usage_life_weekly_project'] = array(
+    '#type' => 'select',
+    '#title' => t('Weekly project data lifespan'),
+    '#default_value' => variable_get('project_usage_life_weekly_project', PROJECT_USAGE_YEAR),
+    '#options' => $age_options,
+    '#description' => t("Discard the weekly usage for project nodes after this amount of time has passed. "),
+  );
+  $form['project_usage_life_weekly_release'] = array(
+    '#type' => 'select',
+    '#title' => t('Weekly release data lifespan'),
+    '#default_value' => variable_get('project_usage_life_weekly_release', 26 * PROJECT_USAGE_WEEK),
+    '#options' => $age_options,
+    '#description' => t("Discard the weekly usage for project nodes after this amount of time has passed. "),
+  );
+  return system_settings_form($form);
+}
+
+/**
+ * Implementation of hook_cron().
+ * 
+ * Call the daily and weekly processing tasks as needed.
+ */
+function project_usage_cron() {
+  $now = time();
+
+  // Figure out if it's been 24 hours since our last daily processing.
+  if (variable_get('project_usage_last_daily', 0) <= ($now - PROJECT_USAGE_DAY)) {
+    project_usage_process_daily();
+    variable_set('project_usage_last_daily', $now);
+  }
+
+  // Figure out if it's been a week since we did weekly stats. If the weekly
+  // data has never been processed go back only as far as there is daily data.
+  $default = $now - variable_get('project_usage_life_daily', 4 * PROJECT_USAGE_WEEK);
+  $last_weekly = variable_get('project_usage_last_weekly', $default);
+  if ($last_weekly <= ($now - PROJECT_USAGE_WEEK)) {
+    project_usage_process_weekly($last_weekly);
+    variable_set('project_usage_last_weekly', $now); 
+  }
+}
+
+/**
+ * Process all the raw data up to the previous day. 
+ * 
+ * The primary key on the {project_usage_raw} table will prevent duplicate
+ * records provided we process them once the day is complete. If we pull them
+ * out too soon and the site checks in again they will be counted twice.
+ */
+function project_usage_process_daily() {
+  watchdog('project_usage', t('Starting to process daily usage data.'));
+
+  // Timestamp for begining of the previous day.
+  $d = getdate(isset($timestamp) ? $timestamp : time());
+  $timestamp = mktime(0, 0, 0, $d['mon'], $d['mday'] - 1, $d['year']);
+
+  // Assign API version term IDs.
+  $terms = array();
+  foreach (project_release_get_api_taxonomy() as $term) {
+    $terms[$term->tid] = $term->name;
+  }
+  $query = db_query("SELECT DISTINCT api_version FROM {project_usage_raw}");
+  while ($row = db_fetch_object($query)) {
+    $tid = array_search($row->api_version, $terms);
+    db_query("UPDATE {project_usage_raw} SET tid = %d WHERE api_version = '%s'", $tid, $row->api_version);
+  }
+  
+  // Asign project and release node IDs.
+  $query = db_query("SELECT DISTINCT project_uri, project_version FROM {project_usage_raw}");
+  while ($row = db_fetch_object($query)) {
+    $pid = db_result(db_query("SELECT pp.nid AS pid FROM {project_projects} pp WHERE pp.uri = '%s'", $row->project_uri));
+    if ($pid) {
+      $nid = db_result(db_query("SELECT prn.nid FROM {project_release_nodes} prn WHERE prn.pid = %d AND prn.version = '%s'", $pid, $row->project_version));
+      db_query("UPDATE {project_usage_raw} SET pid = %d, nid = %d WHERE project_uri = '%s' AND project_version = '%s'", $pid, $nid, $row->project_uri, $row->project_version);
+    }
+  }
+
+  // Move usage records with project node IDs into the daily table and remove
+  // the rest.
+  db_query("INSERT INTO {project_usage_day} (timestamp, site_key, pid, nid, tid) SELECT timestamp, site_key, pid, nid, tid FROM {project_usage_raw} WHERE timestamp < %d AND pid <> 0", $timestamp);
+  db_query("DELETE FROM {project_usage_raw} WHERE timestamp < %d", $timestamp);
+
+  // Remove old daily records.
+  $seconds = variable_get('project_usage_life_daily', 4 * PROJECT_USAGE_WEEK);
+  db_query("DELETE FROM {project_usage_day} WHERE timestamp < %d", time() - $seconds);
+
+  watchdog('project_usage', t('Completed daily usage data processing.'));
+}
+
+/**
+ * Compute the weekly summaries for the week starting at the given timestamp.
+ * 
+ * @param $timestamp
+ *   UNIX timestamp indicating the last time weekly stats were processed. 
+ */
+function project_usage_process_weekly($timestamp) {
+  watchdog('project_usage', t('Starting to process weekly usage data.'));
+  
+  // Get all the weeks since we last ran.
+  $weeks = project_usage_get_weeks_since($timestamp);
+  // Skip the last entry since it's the current, incomplete week.
+  $count = count($weeks) - 1;
+  for ($i = 0; $i < $count; $i++) {
+    $start = $weeks[$i];
+    $end = $weeks[$i + 1];
+
+    // Remove any existing tallies...
+    db_query("DELETE FROM {project_usage_week_project} WHERE timestamp = %d", $start);
+    db_query("DELETE FROM {project_usage_week_release} WHERE timestamp = %d", $start);
+    // ...then compute the tallies.
+    db_query("INSERT INTO {project_usage_week_project} (nid, timestamp, tid, count) SELECT pid, %d, tid, COUNT(DISTINCT site_key) FROM {project_usage_day} WHERE timestamp >= %d AND timestamp < %d AND pid <> 0 GROUP BY pid, tid", $start, $start, $end);
+    db_query("INSERT INTO {project_usage_week_release} (nid, timestamp, count) SELECT nid, %d, COUNT(DISTINCT site_key) FROM {project_usage_day} WHERE timestamp >= %d AND timestamp < %d AND nid <> 0 GROUP BY nid", $start, $start, $end);
+  }
+
+  // Remove any tallies that have aged out.
+  $now = time();
+  $project_life = variable_get('project_usage_life_weekly_project', PROJECT_USAGE_YEAR); 
+  $release_life = variable_get('project_usage_life_weekly_release', 26 * PROJECT_USAGE_WEEK); 
+  db_query("DELETE FROM {project_usage_week_project} WHERE timestamp < %d", $now - $project_life);
+  db_query("DELETE FROM {project_usage_week_release} WHERE timestamp < %d", $now - $release_life);
+  
+  watchdog('project_usage', t('Completed weekly usage data processing.'));
+}
+
+/**
+ * Compute the start of the weeks from a given timestamp up to the start of the
+ * curent week.
+ *
+ * @param $timestamp
+ *   UNIX timestamp. The first returned timestamp will be the beginning of the
+ *   week with this time in it.
+ * @return
+ *   An array of timestamps the first value is the week containing $timestamp.
+ *   Each subsequent value is the time stamp of the next week. The final value
+ *   contains the beginning of the current week.
+ */
+function project_usage_get_weeks_since($timestamp) {
+  $times = array();
+
+  // Compute the start of the current week so we know when to stop.
+  $d = getdate();
+  $this_week = mktime(0, 0, 0, $d['mon'], $d['mday'] - $d['wday'], $d['year']);
+
+  // Then compute all the weeks before that.
+  $d = getdate($timestamp);
+  $i = 0;
+  do {
+    $times[$i] = mktime(0, 0, 0, $d['mon'], $d['mday'] - $d['wday'] + (7 * $i++), $d['year']);  
+  } while ($times[$i] < $this_week);
+
+  return $times;
+}

