project_usage.module

Tracking 5.x-1.x branch
  1. drupal
    1. 5 contributions/project/usage/project_usage.module

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.

Functions & methods

NameDescription
project_usage_cache_timeSets the expiry timestamp for cached project usage pages.
project_usage_chart_axis_labelsAdd axis labels to the chart arguments.
project_usage_chart_label_subsetExtract a subset of labels at regular intervals from a set.
project_usage_daily_timestampCompute a timestamp for the beginning of a day N days ago.
project_usage_devel_cachesImplementation of hook_devel_caches().
project_usage_dispatchMenu handler for project URLs.
project_usage_get_active_weeksReturn an array of the most recent weeks for which we have data.
project_usage_get_weeks_sinceBuild an array of timestamps for the beginning (midnight Sunday) for each week since a given timestamp.
project_usage_gmgetdategetdate() with timezone adjustment.
project_usage_helpImplementation of hook_help().
project_usage_menuImplementation of hook_menu().
project_usage_overviewDisplay an overview of usage for all modules.
project_usage_permImplementation of hook_perm().
project_usage_project_pageDisplay the usage history of a project node.
project_usage_project_page_link_alterImplementation of hook_project_page_link_alter().
project_usage_release_pageDisplay the usage history of a release node.
project_usage_settings_formModule settings form.
project_usage_simpletestImplementation of hook_simpletest().
project_usage_weekly_timestampCompute a timestamp for the beginning of a week N weeks ago.
theme_project_usage_chartConvert the array of Google Chart paramters into an image URL.
theme_project_usage_chart_by_releaseConvert the usage table data into a Google Chart image.
theme_project_usage_header_linksCreate list of links at the top of a usage statistics page.
theme_project_usage_project_pageTheme the output of project/usage/<project> page.
theme_project_usage_release_pageTheme the output of the project/usage/<release nid> page.

Constants

NameDescription
PROJECT_USAGE_DATE_LONGDate 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…
PROJECT_USAGE_DATE_SHORT
PROJECT_USAGE_DAY
PROJECT_USAGE_SHOW_WEEKS
PROJECT_USAGE_WEEK
PROJECT_USAGE_YEAR

File

View source
  1. <?php
  2. /**
  3. * @file
  4. *
  5. * This module provides logging of the requests sent by the
  6. * update_status.module (contrib in 5.x) and update.module (core in 6.x) to the
  7. * project_release.module on updates.drupal.org. The
  8. * release/project-release-serve-history.php script inserts data into the
  9. * {project_usage_raw} table created by this module.
  10. *
  11. * On a daily basis the usage data is matched to project and release nodes
  12. * and moved into the {project_usage_day} table. On a weekly basis the daily
  13. * usage data is tallied and stored in the {project_usage_week} table.
  14. *
  15. * This data is then used to compute live usage statistics about all projects
  16. * hosted on drupal.org. In theory, another site could setup
  17. * update_status.module-style checking to their own project.module-based
  18. * server, in which case, they might want to enable this module. Otherwise,
  19. * sites should just leave this disabled.
  20. */
  21. // Number of seconds in a day.
  22. define('PROJECT_USAGE_DAY', 60 * 60 * 24);
  23. // Number of seconds in a week.
  24. define('PROJECT_USAGE_WEEK', PROJECT_USAGE_DAY * 7);
  25. // Number of seconds in a year.
  26. define('PROJECT_USAGE_YEAR', PROJECT_USAGE_DAY * 365);
  27. /**
  28. * Date formats for month and day. We define our own rather than using core's
  29. * 'date_format_short' and 'date_format_long' variables because our timestamps
  30. * don't have hour or minute resolution so displaying that would be confusing
  31. * and take up extra space.
  32. */
  33. define('PROJECT_USAGE_DATE_LONG', 'F jS');
  34. define('PROJECT_USAGE_DATE_SHORT','M j');
  35. // How many weeks should be shown in the usage pages?
  36. define('PROJECT_USAGE_SHOW_WEEKS', 6);
  37. /**
  38. * Implementation of hook_menu().
  39. */
  40. function project_usage_menu($may_cache) {
  41. $items = array();
  42. if ($may_cache) {
  43. $items[] = array(
  44. 'path' => 'admin/project/project-usage-settings',
  45. 'title' => t('Project usage settings'),
  46. 'callback' => 'drupal_get_form',
  47. 'callback arguments' => array('project_usage_settings_form'),
  48. 'description' => t('Configure how long usage data is retained.'),
  49. 'weight' => 1,
  50. );
  51. $items[] = array(
  52. 'path' => 'project/usage',
  53. 'title' => t('Project usage'),
  54. 'callback' => 'project_usage_dispatch',
  55. 'access' => user_access('view project usage'),
  56. );
  57. }
  58. return $items;
  59. }
  60. /**
  61. * Implementation of hook_help().
  62. */
  63. function project_usage_help($section) {
  64. switch ($section) {
  65. case 'project/usage':
  66. return '<p>'.
  67. t('This page summarizes the usage of all projects on @site_name. For each week beginning on the given date the figures show the number of sites that reported they are using (any version of) the project. Detailed usage information for each release of a project is available by clicking the project name.', array('@site_name' => variable_get('site_name', t('this site'))))
  68. .'</p>';
  69. }
  70. }
  71. /**
  72. * Implementation of hook_perm().
  73. */
  74. function project_usage_perm() {
  75. return array(
  76. 'view project usage',
  77. );
  78. }
  79. /**
  80. * Menu handler for project URLs.
  81. *
  82. * @param $key
  83. * Optional node id or project uri. NULL gets the overview page, project
  84. * nids and uris get the project usage page, release nids get the release
  85. * usage page, and everything else gets a not found. In addition, if a user
  86. * does not have permission to view the project or release node they've
  87. * requested, they get an access denied page.
  88. */
  89. function project_usage_dispatch($key = NULL) {
  90. if (!isset($key)) {
  91. return project_usage_overview();
  92. }
  93. // Load the node the user has requested. We want to only use
  94. // project_project_retrieve() if the $key parameter is not numeric because
  95. // project_project_retrieve() will only return a project_project node, and
  96. // we want to allow $node to also be a project_release node.
  97. if (is_numeric($key)) {
  98. $node = node_load($key);
  99. }
  100. else {
  101. $node = project_project_retrieve($key);
  102. }
  103. if (!empty($node->nid)) {
  104. // Make sure that the user has the permission to view this project/
  105. // project_release node.
  106. if (node_access('view', $node)) {
  107. if ($node->type == 'project_project') {
  108. return project_usage_project_page($node);
  109. }
  110. if ($node->type == 'project_release') {
  111. return project_usage_release_page($node);
  112. }
  113. }
  114. else {
  115. return drupal_access_denied();
  116. }
  117. }
  118. return drupal_not_found();
  119. }
  120. /**
  121. * Display an overview of usage for all modules.
  122. */
  123. function project_usage_overview() {
  124. drupal_add_css(drupal_get_path('module', 'project_usage') .'/project_usage.css');
  125. drupal_set_title(t('Project usage overview'));
  126. // Grab an array of active week timestamps.
  127. $weeks = project_usage_get_active_weeks();
  128. // In order to get the project usage data into a sortable table, we've gotta
  129. // write a pretty evil query:
  130. //
  131. // - We need to create a separate column for each week to allow sorting by
  132. // usage in any week (the tablesort_sql() requires that anything you can
  133. // sort on has a distinct field in the underlying query). However, some
  134. // weeks may not have any usage data, forcing us to use a LEFT JOIN,
  135. // rather than the more efficient INNER JOIN.
  136. // - The LEFT JOINs mean we have to limit the entries in {node} so that
  137. // we're not including things like forum posts, hence the WHERE IN below.
  138. // - Each project may have multiple records in {project_usage_week_project}
  139. // to track usage for API version. We need to SUM() them to get a total
  140. // count forcing us to GROUP BY. Sadly, I can't explain why we need
  141. // SUM(DISTINCT)... it just works(TM).
  142. $sql_elements = project_empty_query();
  143. // Ignore the order_bys generated by project_empty_query(), and use
  144. // the tablesort instead.
  145. unset($sql_elements['order_bys']);
  146. $where_args = array();
  147. $sql_elements['fields']['pieces'] = array('n.nid', 'n.title', 'pp.uri');
  148. $sql_elements['from']['pieces'][] = '{node} n ';
  149. $sql_elements['joins']['pieces'][] = "INNER JOIN {project_projects} pp ON n.nid = pp.nid";
  150. $sql_elements['wheres']['pieces'] = array('n.nid IN (SELECT nid FROM {project_usage_week_project}) AND n.status = %d');
  151. $where_args[] = 1; // n.status = 1
  152. $sql_elements['group_bys']['pieces'] = array('n.nid', 'n.title');
  153. $headers = array(array('field' => 'n.title', 'data' => t('Project')));
  154. $joins_args = array();
  155. foreach ($weeks as $i => $week) {
  156. // Note that "{$i}" in these query snippets are used to add a week integer
  157. // to the table and field aliases so we can uniquely identify each column
  158. // for sorting purposes. These are not literals in the query, we need
  159. // these aliases to be unique via this PHP string operation before we even
  160. // build the query.
  161. $sql_elements['fields']['pieces'][] = "SUM(DISTINCT p{$i}.count) AS week{$i}";
  162. $sql_elements['joins']['pieces'][] = "LEFT JOIN {project_usage_week_project} p{$i} ON n.nid = p{$i}.nid AND p{$i}.timestamp = %d";
  163. $joins_args[] = $week;
  164. $header = array(
  165. 'field' => "week{$i}",
  166. 'data' => format_date($week, 'custom', variable_get('project_usage_date_short', PROJECT_USAGE_DATE_SHORT), 0),
  167. 'class' => 'project-usage-numbers'
  168. );
  169. if ($i == 0) {
  170. $header['sort'] = 'desc';
  171. }
  172. $headers[] = $header;
  173. }
  174. // Check for a cached page. The cache id needs to take into account the sort
  175. // column and order.
  176. $sort = tablesort_init($headers);
  177. $cid = 'overview:'. $sort['sql'] .':'. $sort['sort'];
  178. if (project_can_cache() && $cached = cache_get($cid, 'cache_project_usage')) {
  179. return $cached->data;
  180. }
  181. $args = array_merge($joins_args, $where_args);
  182. $result = db_query(project_build_query($sql_elements) . tablesort_sql($headers), $args);
  183. while ($line = db_fetch_array($result)) {
  184. $row = array(array('data' => l($line['title'], 'project/usage/'. $line['uri'])));
  185. foreach ($weeks as $i => $week) {
  186. $row[] = array('data' => number_format($line["week{$i}"]), 'class' => 'project-usage-numbers');
  187. }
  188. $rows[] = $row;
  189. }
  190. $output = theme('table', $headers, $rows, array('id' => 'project-usage-all-projects'));
  191. // Cache the completed page.
  192. if (project_can_cache()) {
  193. cache_set($cid, 'cache_project_usage', $output, project_usage_cache_time());
  194. }
  195. return $output;
  196. }
  197. /**
  198. * Display the usage history of a project node.
  199. */
  200. function project_usage_project_page($node) {
  201. drupal_add_css(drupal_get_path('module', 'project_usage') .'/project_usage.css');
  202. $breadcrumb = array(
  203. l(t('Usage'), 'project/usage'),
  204. );
  205. project_project_set_breadcrumb(NULL, $breadcrumb);
  206. drupal_set_title(t('Usage statistics for %project', array('%project' => $node->title)));
  207. // In order to keep the database load down we need to cache these pages.
  208. // Because the release usage table is sortable, the cache id needs to take
  209. // into account the sort parameters. The easiest way to ensure we have valid
  210. // sorting parameters is to build the table headers and let the tablesort
  211. // functions do it. This means we end up doing most of the work to build the
  212. // page's second table early on. We might as well finish the job, then build
  213. // the other table and output them in the correct order.
  214. // Grab an array of active week timestamps.
  215. $weeks = project_usage_get_active_weeks();
  216. $releases = project_release_get_releases($node, FALSE);
  217. // If there are no releases for this project, we can skip the rest
  218. // of this function.
  219. if (empty($releases)) {
  220. return theme('project_usage_project_page', $node);
  221. }
  222. // Build a table showing this week's usage for each release. In order to get
  223. // the release usage data into a sortable table, we need another evil query:
  224. // - We need to create a separate column for each week to allow sorting by
  225. // usage in any week (the tablesort_sql() requires that anything you can
  226. // sort on has a distinct field in the underlying query). However, some
  227. // weeks may not have any usage data, forcing us to use a LEFT JOIN,
  228. // rather than the more efficient INNER JOIN.
  229. // - We need to create a column for each week but some weeks may not have any
  230. // usage data, forcing us to use a LEFT JOIN, rather than the more
  231. // efficient INNER JOIN.
  232. // - The LEFT JOINs mean we have to limit the entries in {node} so that we're
  233. // not including things like forum posts, hence the WHERE IN below.
  234. $sql_elements = project_empty_query();
  235. // Ignore the order_bys generated by project_empty_query(), and use
  236. // the tablesort instead.
  237. unset($sql_elements['order_bys']);
  238. $sql_elements['fields']['pieces'] = array('n.nid');
  239. $sql_elements['from']['pieces'][] = '{node} n ';
  240. $sql_elements['wheres']['pieces'] = array('n.nid IN ('. implode(', ', array_fill(0, count($releases), '%d')) .') AND n.status = %d');
  241. $where_args = array_keys($releases);
  242. $where_args[] = 1; // n.status = 1
  243. $release_header = array(array('field' => 'n.title', 'data' => t('Release'), 'sort' => 'desc'));
  244. $joins_args = array();
  245. foreach ($weeks as $i => $week) {
  246. // Note that "{$i}" in these query snippets are used to add a week integer
  247. // to the table and field aliases so we can uniquely identify each column
  248. // for sorting purposes. These are not literals in the query, we need
  249. // these aliases to be unique via this PHP string operation before we even
  250. // build the query.
  251. $sql_elements['fields']['pieces'][] = "p{$i}.count AS week{$i}";
  252. $sql_elements['joins']['pieces'][] = "LEFT JOIN {project_usage_week_release} p{$i} ON n.nid = p{$i}.nid AND p{$i}.timestamp = %d";
  253. $joins_args[] = $week;
  254. $release_header[] = array(
  255. 'field' => "week{$i}",
  256. 'data' => format_date($week, 'custom', variable_get('project_usage_date_short', PROJECT_USAGE_DATE_SHORT), 0),
  257. 'class' => 'project-usage-numbers',
  258. );
  259. }
  260. // Check for a cached page. The cache id needs to take into account the sort
  261. // column and order.
  262. $sort = tablesort_init($release_header);
  263. $cid = 'project:'. $node->nid .':'. $sort['sql'] .':'. $sort['sort'];
  264. if ($cached = cache_get($cid, 'cache_project_usage')) {
  265. return $cached->data;
  266. }
  267. $args = array_merge($joins_args, $where_args);
  268. $result = db_query(project_build_query($sql_elements) . tablesort_sql($release_header), $args);
  269. $release_rows = array();
  270. while ($line = db_fetch_array($result)) {
  271. $sum = 0;
  272. $row = array(array('data' => l($releases[$line['nid']], 'project/usage/'. $line['nid'])));
  273. foreach ($weeks as $i => $week) {
  274. $sum += $line["week{$i}"];
  275. $row[] = array('data' => number_format($line["week{$i}"]), 'class' => 'project-usage-numbers');
  276. }
  277. // Skip any release with no usage.
  278. if ($sum) {
  279. $release_rows[] = $row;
  280. }
  281. }
  282. // Build a table of the weekly usage data with a column for each API version.
  283. // Get an array of the weeks going back as far as we have data...
  284. $oldest = db_result(db_query("SELECT MIN(puwp.timestamp) FROM {project_usage_week_project} puwp WHERE puwp.nid = %d", $node->nid));
  285. if ($oldest === NULL) {
  286. $weeks = array();
  287. }
  288. else {
  289. $weeks = project_usage_get_weeks_since($oldest);
  290. // ...ignore the current week, since we won't have usage data for that and
  291. // reverse the order so it's newest to oldest.
  292. array_pop($weeks);
  293. $weeks = array_reverse($weeks);
  294. }
  295. // The number of columns varies depending on how many different API versions
  296. // are in use. Set up the header and a blank, template row, based on the
  297. // number of distinct terms in use. This *could* be done with LEFT JOINs,
  298. // but it ends up being a more expensive query and harder to read.
  299. $project_header = array(0 => array('data' => t('Week')));
  300. $blank_row = array(0 => array('data' => ''));
  301. $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);
  302. while ($row = db_fetch_object($result)) {
  303. $project_header[$row->tid] = array('data' => check_plain($row->name), 'class' => 'project-usage-numbers');
  304. $blank_row[$row->tid] = array('data' => 0, 'class' => 'project-usage-numbers');
  305. }
  306. // Now create a blank table with a row for each week and formatted date in
  307. // the first column...
  308. $project_rows = array();
  309. foreach ($weeks as $week) {
  310. $project_rows[$week] = $blank_row;
  311. $project_rows[$week][0]['data'] = format_date($week, 'custom', variable_get('project_usage_date_long', PROJECT_USAGE_DATE_LONG), 0);
  312. }
  313. // ...then fill it in with our data.
  314. $result = db_query("SELECT timestamp, tid, count FROM {project_usage_week_project} WHERE nid = %d", $node->nid);
  315. while ($row = db_fetch_object($result)) {
  316. $project_rows[$row->timestamp][$row->tid]['data'] = number_format($row->count);
  317. }
  318. $output = theme('project_usage_project_page', $node, $release_header, $release_rows, $project_header, $project_rows);
  319. // Cache the completed page.
  320. if (project_can_cache()) {
  321. cache_set($cid, 'cache_project_usage', $output, project_usage_cache_time());
  322. }
  323. return $output;
  324. }
  325. /**
  326. * Display the usage history of a release node.
  327. */
  328. function project_usage_release_page($node) {
  329. drupal_add_css(drupal_get_path('module', 'project_usage') .'/project_usage.css');
  330. $project = node_load($node->pid);
  331. $breadcrumb = array(
  332. l(t('Usage'), 'project/usage'),
  333. l($project->title, 'project/usage/'. $project->nid),
  334. );
  335. project_project_set_breadcrumb(NULL, $breadcrumb);
  336. drupal_set_title(t('Usage statistics for %release', array('%release' => $node->title)));
  337. // Check for a cached page.
  338. $cid = "release:{$node->nid}";
  339. if (project_can_cache() && $cached = cache_get($cid, 'cache_project_usage')) {
  340. return $cached->data;
  341. }
  342. // Table displaying the usage back through time.
  343. $header = array(
  344. array('data' => t('Week starting')),
  345. array('data' => t('Count'), 'class' => 'project-usage-numbers'),
  346. );
  347. $rows = array();
  348. $query = db_query("SELECT timestamp, count FROM {project_usage_week_release} WHERE nid = %d ORDER BY timestamp DESC", $node->nid);
  349. while ($row = db_fetch_object($query)) {
  350. $rows[] = array(
  351. array('data' => format_date($row->timestamp, 'custom', variable_get('project_usage_date_long', PROJECT_USAGE_DATE_LONG), 0)),
  352. array('data' => number_format($row->count), 'class' => 'project-usage-numbers'),
  353. );
  354. }
  355. $output = theme('project_usage_release_page', $project, $node, $header, $rows);
  356. // Cache the completed page.
  357. if (project_can_cache()) {
  358. cache_set($cid, 'cache_project_usage', $output, project_usage_cache_time());
  359. }
  360. return $output;
  361. }
  362. /**
  363. * Module settings form.
  364. */
  365. function project_usage_settings_form() {
  366. $times = array(
  367. 3 * PROJECT_USAGE_YEAR,
  368. 2 * PROJECT_USAGE_YEAR,
  369. 1 * PROJECT_USAGE_YEAR,
  370. 26 * PROJECT_USAGE_WEEK,
  371. 12 * PROJECT_USAGE_WEEK,
  372. 8 * PROJECT_USAGE_WEEK,
  373. 4 * PROJECT_USAGE_WEEK,
  374. );
  375. $age_options = drupal_map_assoc($times, 'format_interval');
  376. $form['project_usage_life_daily'] = array(
  377. '#type' => 'select',
  378. '#title' => t('Daily usage data lifespan'),
  379. '#default_value' => variable_get('project_usage_life_daily', 4 * PROJECT_USAGE_WEEK),
  380. '#options' => $age_options,
  381. '#description' => t('Discard the daily usage data after this amount of time has passed.'),
  382. );
  383. $form['project_usage_life_weekly_project'] = array(
  384. '#type' => 'select',
  385. '#title' => t('Weekly project data lifespan'),
  386. '#default_value' => variable_get('project_usage_life_weekly_project', PROJECT_USAGE_YEAR),
  387. '#options' => $age_options,
  388. '#description' => t('Discard the weekly usage for project nodes after this amount of time has passed.'),
  389. );
  390. $form['project_usage_life_weekly_release'] = array(
  391. '#type' => 'select',
  392. '#title' => t('Weekly release data lifespan'),
  393. '#default_value' => variable_get('project_usage_life_weekly_release', 26 * PROJECT_USAGE_WEEK),
  394. '#options' => $age_options,
  395. '#description' => t('Discard the weekly usage for project nodes after this amount of time has passed.'),
  396. );
  397. return system_settings_form($form);
  398. }
  399. /**
  400. * getdate() with timezone adjustment.
  401. *
  402. * PHP's getdate() is affected by the server's timezone. We need to cancel it
  403. * out so everything is GMT.
  404. *
  405. * @param $timestamp
  406. * An optional, integer UNIX timestamp.
  407. * @return
  408. * An array with results identical to PHP's getdate().
  409. */
  410. function project_usage_gmgetdate($timestamp = NULL) {
  411. $timestamp = isset($timestamp) ? $timestamp : time();
  412. $gmt_offset = (int) date('Z', $timestamp);
  413. return getdate($timestamp - $gmt_offset);
  414. }
  415. /**
  416. * Compute a timestamp for the beginning of a day N days ago.
  417. *
  418. * @param $time
  419. * Mixed, either a GMT timestamp or an array returned by
  420. * project_usage_gmgetdate().
  421. * @param $days_ago
  422. * An integer specifying a number of days previous. A value of 0 indicates
  423. * the current day.
  424. *
  425. * @return
  426. * GMT UNIX timestamp.
  427. */
  428. function project_usage_daily_timestamp($time = NULL, $days_ago = 0) {
  429. $time_parts = is_array($time) ? $time : project_usage_gmgetdate($time);
  430. $day = $time_parts['mday'] - $days_ago;
  431. return gmmktime(0, 0, 0, $time_parts['mon'], $day, $time_parts['year']);
  432. }
  433. /**
  434. * Compute a timestamp for the beginning of a week N weeks ago.
  435. *
  436. * @param $time
  437. * Mixed. Integer timestamp or an array returned by project_usage_gmgetdate().
  438. * @param $weeks_ago
  439. * An integer specifying a number of weeks previous. A value of 0 indicates
  440. * the current week.
  441. *
  442. * @return
  443. * GMT UNIX timestamp.
  444. */
  445. function project_usage_weekly_timestamp($time = NULL, $weeks_ago = 0) {
  446. $time_parts = is_array($time) ? $time : project_usage_gmgetdate($time);
  447. $day = $time_parts['mday'] - $time_parts['wday'] + (7 * $weeks_ago);
  448. return gmmktime(0, 0, 0, $time_parts['mon'], $day, $time_parts['year']);
  449. }
  450. /**
  451. * Build an array of timestamps for the beginning (midnight Sunday) for each
  452. * week since a given timestamp.
  453. *
  454. * @param $timestamp
  455. * UNIX timestamp. The first returned timestamp will be the beginning of the
  456. * week with this time in it.
  457. * @return
  458. * An array of GMT timestamps sorted in ascending order. The first value is
  459. * is the week containing $timestamp. Each subsequent value is the timestamp
  460. * for the next week. The final value is the beginning of the current week.
  461. */
  462. function project_usage_get_weeks_since($timestamp) {
  463. $times = array();
  464. // First, compute the start of the current week so we know when to stop...
  465. $this_week = project_usage_weekly_timestamp();
  466. // ...then compute all the weeks up to that.
  467. $parts = project_usage_gmgetdate($timestamp);
  468. $i = 0;
  469. do {
  470. $times[$i] = project_usage_weekly_timestamp($parts, $i);
  471. } while ($times[$i++] < $this_week);
  472. return $times;
  473. }
  474. /**
  475. * Return an array of the most recent weeks for which we have data.
  476. *
  477. * @return
  478. * An array of UNIX timestamps sorted newest to oldest. Will not include
  479. * the current week.
  480. */
  481. function project_usage_get_active_weeks($reset = FALSE) {
  482. $weeks = variable_get('project_usage_active_weeks', array());
  483. if ($reset || empty($weeks)) {
  484. $count = variable_get('project_usage_show_weeks', PROJECT_USAGE_SHOW_WEEKS);
  485. $query = db_query_range("SELECT DISTINCT(timestamp) FROM {project_usage_week_project} ORDER BY timestamp DESC", array(), 0, $count);
  486. $weeks = array();
  487. while ($week = db_fetch_object($query)) {
  488. $weeks[] = $week->timestamp;
  489. }
  490. variable_set('project_usage_active_weeks', $weeks);
  491. }
  492. return $weeks;
  493. }
  494. /**
  495. * Implementation of hook_simpletest().
  496. */
  497. function project_usage_simpletest() {
  498. $dir = drupal_get_path('module', 'project_usage'). '/tests';
  499. $tests = file_scan_directory($dir, '\.test$');
  500. return array_keys($tests);
  501. }
  502. /**
  503. * Implementation of hook_devel_caches().
  504. *
  505. * Lets the devel module know about our cache table so it can clear it.
  506. */
  507. function project_usage_devel_caches() {
  508. return array('cache_project_usage');
  509. }
  510. /**
  511. * Sets the expiry timestamp for cached project usage pages.
  512. *
  513. * Default is 24 hours.
  514. *
  515. * @return The UNIX timestamp to expire the page at.
  516. */
  517. function project_usage_cache_time() {
  518. return time() + variable_get('project_usage_cache_length', 86400);
  519. }
  520. /**
  521. * Theme the output of project/usage/<project> page.
  522. *
  523. * @param $project
  524. * A fully loaded $node object for a project.
  525. * @param $release_header
  526. * A table header for the release usage table.
  527. * @param $release_rows
  528. * Table rows for the release usage table.
  529. * @param $project_header
  530. * A table header for the weekly project usage table.
  531. * @param $project_rows
  532. * Table rows for the weekly project usage table.
  533. */
  534. function theme_project_usage_project_page($project, $release_header = NULL, $release_rows = NULL, $project_header = NULL, $project_rows = NULL) {
  535. $output = theme('project_usage_header_links', $project);
  536. if (empty($release_rows)) {
  537. // There are no published releases for a project that the user has access
  538. // to view.
  539. $output .= '<p>'. t('There is no usage information for any release of this project.') .'</p>';
  540. return $output;
  541. }
  542. $output .= '<h3>'. t('Recent release usage') .'</h3>';
  543. $output .= theme('table', $release_header, $release_rows, array('id' => 'project-usage-project-releases'));
  544. $output .= '<h3>'. t('Weekly project usage') .'</h3>';
  545. $output .= theme('project_usage_chart_by_release', t('Weekly @project usage by API version', array('@project' => $project->title)), $project_header, $project_rows);
  546. $output .= theme('table', $project_header, $project_rows, array('id' => 'project-usage-project-api'));
  547. return $output;
  548. }
  549. /**
  550. * Theme the output of the project/usage/<release nid> page.
  551. *
  552. * @param $project
  553. * A fully loaded $node object for a project.
  554. * @param $release
  555. * A fully loaded $node object for a release.
  556. * @param $header
  557. * A table header for the release usage table.
  558. * @param $rows
  559. * Table rows for the release usage table.
  560. */
  561. function theme_project_usage_release_page($project, $release, $header, $rows) {
  562. $output = theme('project_usage_header_links', $project, $release);
  563. // If there is no usage information for a release, don't just
  564. // display an empty usage table.
  565. if (empty($rows)) {
  566. $output .= '<p>' . t('There is no usage information for this release.') . '</p>';
  567. return $output;
  568. }
  569. $output .= theme('project_usage_chart_by_release', t('Weekly @release usage', array('@release' => $release->title)), $header, $rows);
  570. $output .= theme('table', $header, $rows, array('id' => 'project-usage-release'));
  571. return $output;
  572. }
  573. /**
  574. * Create list of links at the top of a usage statistics page.
  575. *
  576. * @param $project
  577. * A fully loaded $node object for a project.
  578. * @param $release
  579. * If the current statistics page is for a release, the fully loaded $node
  580. * object for that release.
  581. *
  582. * @return
  583. * Themed HTML of a list of links for the top of a statistics page.
  584. */
  585. function theme_project_usage_header_links($project, $release = NULL) {
  586. $links = array();
  587. $links[] = l(t('%project_name project page', array('%project_name' => $project->title)), 'node/'. $project->nid, array(), NULL, NULL, FALSE, TRUE);
  588. if (!empty($release)) {
  589. $links[] = l(t('%release_name release page', array('%release_name' => $release->title)), 'node/' . $release->nid, array(), NULL, NULL, FALSE, TRUE);
  590. $links[] = l(t('All %project_name usage statistics', array('%project_name' => $project->title)), 'project/usage/' . $project->uri, array(), NULL, NULL, FALSE, TRUE);
  591. }
  592. $links[] = l(t('Usage statistics for all projects'), 'project/usage');
  593. return theme('item_list', $links);
  594. }
  595. /**
  596. * Implementation of hook_project_page_link_alter().
  597. */
  598. function project_usage_project_page_link_alter($node, &$links) {
  599. if (user_access('view project usage') && $node->releases) {
  600. $links['resources']['links']['project_usage'] = l(t('View usage statistics'), 'project/usage/' . $node->uri);
  601. }
  602. }
  603. /**
  604. * Convert the usage table data into a Google Chart image.
  605. *
  606. * First column should be the weeks, each subsequent column should be a data
  607. * series.
  608. *
  609. * @param $header
  610. * A table header for the weekly usage table.
  611. * @param $rows
  612. * Table rows for the weekly usage table.
  613. * @return
  614. * An HTML IMG element, or empty string if there is an error such as
  615. * insufficent data.
  616. */
  617. function theme_project_usage_chart_by_release($title, $header, $rows) {
  618. // Make sure we have enough data to make a useful chart.
  619. if (count($rows) < 2 || count($header) < 2) {
  620. return '';
  621. }
  622. // Reverse the order of the rows so it's oldest to newest.
  623. $rows = array_reverse($rows);
  624. // Pull the API versions from the table header for use as a legend. Since the
  625. // table is keyed strangely, make note of which tid is in which order so we
  626. // can efficiently iterate over the columns.
  627. $legend = array();
  628. $mapping = array();
  629. foreach ($header as $tid => $cell) {
  630. $legend[] = $cell['data'];
  631. $mapping[] = $tid;
  632. }
  633. // Drop the date column from the legend and mapping since it's the other axis.
  634. unset($legend[0]);
  635. unset($mapping[0]);
  636. // Rotate the table so each series is in a row in the array and grab the
  637. // dates for use as axis labels.
  638. $series = array();
  639. $date_axis = array();
  640. foreach (array_values($rows) as $i => $row) {
  641. $date_axis[$i] = $row[0]['data'];
  642. foreach ($mapping as $j => $tid) {
  643. // FIXME: The table values have commas in them from number_format(). We
  644. // need to remove them because we'll use commas to separate the values
  645. // in the URL string. It might be better to pass in clean number values
  646. // and format them here rather than have to uncook them.
  647. $series[$j][$i] = (int) str_replace(',', '', $row[$tid]['data']);
  648. }
  649. }
  650. // Now convert the series into strings with the data. Along the way figure
  651. // out the range of data.
  652. $min = $max = 0;
  653. $data = array();
  654. foreach ($series as $s) {
  655. $data[] = implode(',', $s);
  656. $max = max($max, max($s));
  657. }
  658. // Round the max up to the next decimal place (3->10, 19->20, 8703->9000) so
  659. // that the labels have round numbers and the entire range is visible. We're
  660. // forced to build the number as a string because PHP's round() function
  661. // can round down (3->0, not 3->10) and it returns floats that loose
  662. // precision (causing 90,000 to display as 89,999.99). We pull off the first
  663. // digit, add 1 to that, and then pad the rest with zeros.
  664. $zeros = strlen($max) - 1;
  665. $max = ($zeros > 0) ? (substr($max, 0, 1) + 1 . str_repeat('0', $zeros)) : 10;
  666. $value_axis = range($min, $max, '1'. str_repeat('0', $zeros));
  667. // Might need more colors than this.
  668. $colors = array('EDAA00', '0062A0', 'A17300', 'ED8200', '38B4BA', '215D6E');
  669. // The key values in this array are dictated by the Google Charts API:
  670. // http://code.google.com/apis/chart/
  671. $args = array(
  672. // Chart title.
  673. 'chtt' => check_plain($title),
  674. // Dimensions as width x height in pixels.
  675. 'chs' => '600x200',
  676. // Chart type
  677. 'cht' => 'lc',
  678. // Pick some colors.
  679. 'chco' => implode(',', array_slice($colors, 0, count($series))),
  680. 'chd' => 't:'. implode('|', $data),
  681. // Set the range of the chart
  682. 'chds' => implode(',', array_fill(0, count($series), $min .','. $max)),
  683. // Legend is the header titles after excluding the date.
  684. 'chdl' => implode('|', $legend),
  685. );
  686. project_usage_chart_axis_labels($args, array(
  687. 'x' => project_usage_chart_label_subset($date_axis, 5),
  688. 'y' => array_map('number_format', project_usage_chart_label_subset($value_axis, 5)),
  689. ));
  690. return theme('project_usage_chart', $args);
  691. }
  692. /**
  693. * Extract a subset of labels at regular intervals from a set.
  694. *
  695. * @param $labels
  696. * Array of values to choose from.
  697. * @param $n
  698. * Number of segments in the set. This number should divide $labels with no
  699. * remander.
  700. */
  701. function project_usage_chart_label_subset($labels, $n) {
  702. $subset = array();
  703. $count = count($labels) - 1;
  704. // We can't give them back more labels that we're given.
  705. $n = min($count, $n - 1);
  706. for ($i = 0; $i <= $n; $i++) {
  707. $subset[] = $labels[(int) ($count * ($i / $n))];
  708. }
  709. return $subset;
  710. }
  711. /**
  712. * Add axis labels to the chart arguments.
  713. *
  714. * @param $args
  715. * The array where the chart is being built.
  716. * @param $labels
  717. * Array keyed by axis (x, t, y, r) the value is an array of labels for that
  718. * axis.
  719. * @see http://code.google.com/apis/chart/labels.html#multiple_axes_labels
  720. */
  721. function project_usage_chart_axis_labels(&$args, $labels) {
  722. $keys = array_keys($labels);
  723. $args['chxt'] = implode(',', $keys);
  724. $l = array();
  725. foreach ($keys as $i => $key) {
  726. $l[$i] = $i .':|' . implode('|', $labels[$key]);
  727. }
  728. $args['chxl'] = implode('|', $l);
  729. }
  730. /**
  731. * Convert the array of Google Chart paramters into an image URL.
  732. *
  733. * @param $args
  734. * Array of key, value pairs to turn into a Google Charts image.
  735. * @return
  736. * HTML image element.
  737. */
  738. function theme_project_usage_chart($args) {
  739. $params = array();
  740. foreach ($args as $key => $value) {
  741. $params[] = $key .'='. $value;
  742. }
  743. // If the chart has a title use that for the image's alt and title values.
  744. $title = empty($args['chtt']) ? '' : $args['chtt'];
  745. return theme('image', 'http://chart.apis.google.com/chart?'. implode('&', $params), $title, $title, NULL, FALSE);
  746. }