? 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,53 @@
+<?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} (
+          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',
+  );
+  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_discard_after');
+}

--- usage/project_usage.module
+++ usage/project_usage.module
@@ -0,0 +1,229 @@
+<?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 week.
+define('PROJECT_USAGE_WEEK', 60 * 60 * 24 * 7);
+// Number of seconds in a year.
+define('PROJECT_USAGE_YEAR', 60 * 60 * 24 * 356);
+
+/**
+ * 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;
+}
+
+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_discard_after'] = array(
+    '#type' => 'select',
+    '#title' => t('Discard usage data after'),
+    '#default_value' => variable_get('project_usage_discard_after', PROJECT_USAGE_YEAR),
+    '#options' => $age_options,
+    '#description' => t("Discard the daily usage data after this amount of time has passed. This does not affect the weekly usage tallies."),
+  );
+  return system_settings_form($form);
+}
+
+/**
+ * Implementation of hook_cron().
+ * 
+ * Call the daily and weekly processing tasks as needed.
+ */
+function project_usage_cron() {
+  $time = time();
+
+  // Figure out if it's been 24 hours since our last daily processing.
+  if (variable_get('project_usage_last_daily', 0) <= ($time - (60 * 60 * 24))) {
+    project_usage_process_day();
+    variable_set('project_usage_last_daily', $time);
+  }
+
+  // Figure out if we've processed the last weeks data.
+  $week = date('Y.W', $time);
+  if (variable_get('project_usage_last_weekly', 0) <= $week) {
+    project_usage_process_week();
+    variable_set('project_usage_last_weekly', $week);
+  }
+}
+
+/**
+ * Process the previous day's data. 
+ * 
+ * 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_day() {
+  watchdog('project_usage', t('Starting to process daily usage data.'));
+
+  // Timestamp for begining of the previous day.
+  $timestamp = project_usage_timestamp_days_ago(1);
+
+  // 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);
+
+  watchdog('project_usage', t('Completed daily usage data processing.'));
+}
+
+/**
+ * Compute the weekly summaries for the previous week.
+ */
+function project_usage_process_week() {
+  watchdog('project_usage', t('Starting to process weekly usage data.'));
+
+  // Remove old data.
+  $since = time() - variable_get('project_usage_discard_after', PROJECT_USAGE_YEAR); 
+  db_query("DELETE FROM {project_usage_day} WHERE timestamp < %d", $since);
+
+  // Determine the beginning and end of the prior week...
+  $range = project_usage_get_week_range(1);
+  // ...remove any existing counts...
+  db_query("DELETE FROM {project_usage_week} WHERE timestamp = %d", $range['start']);
+  // ...then recompute the total counts.
+  db_query("INSERT INTO {project_usage_week} (nid, timestamp, count) SELECT pid, %d, COUNT(DISTINCT site_key) FROM {project_usage_day} WHERE timestamp >= %d AND timestamp < %d AND pid <> 0 GROUP BY pid", $range['start'], $range['start'], $range['end']);
+  db_query("INSERT INTO {project_usage_week} (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", $range['start'], $range['start'], $range['end']);
+
+  watchdog('project_usage', t('Completed weekly usage data processing.'));
+}
+
+/**
+ * Compute the timestamp of a day.
+ * 
+ * @param $days
+ *   An interger specifying how many days to go back, 0 = today, 1 = yesterday.
+ * @return
+ *   UNIX timestamp with the begining of that day. 
+ */
+function project_usage_timestamp_days_ago($days = 1, $timestamp = NULL) {
+  $d = getdate(isset($timestamp) ? $timestamp : time());
+  return mktime(0, 0, 0, $d['mon'], $d['mday'] - $d['wday'] - $days, $d['year']);
+  
+}
+
+/**
+ * Compute the beginning and end a week containing a given timestamp.
+ * 
+ * @param $weeks
+ *   Integer specfying the number of weeks to go back.
+ * @param $timestamp
+ *   UNIX timestamp.
+ */
+function project_usage_get_week_range($weeks = 1, $timestamp = NULL) {
+  $d = getdate(isset($timestamp) ? $timestamp : time());
+  return array(
+    'start' => mktime(0, 0, 0, $d['mon'], $d['mday'] - $d['wday'] - (7 * $weeks), $d['year']),
+    'end' => mktime(0, 0, 0, $d['mon'], $d['mday'] - $d['wday'], $d['year']),
+  );
+}
+
+/**
+ * Implementation of the metric's module's hook_metrics_functions().
+ */
+function project_usage_metrics_functions() {
+  return array(
+    '_project_usage_weekly_log',
+  );
+}
+
+/**
+ * Metric to determine a project or release node node's usage.
+ */
+function _project_usage_weekly_log($op, $options = NULL, $node = NULL) {
+  switch ($op) {
+    case 'info':
+      return array(
+        'name' => t('Project usage: weekly usage'),
+        'description' => t("Returns a score based on the number of sites using a project or release."),
+      );
+
+    case 'compute':
+      $weeks = isset($options['weeks']) ? $options['weeks'] : 1;
+      $range = project_usage_get_week_range(time(), $weeks);
+      $result = db_query("SELECT SUM(count) FROM {project_usage_week_project} WHERE nid = %d AND timestamp >= %d AND timestamp < %d", $node->nid, $range['start'], $range['end']);
+      if ($count = db_result($result)) {
+        return array(
+          'value' => log($result),
+          'description' => t('This project is used on !sites in the last !weeks.', array(
+            '!sites' => format_plural($count, '1 site', '@count sites'),
+            '!weeks' => format_plural($weeks, '1 week', '@count weeks'),
+          )),
+        );
+      }
+      return NULL;
+
+    case 'options':
+      $weeks = drupal_map_assoc(array(1, 2, 3, 4, 6, 8, 10, 20, 26, 52));
+      $form['weeks'] = array(
+        '#type' => 'select',
+        '#title' => t('Weeks'),
+        '#default_value' => isset($options['weeks']) ? $options['weeks'] : 1,
+        '#options' => $weeks,
+      );
+      return $form;
+  }
+}

