Index: project_usage.install =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/project/usage/project_usage.install,v retrieving revision 1.1 diff -u -p -u -r1.1 project_usage.install --- project_usage.install 7 Aug 2007 20:21:33 -0000 1.1 +++ project_usage.install 19 Aug 2007 20:08:07 -0000 @@ -38,6 +38,15 @@ function project_usage_install() { count int unsigned NOT NULL default '0', PRIMARY KEY (nid, timestamp) ) /*!40100 DEFAULT CHARACTER SET utf8 */;"); + db_query("CREATE TABLE IF NOT EXISTS {cache_project_usage} ( + cid varchar(255) BINARY NOT NULL default '', + data longblob, + expire int NOT NULL default '0', + created int NOT NULL default '0', + headers text, + PRIMARY KEY (cid), + INDEX expire (expire) + ) /*!40100 DEFAULT CHARACTER SET UTF8 */;"); break; } } @@ -48,6 +57,7 @@ function project_usage_uninstall() { 'project_usage_day', 'project_usage_week_project', 'project_usage_week_release', + 'cache_project_usage', ); foreach ($tables as $table) { if (db_table_exists($table)) { @@ -61,8 +71,32 @@ function project_usage_uninstall() { 'project_usage_life_daily', 'project_usage_life_weekly_project', 'project_usage_life_weekly_release', + 'project_usage_date_long', + 'project_usage_date_short', ); foreach ($variables as $variable) { variable_del($variable); } } + +/** + * Add a cache table {cache_project_release}. + */ +function project_usage_update_5000() { + $ret = array(); + switch ($GLOBALS['db_type']) { + case 'mysql': + case 'mysqli': + $ret[] = update_sql("CREATE TABLE IF NOT EXISTS {cache_project_usage} ( + cid varchar(255) BINARY NOT NULL default '', + data longblob, + expire int NOT NULL default '0', + created int NOT NULL default '0', + headers text, + PRIMARY KEY (cid), + INDEX expire (expire) + ) /*!40100 DEFAULT CHARACTER SET UTF8 */;"); + break; + } + return $ret; +} Index: project_usage.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/project/usage/project_usage.module,v retrieving revision 1.4 diff -u -p -u -r1.4 project_usage.module --- project_usage.module 14 Aug 2007 00:51:53 -0000 1.4 +++ project_usage.module 20 Aug 2007 17:56:30 -0000 @@ -5,20 +5,20 @@ /** * @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 + * 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 + * + * 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, + * 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. */ @@ -29,6 +29,15 @@ define('PROJECT_USAGE_WEEK', PROJECT_USA // Number of seconds in a year. define('PROJECT_USAGE_YEAR', PROJECT_USAGE_DAY * 365); +// Date formats for month and day. We define our own rather than using core's +// 'date_format_short' and 'date_format_long' variables because our timestamps +// don't have hour or minute resolution so displaying that would be confusing +// and take up extra space. +define('PROJECT_USAGE_DATE_LONG', variable_get('project_usage_date_long', 'F jS')); +define('PROJECT_USAGE_DATE_SHORT', variable_get('project_usage_date_short', 'M j')); + +define('PROJECT_USAGE_SHOW_WEEKS', 6); + /** * Implementation of hook_menu(). */ @@ -43,11 +52,274 @@ function project_usage_menu($may_cache) 'description' => t('Configure how long usage data is retained.'), 'weight' => 1, ); + $items[] = array( + 'path' => 'project-usage', + 'title' => t('Project usage'), + 'callback' => 'project_usage_overview', + 'access' => user_access('view project usage'), + ); + } + else { + // Project and release usage tabs. + if (arg(0) == 'node' && is_numeric(arg(1))) { + $node = node_load(arg(1)); + if ($node->nid && ($node->type == 'project_project' || $node->type == 'project_release')) { + $items[] = array( + 'path' => 'node/'. arg(1) .'/usage', + 'title' => t('Usage'), + 'access' => user_access('view project usage'), + 'callback' => ($node->type == 'project_project') ? 'project_usage_project_page' : 'project_usage_release_page', + 'callback arguments' => array($node), + 'type' => MENU_LOCAL_TASK, + 'weight' => 2, + ); + } + } } return $items; } /** + * Implementation of hook_help(). + */ +function project_usage_help($section) { + switch ($section) { + case 'project-usage': + return t('The following is a summary of the usage information for the projects on this site. The count is the total number of sites using any version of the project. Only sites that have opted to allow usage information to be tracked are included.'); + + case 'admin/modules#description': + return t('Collects and processes usage information about projects and releases.'); + } +} + +/** + * Implementation of hook_perm(). + */ +function project_usage_perm() { + $perms = array( + 'view project usage', + ); + return $perms; +} + +/** + * Display and overview of usage for all modules. + */ +function project_usage_overview() { + drupal_add_css(drupal_get_path('module', 'project_usage') .'/project_usage.css'); + drupal_set_title(t('Project usage overview')); + + $week_count = PROJECT_USAGE_SHOW_WEEKS; + + // In order to get the project usage data into a sortable table, we've gotta + // write a pretty evil query: + // - We need to create a column for each week but some weeks may not have any + // usage data, forcing us to use a LEFT JOIN, rather than the more + // efficient INNER JOIN. + // - The LEFT JOINs mean we have to limit the entries in {node} so that we're + // not including things like forum posts, hence the WHERE IN below. + // - Each project may have multiple records in {project_usage_week_project} + // to track usage for API version. We need to SUM() them to get a total + // count forcing us to GROUP BY. Sadly, I can't explain why we need + // SUM(DISTINCT)... it just works(TM). + $header = array(array('field' => 'n.title', 'data' => t('Project'), 'sort' => 'asc')); + $fields = array('n.nid', 'n.title'); + $joins = array(); + $wheres = array('n.nid IN (SELECT nid FROM {project_usage_week_project})'); + $groupbys = array('n.nid', 'n.title'); + foreach (project_usage_get_last_weeks($week_count) as $i => $week) { + $header[] = array('field' => "week{$i}", 'data' => format_date($week, 'custom', PROJECT_USAGE_DATE_SHORT, 0)); + $fields[] = "SUM(DISTINCT p{$i}.count) AS week{$i}"; + $joins[] = "LEFT JOIN {project_usage_week_project} p{$i} ON n.nid = p{$i}.nid AND p{$i}.timestamp = $week"; + } + + + // Check for a cached page. The cache id needs to take into account the sort + // column and order. + $sort = tablesort_init($header); + $cid = 'overview:'. $sort['sql'] .':'. $sort['sort']; + if ($cached = cache_get($cid, 'cache_project_usage')) { + return $cached->data; + } + + + // Fire off all those LEFT JOINs... you can almost hear the DB crying. + $result = db_query(_project_usage_query($header, $fields, $joins, $wheres, $groupbys)); + while ($line = db_fetch_array($result)) { + $row = array(array('data' => l($line['title'], 'node/'. $line['nid'] .'/usage'))); + for ($i = 0; $i < $week_count; $i++) { + $row[] = array('data' => number_format($line["week{$i}"]), 'class' => 'project-usage-numbers'); + } + $rows[] = $row; + } + $output = theme('table', $header, $rows); + + + // Cache the completed page. + cache_set($cid, 'cache_project_usage', $output); + return $output; +} + +/** + * Display the usage history of a project node. + */ +function project_usage_project_page($node) { + drupal_add_css(drupal_get_path('module', 'project_usage') .'/project_usage.css'); + project_project_set_breadcrumb($node, $breadcrumb); + drupal_set_title(check_plain($node->title)); + + // In order to keep the database load down we need to cache these pages. + // Because the release usage table is sortable, the cache id needs to take + // into account the sort parameters. The easiest way to ensure we have valid + // sorting parameters is to build the table headers and let the tablesort + // functions do it. This means we end up doing most of the work to build the + // page's second table early on. We might as well finish the job, then build + // the other table and output them in the correct order. + + $week_count = PROJECT_USAGE_SHOW_WEEKS; + $releases = project_release_get_releases($node, FALSE); + + // Build a table showing this week's usage for each release. In order to get + // the release usage data into a sortable table, we've gotta write another + // evil query: + // - We need to create a column for each week but some weeks may not have any + // usage data, forcing us to use a LEFT JOIN, rather than the more + // efficient INNER JOIN. + // - The LEFT JOINs mean we have to limit the entries in {node} so that we're + // not including things like forum posts, hence the WHERE IN below. + $release_header = array(array('field' => 'n.title', 'data' => t('Release'), 'sort' => 'desc')); + $fields = array('n.nid'); + $joins = array(); + $wheres = array('n.nid IN ('. implode(',', array_keys($releases)) .')'); + foreach (project_usage_get_last_weeks($week_count) as $i => $week) { + $release_header[] = array( + 'field' => "week{$i}", + 'data' => format_date($week, 'custom', PROJECT_USAGE_DATE_SHORT, 0), + 'class' => 'project-usage-week', + ); + $fields[] = "p{$i}.count AS week{$i}"; + $joins[] = "LEFT JOIN {project_usage_week_release} p{$i} ON n.nid = p{$i}.nid AND p{$i}.timestamp = $week"; + } + + + // Check for a cached page. The cache id needs to take into account the sort + // column and order. + $sort = tablesort_init($release_header); + $cid = 'project:'. $node->nid .':'. $sort['sql'] .':'. $sort['sort']; + if ($cached = cache_get($cid, 'cache_project_usage')) { + return $cached->data; + } + + + // Execute our, rather expensive, query. + $release_rows = array(); + $result = db_query(_project_usage_query($release_header, $fields, $joins, $wheres)); + while ($line = db_fetch_array($result)) { + $row = array(array('data' => l($releases[$line['nid']], 'node/'. $line['nid'] .'/usage'))); + for ($i = 0; $i < $week_count; $i++) { + $row[] = array('data' => number_format($line["week{$i}"]), 'class' => 'project-usage-numbers'); + } + $release_rows[] = $row; + } + + + // Build a table of the weekly usage data with a column for each API version. + // Get an array of the weeks going back as far as we have data... + $oldest = db_result(db_query("SELECT MIN(timestamp) FROM {project_usage_week_project} WHERE nid = %d", $node->nid)); + if ($oldest === NULL) { + $weeks = array(); + } + else { + $weeks = project_usage_get_weeks_since($oldest); + // ...ignore the current week, since we won't have usage data for that and + // reverse the order so it's newest to oldest. + array_pop($weeks); + $weeks = array_reverse($weeks); + } + + // The number of columns varies depending on how many different API versions + // are in use. Set up the header and a blank, template row, based on the + // number of distinct terms in use. + $project_header = array(t('Week')); + $blank_row = array(); + $result = db_query("SELECT DISTINCT td.tid, td.name FROM {project_usage_week_project} p INNER JOIN {term_data} td ON p.tid = td.tid WHERE p.nid = %d ORDER BY td.weight, td.name", $node->nid); + while ($row = db_fetch_object($result)) { + $project_header[$row->tid] = array('data' => $row->name, 'class' => 'project-usage-week'); + $blank_row[$row->tid] = array('data' => 0, 'class' => 'project-usage-numbers'); + } + + // Now create a blank table with a row for each week and formatted date in + // the first column... + $project_rows = array(); + foreach ($weeks as $week) { + $project_rows[$week] = array(0 => format_date($week, 'custom', PROJECT_USAGE_DATE_LONG, 0)) + $blank_row; + } + // ...then fill it in with our data. + $result = db_query("SELECT timestamp, tid, count FROM {project_usage_week_project} WHERE nid = %d", $node->nid); + while ($row = db_fetch_object($result)) { + $project_rows[$row->timestamp][$row->tid]['data'] = number_format($row->count); + } + // Strip out the keys so that the odd/even row themeing works correctly. + $project_rows = array_values($project_rows); + + + $output = '