blob: fd4577d8ed6ad8e79e35f27778a24872acfd7fd4 [file] [log] [blame]
<?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 );
}
}