? _release
? test.patch
? 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 03:27:54 -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 on checking that the tables exist.
+  if (db_table_exists('project_usage_day')) {
+    $site_key = $_GET['sitekey'];
+    $project_version = isset($_GET['version']) ? $_GET['version'] : '';
+
+    // Compute a time stamp 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,58 @@
+<?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',
+          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',
+          PRIMARY KEY (pid, nid),
+          UNIQUE KEY (project_uri, timestamp, site_key)          
+        ) /*!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',
+          count int unsigned NOT NULL default '0',
+          PRIMARY KEY (nid, timestamp)
+        ) /*!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_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_discard_after');
+}

--- usage/project_usage.module
+++ usage/project_usage.module
@@ -0,0 +1,230 @@
+<?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 inserted data into the 
+ * tables created by this module. 
+ * 
+ * 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.
+ */
+
+define('PROJECT_USAGE_YEAR', 31536000);
+
+/**
+ * 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() {
+  $day = 60*60*24;
+  $week = 7 * $day;
+  $year = 356 * $day;
+  $times = array(3 * $year, 2 * $year, 1 * $year, 26 * $week, 12 * $week, 8 * $week, 4 * $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 raw 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() {
+  $cur_time = time();
+
+  // Figure out if it's been 24 hours since our last daily processing.
+  if (variable_get('project_usage_last_daily', 0) <= ($cur_time - (60 * 60 * 24))) {
+    project_usage_process_day();
+    variable_set('project_usage_last_daily', $cur_time);
+  }
+
+  // Figure out if we've processed the last weeks data.
+  $week = date('Y.W', $cur_time);
+  if (variable_get('project_usage_last_weekly', 0) <= $week) {
+    project_usage_process_week();
+    variable_set('project_usage_last_weekly', $week);
+  }
+}
+
+/**
+ * Clean up today's data.
+ */
+function project_usage_process_day() {
+  watchdog('project_usage', t('Starting to process daily usage data.'));
+
+  // Process the previous day. The primary key on the table will prevent
+  // duplicate records but if we pull them out and the site checks in again
+  // they'll be counted twice.
+  $timestamp = project_usage_timestamp_days_ago(1);
+
+  // Set project and release node IDs.
+  $query = db_query("SELECT DISTINCT project_uri, project_version, api_version FROM {project_usage_raw}");
+  while ($row = db_fetch_object($query)) {
+    $release = db_fetch_object(db_query("SELECT prn.pid, prn.nid FROM project_projects pp INNER JOIN project_release_nodes prn ON pp.nid = prn.pid WHERE pp.uri = '%s' AND prn.version = '%s'", $row->project_uri, $row->project_version));
+    if ($release) {
+      db_query("UPDATE {project_usage_raw} SET pid = %d, nid = %d WHERE project_uri = '%s' AND project_version = '%s'", $release->pid, $release->nid, $row->project_uri, $row->project_version);
+    }
+  }
+
+  // Copy usage with a project node ID into the daily log.  
+  db_query("INSERT INTO {project_usage_day} (timestamp, site_key, pid, nid) SELECT timestamp, site_key, pid, nid FROM project_usage_raw WHERE timestamp < %d AND pid <> 0", $timestamp);
+  
+  // Drop all the old records
+  db_query("DELETE FROM {project_usage_raw} WHERE timestamp < %d", $since);
+  
+  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_project} WHERE timestamp = %d", $range['start']);
+  db_query("DELETE FROM {project_usage_week_release} WHERE timestamp = %d", $range['start']);
+  // ...then recompute the total counts.
+  db_query("INSERT INTO {project_usage_week_project} (nid, timestamp, count) SELECT pid, %d, COUNT(DISTINCT site_key) FROM project_usage_day WHERE pid <> 0 AND timestamp >= %d AND timestamp < %d GROUP BY pid", $range['start'], $range['start'], $range['end']);
+  db_query("INSERT INTO {project_usage_week_release} (nid, timestamp, count) SELECT nid, %d, COUNT(DISTINCT site_key) FROM project_usage_day WHERE nid <> 0 AND timestamp >= %d AND timestamp < %d 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_project_weekly_log',
+    '_project_usage_release_weekly_log',
+  );
+}
+
+
+function _project_usage_project_weekly_log($op, $options = NULL, $node = NULL) {
+  switch ($op) {
+    case 'info':
+      return array(
+        'name' => t('Project usage: Project node, weekly usage'),
+        'description' => t("Returns a score based on the number of sites using a project."),
+      );
+
+    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 @count sites in the last @weeks.', array('@count' => $count, '@weeks' => $weeks)),
+        );
+      }
+      return NULL;
+
+    case 'options':
+      return _project_usage_week_option($options);
+  }
+}
+
+function _project_usage_release_weekly_log($op, $options = NULL, $node = NULL) {
+  switch ($op) {
+    case 'info':
+      return array(
+        'name' => t('Project usage: Release node, weekly usage'),
+        'description' => t("Returns a score based on the number of sites using a 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_release WHERE nid = %d AND timestamp >= %d AND timestamp < %d", $node->nid, $range['start'], $range['end']);
+      if ($count = db_result($result)) {
+        return array(
+          'value' => log($count), 
+          'description' => t('This project is used on @count sites in the last @weeks weeks.', array('@count' => $count, '@weeks' => $weeks)),
+        );
+      }
+      return NULL;
+
+    case 'options':
+      return _project_usage_week_option($options);
+  }
+}
+
+function _project_usage_week_option($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;
+}

