Skip to content

Commit dec5183

Browse files
author
Michael Peters
committed
Initial commit on burndown project
0 parents  commit dec5183

8 files changed

+440
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.phutil_module_cache

__phutil_library_init__.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?php
2+
3+
phutil_register_library('burndown', __FILE__);

__phutil_library_map__.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/**
4+
* This file is automatically generated. Use 'arc liberate' to rebuild it.
5+
* @generated
6+
* @phutil-library-version 2
7+
*/
8+
9+
phutil_register_library_map(array(
10+
'__library_version__' => 2,
11+
'class' =>
12+
array(
13+
'BurndownApplication' => 'src/BurndownApplication.php',
14+
'BurndownController' => 'src/BurndownController.php',
15+
'SprintEndDateField' => 'src/SprintEndDateField.php',
16+
'SprintProjectCustomField' => 'src/SprintProjectCustomField.php',
17+
'SprintStartDateField' => 'src/SprintStartDateField.php',
18+
),
19+
'function' =>
20+
array(
21+
),
22+
'xmap' =>
23+
array(
24+
'BurndownApplication' => 'PhabricatorApplication',
25+
'BurndownController' => 'PhabricatorController',
26+
'SprintEndDateField' => 'SprintProjectCustomField',
27+
'SprintProjectCustomField' =>
28+
array(
29+
0 => 'PhabricatorProjectCustomField',
30+
1 => 'PhabricatorStandardCustomFieldInterface',
31+
),
32+
'SprintStartDateField' => 'SprintProjectCustomField',
33+
),
34+
));

src/BurndownApplication.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
final class BurndownApplication extends PhabricatorApplication {
4+
5+
public function getName() {
6+
return pht('Burndown Extensions');
7+
}
8+
9+
public function getBaseURI() {
10+
return '/burn/';
11+
}
12+
13+
14+
public function getRoutes() {
15+
return array(
16+
'/burn/' => array(
17+
'burndown/(?P<id>\d+)/' => 'BurndownController',
18+
),
19+
);
20+
}
21+
22+
}

src/BurndownController.php

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
<?php
2+
3+
final class BurndownController extends PhabricatorController {
4+
5+
private $projectID;
6+
7+
public function willProcessRequest(array $data) {
8+
$this->projectID = $data['id'];
9+
}
10+
11+
public function processRequest() {
12+
13+
$request = $this->getRequest();
14+
$viewer = $request->getUser();
15+
16+
// Load the project we're looking at, based on the project ID in the URL.
17+
$project = id(new PhabricatorProjectQuery())
18+
->setViewer($viewer)
19+
->withIDs(array($this->projectID))
20+
->executeOne();
21+
if (!$project) {
22+
return new Aphront404Response();
23+
}
24+
25+
// Load the data for the chart. This approach tries to be simple, but loads
26+
// and processes large amounts of unnecessary data, so it is not especially
27+
// fast. Some performance improvements can be made at the cost of fragility
28+
// by using raw SQL; real improvements can be made once Facts comes online.
29+
30+
// First, load *every task* in the project. We have to do something like
31+
// this because there's no straightforward way to determine which tasks
32+
// have activity in the project period.
33+
$tasks = id(new ManiphestTaskQuery())
34+
->setViewer($viewer)
35+
->withAnyProjects(array($project->getPHID()))
36+
->execute();
37+
38+
// Now load *every transaction* for those tasks. This loads all the
39+
// comments, etc., for every one of the tasks. Again, not very fast, but
40+
// we largely do not have ways to select this data more narrowly yet.
41+
if ($tasks) {
42+
$task_phids = mpull($tasks, 'getPHID');
43+
44+
$xactions = id(new ManiphestTransactionQuery())
45+
->setViewer($viewer)
46+
->withObjectPHIDs($task_phids)
47+
->execute();
48+
}
49+
50+
// Examine all the transactions and extract "events" out of them. These are
51+
// times when a task was opened or closed. Make some effort to also track
52+
// "scope" events (when a task was added or removed from a project).
53+
$scope_phids = array($project->getPHID());
54+
$events = $this->extractEvents($xactions, $scope_phids);
55+
56+
57+
// TODO: Render an actual chart. For now, I'm rendering a table with the
58+
// data in it instead.
59+
60+
$xactions = mpull($xactions, null, 'getPHID');
61+
$tasks = mpull($tasks, null, 'getPHID');
62+
63+
$rows = array();
64+
foreach ($events as $event) {
65+
$task_phid = $xactions[$event['transactionPHID']]->getObjectPHID();
66+
$task = $tasks[$task_phid];
67+
68+
$rows[] = array(
69+
phabricator_datetime($event['epoch'], $viewer),
70+
$event['type'],
71+
phutil_tag(
72+
'a',
73+
array(
74+
'href' => '/'.$task->getMonogram(),
75+
),
76+
$task->getMonogram().': '.$task->getTitle()),
77+
);
78+
}
79+
80+
$table = id(new AphrontTableView($rows))
81+
->setHeaders(
82+
array(
83+
pht('When'),
84+
pht('Type'),
85+
pht('Task'),
86+
))
87+
->setColumnClasses(
88+
array(
89+
'',
90+
'',
91+
'wide',
92+
));
93+
94+
$box = id(new PHUIObjectBoxView())
95+
->setHeaderText(pht('Raw Data for Eventual Chart'))
96+
->appendChild($table);
97+
98+
$crumbs = $this->buildApplicationCrumbs();
99+
$crumbs->addTextCrumb(
100+
$project->getName(),
101+
'/project/view/'.$project->getID());
102+
$crumbs->addTextCrumb(pht('Burndown'));
103+
104+
return $this->buildApplicationPage(
105+
array(
106+
$crumbs,
107+
$box,
108+
),
109+
array(
110+
'title' => array(pht('Burndown'), $project->getName()),
111+
'device' => true,
112+
));
113+
}
114+
115+
116+
/**
117+
* Extract important events (the times when tasks were opened or closed)
118+
* from a list of transactions.
119+
*
120+
* @param list<ManiphestTransaction> List of transactions.
121+
* @param list<phid> List of project PHIDs to emit "scope" events for.
122+
* @return list<dict> Chronologically sorted events.
123+
*/
124+
private function extractEvents(array $xactions, array $scope_phids) {
125+
assert_instances_of($xactions, 'ManiphestTransaction');
126+
127+
$scope_phids = array_fuse($scope_phids);
128+
129+
$events = array();
130+
foreach ($xactions as $xaction) {
131+
$old = $xaction->getOldValue();
132+
$new = $xaction->getNewValue();
133+
134+
$event_type = null;
135+
switch ($xaction->getTransactionType()) {
136+
case ManiphestTransaction::TYPE_STATUS:
137+
$old_is_closed = ($old === null) ||
138+
ManiphestTaskStatus::isClosedStatus($old);
139+
$new_is_closed = ManiphestTaskStatus::isClosedStatus($new);
140+
141+
if ($old_is_closed == $new_is_closed) {
142+
// This was just a status change from one open status to another,
143+
// or from one closed status to another, so it's not an event we
144+
// care about.
145+
break;
146+
}
147+
148+
if ($new_is_closed) {
149+
$event_type = 'close';
150+
} else {
151+
$event_type = 'open';
152+
}
153+
break;
154+
155+
case ManiphestTransaction::TYPE_PROJECTS:
156+
$old = array_fuse($old);
157+
$new = array_fuse($new);
158+
159+
$in_old_scope = array_intersect_key($scope_phids, $old);
160+
$in_new_scope = array_intersect_key($scope_phids, $new);
161+
162+
if ($in_new_scope && !$in_old_scope) {
163+
$event_type = 'scope-expand';
164+
} else if ($in_old_scope && !$in_new_scope) {
165+
// NOTE: We will miss some of these events, becuase we are only
166+
// examining tasks that are currently in the project. If a task
167+
// is removed from the project and not added again later, it will
168+
// just vanish from the chart completely, not show up as a
169+
// scope contraction. We can't do better until the Facts application
170+
// is avialable without examining *every* task.
171+
$event_type = 'scope-contract';
172+
}
173+
break;
174+
175+
// TODO: To find events where scope was changed by altering the number
176+
// of points for a task, you can examine custom field transactions,
177+
// which have type PhabricatorTransactions::TYPE_CUSTOMFIELD.
178+
179+
default:
180+
// This is something else (comment, subscription change, etc) that
181+
// we don't care about for now.
182+
break;
183+
}
184+
185+
// If we found some kind of event that we care about, stick it in the
186+
// list of events.
187+
if ($event_type !== null) {
188+
$events[] = array(
189+
'transactionPHID' => $xaction->getPHID(),
190+
'epoch' => $xaction->getDateCreated(),
191+
'type' => $event_type,
192+
);
193+
}
194+
}
195+
196+
// Sort all events chronologically.
197+
$events = isort($events, 'epoch');
198+
199+
return $events;
200+
}
201+
202+
203+
}

src/SprintEndDateField.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
final class SprintEndDateField extends SprintProjectCustomField {
4+
5+
public function __construct() {
6+
$proxy = id(new PhabricatorStandardCustomFieldDate())
7+
->setFieldKey($this->getFieldKey())
8+
->setApplicationField($this)
9+
->setFieldConfig(array(
10+
'name' => $this->getFieldName(),
11+
'description' => $this->getFieldDescription(),
12+
));
13+
14+
$this->setProxy($proxy);
15+
}
16+
17+
// == General field identity stuff
18+
public function getFieldKey() {
19+
return 'isdc:sprint:enddate';
20+
}
21+
22+
public function getFieldName() {
23+
return 'Sprint End Date';
24+
}
25+
26+
public function getFieldDescription() {
27+
return 'When a sprint ends';
28+
}
29+
30+
public function renderPropertyViewValue(array $handles) {
31+
if (!$this->shouldShowSprintFields()) {
32+
return;
33+
}
34+
35+
if ($this->getProxy()->getFieldValue())
36+
{
37+
return parent::renderPropertyViewValue($handles);
38+
}
39+
40+
return null;
41+
}
42+
43+
// == Search
44+
public function shouldAppearInApplicationSearch()
45+
{
46+
return true;
47+
}
48+
49+
}

src/SprintProjectCustomField.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
abstract class SprintProjectCustomField extends PhabricatorProjectCustomField
4+
implements PhabricatorStandardCustomFieldInterface {
5+
6+
/**
7+
* Use this function to determine whether to show sprint fields
8+
*
9+
* public function renderPropertyViewValue(array $handles) {
10+
* if (!$this->shouldShowSprintFields()) {
11+
* return
12+
* }
13+
* // Actually show something
14+
*
15+
* NOTE: You can NOT call this in functions like "shouldAppearInEditView" because
16+
* $this->getObject() is not available yet.
17+
*
18+
*/
19+
protected function shouldShowSprintFields()
20+
{
21+
return (strpos($this->getObject()->getName(),'Sprint') !== FALSE);
22+
}
23+
24+
/**
25+
* As nearly as I can tell, this is never actually used, but is required in order to
26+
* implement PhabricatorStandardCustomFieldInterface
27+
*/
28+
public function getStandardCustomFieldNamespace() {
29+
return 'projects';
30+
}
31+
32+
/**
33+
* Each subclass must either declare a proxy or implement this method
34+
*/
35+
public function renderPropertyViewLabel() {
36+
if (!$this->shouldShowSprintFields()) {
37+
return;
38+
}
39+
40+
if ($this->getProxy()) {
41+
return $this->getProxy()->renderPropertyViewLabel();
42+
}
43+
return $this->getFieldName();
44+
45+
}
46+
47+
/**
48+
* Each subclass must either declare a proxy or implement this method
49+
*/
50+
public function renderPropertyViewValue(array $handles) {
51+
if (!$this->shouldShowSprintFields()) {
52+
return;
53+
}
54+
55+
if ($this->getProxy()) {
56+
return $this->getProxy()->renderPropertyViewValue($handles);
57+
}
58+
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
59+
}
60+
61+
// == Edit View
62+
public function shouldAppearInEditView() {
63+
return true;
64+
}
65+
66+
/**
67+
* Each subclass must either declare a proxy or implement this method
68+
*/
69+
public function renderEditControl(array $handles) {
70+
if (!$this->shouldShowSprintFields()) {
71+
return;
72+
}
73+
74+
if ($this->getProxy()) {
75+
return $this->getProxy()->renderEditControl($handles);
76+
}
77+
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
78+
}
79+
}

0 commit comments

Comments
 (0)