? usage
Index: release/project_release.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project/release/project_release.module,v
retrieving revision 1.35
diff -u -r1.35 project_release.module
--- release/project_release.module	14 Jul 2007 17:19:09 -0000	1.35
+++ release/project_release.module	16 Jul 2007 21:22:02 -0000
@@ -177,10 +177,29 @@
  *   Optionally, the major version to check for. If unspecified it will use
  *   the default major version as specified by the
  *   {project_release_default_version} table. This should usually be blank.
+ * @param $site_key
+ *   If the client has been configured to allow the server to log
+ *   anonymous usage data, the $site_key will be an md5 hash of the
+ *   site's baseurl and its private key, so we can uniquely but
+ *   anonymously identify the site for saving usage statistics.
  */
-function project_release_data($project = NULL, $api_version = NULL, $major_version = NULL) {
+function project_release_data($project = NULL, $api_version = NULL, $major_version = NULL, $site_key = NULL) {
   $data = array();
 
+  // Find the $tid for the API version specified.
+  // Stop processing when $api_version = $term; that's our tid.
+  // If we don't have a match, this should assume the most current.
+  $tids = project_release_compatibility_list();
+  foreach ($tids as $tid => $term) {
+    if ($api_version == $term) {
+      break;
+    }
+  }
+
+  if (!empty($site_key) && module_exists('project_usage')) {
+    project_usage_save_raw_query($site_key, $project, $tid);
+  }
+
   // Handle caching for 'all' projects
   if (!is_array($project) && $project == 'all') {
     $cid = 'project_release_data_all:'. $api_version;
@@ -192,16 +211,6 @@
     }
   }
 
-  // Find the $tid for the API version specified.
-  // Stop processing when $api_version = $term; that's our tid.
-  // If we don't have a match, this should assume the most current.
-  $tids = project_release_compatibility_list();
-  foreach ($tids as $tid => $term) {
-    if ($api_version == $term) {
-      break;
-    }
-  }
-
   $where = array();
   $parameters = array();
   $join_parameters = array();
--- usage/README.txt
+++ usage/README.txt
@@ -0,0 +1,11 @@
+The project_usage.module provides logging of the XML-RPC queries that
+are sent by the update_status.module (contrib for 5.x) to the
+project_release.module on drupal.org. 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 XML-RPC server
+running the project.module and project_release.module, in which case,
+they might want to enable this module. Otherwise, sites should leave
+this disabled.
+
+$Id$

--- usage/project_usage.info
+++ usage/project_usage.info
@@ -0,0 +1,6 @@
+; $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
+version = "$Name:  $"

--- usage/project_usage.install
+++ usage/project_usage.install
@@ -0,0 +1,114 @@
+<?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_data} (
+          site_key varchar(32) NOT NULL default '',
+          timestamp int unsigned NOT NULL default '0',
+          query_data longtext NOT NULL default '',
+          version_tid int unsigned NOT NULL default '0',
+          PRIMARY KEY (site_key)
+        ) /*!40100 DEFAULT CHARACTER SET utf8 */;");
+      db_query("CREATE TABLE IF NOT EXISTS {project_usage_normal_data} (
+          site_key varchar(32) NOT NULL default '',
+          timestamp int unsigned NOT NULL default '0',
+          pid int unsigned NOT NULL default '0',
+          version_tid int unsigned NOT NULL default '0',
+          version_major int unsigned NOT NULL default '0',
+          PRIMARY KEY (site_key, pid),
+          KEY (pid),
+          KEY (version_tid),
+          KEY (version_major)
+        ) /*!40100 DEFAULT CHARACTER SET utf8 */;");
+      db_query("CREATE TABLE IF NOT EXISTS {project_usage_count} (
+          pid int unsigned NOT NULL default '0',
+          version_tid int unsigned NOT NULL default '0',
+          version_major int unsigned NOT NULL default '0',
+          count int unsigned NOT NULL default '0',
+          PRIMARY KEY (pid),
+          KEY (version_tid),
+          KEY (version_major)
+        ) /*!40100 DEFAULT CHARACTER SET utf8 */;");
+      db_query("CREATE TABLE IF NOT EXISTS {project_usage_count_history} (
+          pid int unsigned NOT NULL default '0',
+          year int unsigned NOT NULL default '0',
+          month int unsigned NOT NULL default '0',
+          version_tid int unsigned NOT NULL default '0',
+          version_major int unsigned NOT NULL default '0',
+          count int unsigned NOT NULL default '0',
+          PRIMARY KEY (pid, year, month),
+          KEY (version_tid),
+          KEY (version_major)
+        ) /*!40100 DEFAULT CHARACTER SET utf8 */;");
+      break;
+
+    case 'pgsql':
+      if (!db_table_exists('project_usage_raw_data')) {
+        db_query("CREATE TABLE {project_usage_raw_data} (
+            site_key varchar(32) NOT NULL default '',
+            timestamp int NOT NULL default '0',
+            query_data text NOT NULL default '',
+            version_tid int NOT NULL default '0',
+            PRIMARY KEY (site_key)
+          );");
+      }
+      if (!db_table_exists('project_usage_normal_data')) {
+        db_query("CREATE TABLE {project_usage_normal_data} (
+            site_key varchar(32) NOT NULL default '',
+            timestamp int NOT NULL default '0',
+            pid int NOT NULL default '0',
+            version_tid int NOT NULL default '0',
+            version_major int NOT NULL default '0',
+            PRIMARY KEY (site_key, pid)
+          );");
+        db_query("CREATE INDEX {project_usage_normal_data}_pid_idx ON {project_usage_normal_data} (pid)");
+        db_query("CREATE INDEX {project_usage_normal_data}_version_tid_idx ON {project_usage_normal_data} (version_tid)");
+        db_query("CREATE INDEX {project_usage_normal_data}_version_major_idx ON {project_usage_normal_data} (version_major)");
+      }
+      if (!db_table_exists('project_usage_count')) {
+        db_query("CREATE TABLE {project_usage_count} (
+            pid int NOT NULL default '0',
+            version_tid int NOT NULL default '0',
+            version_major int NOT NULL default '0',
+            count int NOT NULL default '0',
+            PRIMARY KEY (pid)
+          );");
+        db_query("CREATE INDEX {project_usage_count}_version_tid_idx ON {project_usage_count} (version_tid)");
+        db_query("CREATE INDEX {project_usage_count}_version_major_idx ON {project_usage_count} (version_major)");
+      }
+      if (!db_table_exists('project_usage_count_history')) {
+        db_query("CREATE TABLE {project_usage_count_history} (
+            pid int NOT NULL default '0',
+            year int NOT NULL default '0',
+            month int NOT NULL default '0',
+            version_tid int NOT NULL default '0',
+            version_major int NOT NULL default '0',
+            count int NOT NULL default '0',
+            PRIMARY KEY (pid, year, month)
+          );");
+        db_query("CREATE INDEX {project_usage_count_history}_version_tid_idx ON {project_usage_count_history} (version_tid)");
+        db_query("CREATE INDEX {project_usage_count_history}_version_major_idx ON {project_usage_count_history} (version_major)");
+      }
+      break;
+  }
+}
+
+function project_usage_uninstall() {
+  $tables = array(
+    'project_usage_raw_data',
+    'project_usage_normal_data',
+    'project_usage_count',
+    'project_usage_count_history',
+  );
+  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_monthly');
+}

--- usage/project_usage.module
+++ usage/project_usage.module
@@ -0,0 +1,152 @@
+<?php
+// $Id$
+// $Name$
+
+/**
+ * @file
+ *
+ * This module provides logging of the XML-RPC queries that are 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 calls this module to store
+ * the data to the tables defined 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
+ * XML-RPC server, in which case, they might want to enable this module.
+ * Otherwise, sites should just leave this disabled.
+ */
+
+/**
+ * Quickly saves raw query data for later processing of project usage.
+ *
+ * @param $site_key
+ *   If the client has been configured to allow the server to log anonymous
+ *   usage data, the $site_key will be an md5hash of the site's baseurl and its
+ *   private key, so we can uniquely but anonymously identify the site for 
+ *   saving usage statistics.
+ * @param $projects
+ *   The 'short name' (stored as 'url') of a project, or the keyword 'all' for
+ *   all projects. Usually an array of projects.
+ * @param $api_tid
+ *   The integer taxonomy term id for the API compatibility in the query.
+ */
+function project_usage_save_raw_query($site_key, $projects, $api_tid) {
+  if (db_num_rows(db_query("SELECT * FROM {project_usage_raw_data} WHERE site_key = '%s'", $site_key))) { 
+    db_query("UPDATE {project_usage_raw_data} SET timestamp = %d, version_tid = %d, query_data = '%s' WHERE site_key = '%s'", time(), $api_tid, serialize($projects), $site_key);
+  }
+  else {
+    db_query("INSERT INTO {project_usage_raw_data} (site_key, timestamp, version_tid, query_data) VALUES ('%s', %d, %d, '%s')", $site_key, time(), $api_tid, serialize($projects));
+  }
+}
+
+/**
+ * Process one day's worth of raw data and populate the normalized and
+ * summary tables we can use for displaying usage statistics.
+ */
+function project_usage_process_day() {
+  watchdog('project_usage', t('Starting to process daily usage data.'));
+
+  // First, throw out all records that are older than 3 days
+  // (everything should be updated every day).
+  db_query("DELETE FROM {project_usage_raw_data} WHERE timestamp < %d", (time() - (60*60*24*3)));
+
+  // TODO: LOCK the tables?
+  db_query("TRUNCATE {project_usage_count}");
+  db_query("TRUNCATE {project_usage_normal_data}");
+  // TODO: drop all keys on {project_usage_normal_data} and rebuild
+  //   after we're done with all the inserts?
+  // TODO: should we just insert everything into normal_data and then
+  //   use SELECT COUNT(*) to populate {project_usage_count}?
+  // TODO: the serialized array is all wrong, it should be
+  //   (name,major) pairs, not just name and a global major.
+  $counts = array();
+  $query = db_query("SELECT * FROM {project_usage_raw_data}");
+  while ($row = db_fetch_object($query)) {
+    $projects = unserialize($row->query_data);
+    foreach ($projects as $project) {
+      $pid = _project_usage_get_pid($project);
+      if (empty($pid)) {
+        // If this project doesn't have a nid, just skip it (it's bogus).
+        continue;
+      }
+      $counts[$version_tid][$pid]++;
+      db_query("INSERT INTO {project_usage_normal_data} (site_key, timestamp, pid, version_tid, version_major) VALUES ('%s', %d, %d, %d, %d)", $row->site_key, $row->timestamp, $pid, $row->version_tid, 1);
+      // TODO: eek! version_major shouldn't be hard-coded like this.
+    }
+  }
+
+  foreach ($counts as $version_tid => $projects) {
+    foreach ($projects as $pid => $count) {
+      db_query("INSERT INTO {project_usage_count} (pid, version_tid, version_major, count) VALUES (%d, %d, %d, %d)", $pid, $version_tid, 1, $count);
+    }
+  }
+
+  // TODO: UNLOCK?
+  // TODO: db_query("TRUNCATE {project_usage_raw_data}"); ?
+  watchdog('project_usage', t('Completed daily usage data processing.'));
+
+}
+
+/**
+ * Perform the monthly archiving of usage summary data.
+ *
+ * @param $year The current year.
+ * @param $month The current month.
+ */
+function project_usage_process_month($year, $month) {
+  watchdog('project_usage', t('Saving monthly usage data for !year-!month', array('!year' => $year, '!month' => $month)));
+  db_query("INSERT INTO {project_usage_count_history}
+    (pid, year, month, version_tid, version_major, count)
+    SELECT pid, %d, %d, version_tid, version_major, count
+    FROM {project_usage_count}", $year, $month);
+}
+
+/**
+ * Implementation of hook_cron()
+ */
+function project_usage_cron() {
+  // Figure out if it's been 24 hours since our last daily processing,
+  // and if so, invoke the processing function.
+  $daily = 60 * 60 * 24;
+  $cur_time = time();
+  if (($cur_time - $daily) >= variable_get('project_usage_last_daily', 0)) {
+    project_usage_process_day();
+    variable_set('project_usage_last_daily', $cur_time);
+  }
+
+  // Figure out if it's the 1st of the month and if we need to do
+  // this month's processing.
+  $day_of_month = date('d', $cur_time);
+  $month = date('m', $cur_time);
+  $year = date('Y', $cur_time);
+  $month_id = $year .'-'. $month;
+  if ($day_of_month == '27' && $month_id != variable_get('project_usage_last_monthly', 0)) {
+    project_usage_process_month($year, $month);
+    variable_set('project_usage_last_monthly', $month_id);
+  }
+}
+
+/**
+ * Returns the nid of the requested project.
+ *
+ * Since we need to do the lookups a *lot* to map project uris into
+ * project nids, during the daily processing run, this function does a
+ * single DB query and caches the results in a static array.
+ *
+ * @param $project_uri
+ *   Short name of the project.
+ * @return
+ *   The node id (nid) of the project.
+ */
+function _project_usage_get_pid($project_uri) {
+  static $projects = array();
+  if (empty($projects)) {
+    $query = db_query("SELECT nid, uri FROM {project_projects}");
+    while ($row = db_fetch_object($query)) {
+      $projects[$row->uri] = $row->nid;
+    }
+  }
+  return $projects[$project_uri];
+}

