| <?php |
| /** |
| * Copyright (c) Eclipse Foundation and others. |
| * |
| * This program and the accompanying materials are made |
| * available under the terms of the Eclipse Public License 2.0 |
| * which is available at https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| */ |
| |
| require_once dirname ( __FILE__ ) . '/Project.class.php'; |
| require_once dirname ( __FILE__ ) . '/License.class.inc'; |
| require_once dirname ( __FILE__ ) . '/common.php'; |
| require_once dirname ( __FILE__ ) . '/debug.php'; |
| |
| /** |
| * The ProjectStatusReporter class provides behaviour to load |
| * information about some subset of the projects, and then |
| * extract and provide that information in various ways. |
| * Several "alerts" and "columns" are defined, each of which knows how |
| * to extract and present some piece of the data. |
| * |
| * An instance is loaded with various "collectors", "alerts" and "values". |
| * |
| * Collectors are a means to load an instance with data; the primary |
| * intent being to preload and cache data rather than compute/load (and |
| * potentially recompute/load) as it is needed. |
| * |
| * Alerts are, essentially, a message and a callback that determines |
| * whether or not to display the message. Alters may be used, for |
| * example, to identify whether or not the project uses and old version |
| * of a license and so must be updated. The callback associated with |
| * an alert is a callable that takes an instance of Project as a parameter. |
| * |
| * Values provide a means of extracting values to display. A function |
| * may be provided to compute the value. In the absense of a function, |
| * the array key is used as an index into the values cached for the |
| * project. |
| */ |
| class ProjectStatusReporter { |
| var $project; |
| var $data; |
| var $collectors; |
| var $alerts; |
| var $values; |
| var $charts; |
| |
| function __construct($id) { |
| $this->collectors = array( |
| 'commits' => function($id) { |
| $sql = " |
| select distinct |
| p.id as id, |
| min(date(ps.initialCommit)) as commits_initial, |
| max(date(ps.latestCommit)) as commits_latest, |
| sum(distinct ps.totalCommits) as commits |
| from Project as p |
| join ProjectRollup as pr on p.id=pr.project |
| left join ProjectStats as ps on pr.subproject=ps.project |
| where p.id = ':id:' |
| group by p.id"; |
| |
| $result = array(); |
| query ( 'dashboard', $sql, array(':id:' => $id), function ($row) use (&$result) { |
| $result = $row; |
| }); |
| return $result; |
| }, |
| 'reviews' => function($id) { |
| $sql = " |
| select distinct |
| max(rev.date) as review, |
| max(rev.date) >= date_sub(now(), interval 1 year) as goodstanding |
| from Project as p |
| left join ProjectReviews as rev on p.id=rev.project |
| and rev.type in ('progress', 'release','graduation', 'creation', 'other') |
| and rev.status = 'success' |
| where p.id = ':id:'"; |
| |
| $result = array(); |
| query ( 'dashboard', $sql, array(':id:' => $id), function ($row) use (&$result) { |
| $result = $row; |
| } ); |
| return $result; |
| }, |
| 'active' => function($id) { |
| $sql = " |
| select distinct |
| count(distinct pca.company) as organizations, |
| count(distinct pda.login) as contributors |
| from Project as p |
| left join ProjectCompanyActivity as pca on p.id=pca.project |
| left join ProjectCommitterActivity as pda on p.id=pda.project |
| where p.id = ':id:'"; |
| |
| $result = array(); |
| query ( 'dashboard', $sql, array(':id:' => $id), function ($row) use (&$result) { |
| $result = $row; |
| } ); |
| return $result; |
| }, |
| 'license' => function($id) { |
| $licenses = License::getLicensesForProject($id); |
| $spdx = License::getSPDXExpression($licenses); |
| return array('license' => $spdx); |
| } |
| ); |
| $this->alerts = array( |
| 'archived' => array( |
| 'message' => 'The project is archived', |
| 'when' => function($project) { |
| return $project->isArchived(); |
| } |
| ), |
| 'since' => array( |
| 'message' => 'The project has not been provisioned', |
| 'when' => function($project) { |
| return $project->getProvisionedDate() == null; |
| } |
| ), |
| 'provisioned' => array( |
| 'message' => 'The project was provisioned in the last three months', |
| 'when' => function($project) { |
| if (!$project->getProvisionedDate()) return false; |
| return $project->getProvisionedDate() > strtotime('-3 months'); |
| } |
| ), |
| 'epl-1.0' => array( |
| 'message' => 'The project uses the EPL-1.0', |
| 'when' => function ($project) { |
| $licenses = License::getLicensesForProject($project->getId()); |
| foreach ($licenses as $license) { |
| if ($license->getId() == 'EPL-1.0') return true; |
| } |
| return false; |
| } |
| ), |
| 'dormant' => array( |
| 'message' => 'The project appears to be dormant', |
| 'when' => function ($project) { |
| $latest = $this->getProjectData('commits_latest'); |
| return strtotime($latest) < strtotime('-1 year'); |
| } |
| ), |
| 'review' => array( |
| 'message' => 'The project is due for a progress review.', |
| 'when' => function($project) { |
| return !$this->getProjectData('goodstanding'); |
| } |
| ), |
| 'no_project_lead' => array( |
| 'message' => 'The project has no project leads', |
| 'when' => function ($project) { |
| return !$project->hasProjectLeads(); |
| } |
| ), |
| 'no_committers' => array( |
| 'message' => 'The project has no committers', |
| 'when' => function ($project) { |
| return !$project->hasCommitters(); |
| } |
| ) |
| ); |
| |
| /* |
| * Note that some of the "values" have a "sort" entry. This is from a |
| * previous use of the ProjectStatusReporter that rendered values in |
| * a table with the ability to sort the table on any column that provides |
| * a "sort" entry. The basic idea being that the function answers the |
| * sort value of a particular column from a particular project. |
| * The values are currently ignored; I decided to leave them in for |
| * now as I may opt to do something similar again. |
| */ |
| |
| $this->values = array ( |
| 'url' => array ( |
| 'label' => 'Project Page (PMI)', |
| 'function' => function ($value, $key, $project) { |
| return "<a href=\"$value\">{$project['name']}</a>"; |
| } |
| ), |
| 'tlp' => array ( |
| 'label' => 'Top-Level', |
| 'function' => function ($value, $key, $project) { |
| $tlp = $project['project']->getTopLevelProject(); |
| return "<a href=\"{$tlp->getUrl()}\">{$tlp->getFormalName()}</a>"; |
| } |
| ), |
| 'started' => array ( |
| 'label' => 'Started', |
| 'function' => function($value, $key, $data) { |
| if (!$project = $data['project']) return null; |
| if (!$started = $project->getProvisionedDate()) return null; |
| return date('Y-m-d', $started); |
| } |
| ), |
| 'license' => array ( |
| 'label' => 'License' |
| ), |
| 'openchain' => array( |
| 'label' => 'OpenChain Compliance', |
| 'function' => function($value, $key, $project) { |
| if ($project['goodstanding']) return '2.1'; |
| return 'Unverified'; |
| } |
| ), |
| 'specification' => array ( |
| 'label' => 'Specification Working Group', |
| 'function' => function ($value, $key, $project) { |
| return $project['project']->getSpecificationWorkingGroupName(); |
| } |
| ), |
| 'phase' => array ( |
| 'label' => 'Phase', |
| 'function' => function ($value, $key, $project) { |
| if ($project['project']->isInIncubationPhase ()) return 'Incubating'; |
| if ($project['project']->isArchived()) return 'Archived'; |
| return'Regular'; |
| } |
| ), |
| 'review' => array( |
| 'label' => 'Last Review' |
| |
| ), |
| 'contributors' => array ( |
| 'label' => 'Active Contributors', |
| 'description' => 'Based on commits over the last three months' |
| ), |
| 'organizations' => array ( |
| 'label' => 'Active Member Companies', |
| 'description' => 'Based on commits over the last three months' |
| ), |
| 'commits_initial' => array ( |
| 'label' => 'Initial Commit' |
| ), |
| 'commits_latest' => array ( |
| 'label' => 'Latest Commit' |
| ), |
| 'commits' => array ( |
| 'label' => 'Commits Count', |
| 'function' => function ($value, $key, $project) { |
| if ($value !== null) |
| return number_format ( (int)$value ); |
| return '--'; |
| }, |
| 'sort' => function ($value, $key, $project) { |
| return ( int ) $value; |
| }, |
| 'class' => 'column-align-right' |
| ), |
| 'commits_latency' => array ( |
| 'label' => 'Time Since Last Commit', |
| 'function' => function ($value, $key, $project) { |
| if ($latest = @$project ['commits_latest']) |
| return self::getTimeSince ( $latest ); |
| return '--'; |
| }, |
| 'sort' => function ($value, $key, $project) { |
| if ($latest = @$project ['commits_latest']) |
| return ( int ) (time () - strtotime ( $latest )); |
| return 0; |
| }, |
| 'class' => 'column-align-right' |
| ) |
| ); |
| $this->charts = array( |
| 'project_activity' => function(Project $project) { |
| return ChartBuilder::named ( "project_activity_" . rand () ) |
| ->title ( "Commit Activity" ) |
| ->description ( "Overall committer activity by month based on commits made |
| against project repositories from :start to :end." ) |
| ->query ( 'dashboard', " |
| select |
| periods.period as period, |
| if (commits.count is null, 0, commits.count) as count |
| from ( |
| select distinct period |
| from ProjectCommitActivity |
| where |
| period |
| between ( |
| select |
| if(max(period) < date_format(':start','%Y%m'), |
| date_format(':end','%Y%m'), |
| greatest(min(period), date_format(':start','%Y%m'))) |
| from ProjectCommitActivity |
| where project=':id') |
| and date_format(':end','%Y%m')) as periods |
| left join |
| ProjectCommitActivity as commits |
| on (periods.period = commits.period and commits.project=':id') |
| order by period |
| " ) |
| ->substitute ( ':id', $project->getId () ) |
| ->substitute ( ':start', (new DateTime())->modify('-4 Years')->format('Y-m-d') ) |
| ->substitute ( ':end', (new DateTime())->modify('-1 Months')->format('Y-m-d') ) |
| ->column ( 'Month', 'period', 'string', function ($value) { |
| return asYearMonth ( $value ); |
| } ) |
| ->column ( 'Commits', 'count', 'number' ) |
| ->columnChart () |
| ->height ( 400 ) |
| ->width ( 640 ) |
| ->option ( 'legend', 'none' ); |
| }, |
| 'project_contributors' => function(Project $project) { |
| return ChartBuilder::named('project_contributors_' . rand()) |
| ->title("Project Contributors by Quarter") |
| ->description( |
| "Quarterly statistics on contribution (distinct contributors).") |
| ->query('dashboard', " |
| select |
| quarter, |
| contributors, |
| committers, |
| companies |
| from ProjectQuarterlyContributionSummary |
| where project=':id' |
| and quarter |
| between concat(year(':start'),'Q', quarter(':start')) |
| and concat(year(':end'),'Q', quarter(':end')) |
| ") |
| ->column('Quarter', 'quarter', 'string', function($value) {return asYearMonth($value);}) |
| ->column('Contributors', 'contributors', 'number') |
| ->column('Committers', 'committers', 'number') |
| ->column('Companies', 'companies', 'number') |
| ->substitute ( ':id', $project->getId () ) |
| ->substitute ( ':start', '2000-01-01') |
| ->substitute ( ':end', (new DateTime())->modify('-3 Months')->format('Y-m-d')) |
| ->height ( 400 ) |
| ->width ( 640 ); |
| } |
| ); |
| $this->loadProject($id); |
| } |
| |
| function values($function) { |
| foreach ( $this->values as $id => $config ) { |
| call_user_func($function, new ProjectStatusReporterColumn ( $id, $config, $this->data ) ); |
| } |
| } |
| |
| function charts($function) { |
| foreach ( $this->charts as $callable ) { |
| $builder = call_user_func($callable, $this->project); |
| $builder->titleString("<h4>:title</h4>"); |
| call_user_func($function, $builder); |
| } |
| } |
| |
| /** |
| * Generate a list of alerts for a particular project. |
| * |
| * @param Project $project |
| * @return string[] |
| */ |
| function alerts() { |
| if ($this->project->isTopLevel()) return; |
| |
| $alerts = array(); |
| foreach ( $this->alerts as $config ) { |
| if (call_user_func ($config['when'], $this->project)) { |
| $alerts[] = $config['message']; |
| } |
| } |
| return $alerts; |
| } |
| |
| public function getColumnDefinition($key) { |
| return $this->values [$key]; |
| } |
| |
| public static function getTimeSince($date) { |
| if (! $date) |
| return 0; |
| $milliseconds = date ( time () - strtotime ( $date ) ); |
| $days = number_format ( ceil ( $milliseconds / (60 * 60 * 24) ) ); |
| return "$days days"; |
| } |
| |
| public function filter($info) { |
| foreach ( $_GET as $key => $value ) { |
| if (isset ( $info [$key] )) { |
| if ($info [$key] != $value) |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * This function answers the value (if any) with a named |
| * index associated with a particular instance of Project. |
| * The values that are available are dependent on the data |
| * that is loaded into the instance. |
| * |
| * @param Project $project |
| * @param string $key |
| * @return mixed|NULL |
| */ |
| private function getProjectData($key) { |
| return @$this->data[$key]; |
| } |
| |
| private function loadProject($id) { |
| $project = Project::getProject($id); |
| |
| $this->project = $project; |
| $this->data = array ( |
| 'project' => $project, |
| 'id' => $id, |
| 'name' => $project->getName (), |
| 'top' => $project->getTopLevelProject ()->getId (), |
| 'topName' => preg_replace ( '/ Root$/', '', $project->getTopLevelProject ()->getName () ), |
| 'phase' => $project->isInIncubationPhase () ? 'Incubating' : 'Regular', |
| 'url' => $project->getUrl () |
| ); |
| |
| // FIXME Experimental: call_user_func does pass-by-value. |
| // Need to sort out pass-by-reference. |
| foreach($this->collectors as $collector) { |
| $this->data += call_user_func($collector, $id); |
| } |
| } |
| |
| /** |
| * This function answers a string containing the quoted ids for |
| * the projects. |
| * |
| * @param mixed $projects |
| * @return string |
| */ |
| private function getProjectIds(&$projects) { |
| $ids = array (); |
| foreach ( $projects as $project ) { |
| $ids [] = "'{$project['id']}'"; |
| } |
| return implode ( ',', $ids ); |
| } |
| |
| public static function valueOrDashes($value) { |
| if (! $value) |
| return '--'; |
| return $value; |
| } |
| } |
| class ProjectStatusReporterColumn { |
| var $id, $config, $data; |
| |
| public function __construct($id, $config, $data) { |
| $this->id = $id; |
| $this->config = $config; |
| $this->data = $data; |
| } |
| |
| public function getTitle() { |
| return $this->config ['label']; |
| } |
| |
| public function getDescription() { |
| return @$this->config ['description']; |
| } |
| |
| public function getValue() { |
| $value = @$this->data[$this->id]; |
| if (isset ( $this->config ['function'] )) { |
| $function = $this->config ['function']; |
| $value = call_user_func($function, $value, $this->id, $this->data ); |
| } |
| |
| return ProjectStatusReporter::valueOrDashes ( $value ); |
| } |
| } |