Move subtask forecast to a plugin

Plugin repo: https://github.com/kanboard/plugin-subtask-forecast
This commit is contained in:
Frederic Guillot
2015-09-20 15:53:28 -04:00
parent a0124b45f9
commit 2021dccc5a
24 changed files with 275 additions and 172 deletions

View File

@@ -52,6 +52,12 @@ class Calendar extends Base
// Tasks with due date
$events = array_merge($events, $filter->copy()->filterByDueDateRange($start, $end)->toAllDayCalendarEvents());
$events = $this->hook->merge('controller:calendar:project:events', $events, array(
'project_id' => $project_id,
'start' => $start,
'end' => $end,
));
$this->response->json($events);
}
@@ -83,10 +89,11 @@ class Calendar extends Base
$events = array_merge($events, $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end));
}
// Subtask estimates
if ($this->config->get('calendar_user_subtasks_forecast') == 1) {
$events = array_merge($events, $this->subtaskForecast->getCalendarEvents($user_id, $end));
}
$events = $this->hook->merge('controller:calendar:user:events', $events, array(
'user_id' => $user_id,
'start' => $start,
'end' => $end,
));
$this->response->json($events);
}

View File

@@ -48,7 +48,7 @@ class Config extends Base
$values += array('integration_slack_webhook' => 0, 'integration_hipchat' => 0, 'integration_gravatar' => 0, 'integration_jabber' => 0);
break;
case 'calendar':
$values += array('calendar_user_subtasks_forecast' => 0, 'calendar_user_subtasks_time_tracking' => 0);
$values += array('calendar_user_subtasks_time_tracking' => 0);
break;
}

View File

@@ -55,7 +55,6 @@ use Pimple\Container;
* @property \Model\ProjectPermission $projectPermission
* @property \Model\Subtask $subtask
* @property \Model\SubtaskExport $subtaskExport
* @property \Model\SubtaskForecast $subtaskForecast
* @property \Model\SubtaskTimeTracking $subtaskTimeTracking
* @property \Model\Swimlane $swimlane
* @property \Model\Task $task

70
app/Core/Plugin/Hook.php Normal file
View File

@@ -0,0 +1,70 @@
<?php
namespace Core\Plugin;
/**
* Plugin Hooks Handler
*
* @package plugin
* @author Frederic Guillot
*/
class Hook
{
/**
* List of hooks
*
* @access private
* @var array
*/
private $hooks = array();
/**
* Bind something on a hook
*
* @access public
* @param string $hook
* @param mixed $value
*/
public function on($hook, $value)
{
if (! isset($this->hooks[$hook])) {
$this->hooks[$hook] = array();
}
$this->hooks[$hook][] = $value;
}
/**
* Get all bindings for a hook
*
* @access public
* @param string $hook
* @return array
*/
public function getListeners($hook)
{
return isset($this->hooks[$hook]) ? $this->hooks[$hook] : array();
}
/**
* Merge listener results with input array
*
* @access public
* @param string $hook
* @param array $values
* @param array $params
* @return array
*/
public function merge($hook, array &$values, array $params = array())
{
foreach ($this->getListeners($hook) as $listener) {
$result = call_user_func_array($listener, $params);
if (is_array($result) && ! empty($result)) {
$values = array_merge($values, $result);
}
}
return $values;
}
}

View File

@@ -10,8 +10,6 @@ namespace Helper;
*/
class Hook extends \Core\Base
{
private $hooks = array();
/**
* Render all attached hooks
*
@@ -24,10 +22,8 @@ class Hook extends \Core\Base
{
$buffer = '';
foreach ($this->hooks as $name => $template) {
if ($hook === $name) {
$buffer .= $this->template->render($template, $variables);
}
foreach ($this->hook->getListeners($hook) as $template) {
$buffer .= $this->template->render($template, $variables);
}
return $buffer;
@@ -43,7 +39,7 @@ class Hook extends \Core\Base
*/
public function attach($hook, $template)
{
$this->hooks[$hook] = $template;
$this->hook->on($hook, $template);
return $this;
}
}

View File

@@ -1,124 +0,0 @@
<?php
namespace Model;
use DateTime;
use DateInterval;
/**
* Subtask Forecast
*
* @package model
* @author Frederic Guillot
*/
class SubtaskForecast extends Base
{
/**
* Get not completed subtasks with an estimate sorted by postition
*
* @access public
* @param integer $user_id
* @return array
*/
public function getSubtasks($user_id)
{
return $this->db
->table(Subtask::TABLE)
->columns(Subtask::TABLE.'.id', Task::TABLE.'.project_id', Subtask::TABLE.'.task_id', Subtask::TABLE.'.title', Subtask::TABLE.'.time_estimated')
->join(Task::TABLE, 'id', 'task_id')
->asc(Task::TABLE.'.position')
->asc(Subtask::TABLE.'.position')
->gt(Subtask::TABLE.'.time_estimated', 0)
->eq(Subtask::TABLE.'.status', Subtask::STATUS_TODO)
->eq(Subtask::TABLE.'.user_id', $user_id)
->findAll();
}
/**
* Get the start date for the forecast
*
* @access public
* @param integer $user_id
* @return array
*/
public function getStartDate($user_id)
{
$subtask = $this->db->table(Subtask::TABLE)
->columns(Subtask::TABLE.'.time_estimated', SubtaskTimeTracking::TABLE.'.start')
->eq(SubtaskTimeTracking::TABLE.'.user_id', $user_id)
->eq(SubtaskTimeTracking::TABLE.'.end', 0)
->status('status', Subtask::STATUS_INPROGRESS)
->join(SubtaskTimeTracking::TABLE, 'subtask_id', 'id')
->findOne();
if ($subtask && $subtask['time_estimated'] && $subtask['start']) {
return date('Y-m-d H:i', $subtask['start'] + $subtask['time_estimated'] * 3600);
}
return date('Y-m-d H:i');
}
/**
* Get all calendar events according to the user timetable and the subtasks estimates
*
* @access public
* @param integer $user_id
* @param string $end End date of the calendar
* @return array
*/
public function getCalendarEvents($user_id, $end)
{
$events = array();
$start_date = new DateTime($this->getStartDate($user_id));
$timetable = $this->timetable->calculate($user_id, $start_date, new DateTime($end));
$subtasks = $this->getSubtasks($user_id);
$total = count($subtasks);
$offset = 0;
foreach ($timetable as $slot) {
$interval = $this->dateParser->getHours($slot[0], $slot[1]);
$start = $slot[0]->getTimestamp();
if ($slot[0] < $start_date) {
if (! $this->dateParser->withinDateRange($start_date, $slot[0], $slot[1])) {
continue;
}
$interval = $this->dateParser->getHours(new DateTime, $slot[1]);
$start = time();
}
while ($offset < $total) {
$event = array(
'id' => $subtasks[$offset]['id'].'-'.$subtasks[$offset]['task_id'].'-'.$offset,
'subtask_id' => $subtasks[$offset]['id'],
'title' => t('#%d', $subtasks[$offset]['task_id']).' '.$subtasks[$offset]['title'],
'url' => $this->helper->url->to('task', 'show', array('task_id' => $subtasks[$offset]['task_id'], 'project_id' => $subtasks[$offset]['project_id'])),
'editable' => false,
'start' => date('Y-m-d\TH:i:s', $start),
);
if ($subtasks[$offset]['time_estimated'] <= $interval) {
$start += $subtasks[$offset]['time_estimated'] * 3600;
$interval -= $subtasks[$offset]['time_estimated'];
$offset++;
$event['end'] = date('Y-m-d\TH:i:s', $start);
$events[] = $event;
}
else {
$subtasks[$offset]['time_estimated'] -= $interval;
$event['end'] = $slot[1]->format('Y-m-d\TH:i:s');
$events[] = $event;
break;
}
}
}
return $events;
}
}

View File

@@ -166,7 +166,6 @@ function version_69($pdo)
$result = $rq->fetch(PDO::FETCH_ASSOC);
$rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
$rq->execute(array('calendar_user_subtasks_forecast', isset($result['subtask_forecast']) && $result['subtask_forecast'] == 1 ? 1 : 0));
$rq->execute(array('calendar_user_subtasks_time_tracking', 0));
$rq->execute(array('calendar_user_tasks', 'date_started'));
$rq->execute(array('calendar_project_tasks', 'date_started'));

View File

@@ -161,7 +161,6 @@ function version_50($pdo)
$result = $rq->fetch(PDO::FETCH_ASSOC);
$rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
$rq->execute(array('calendar_user_subtasks_forecast', isset($result['subtask_forecast']) && $result['subtask_forecast'] == 1 ? 1 : 0));
$rq->execute(array('calendar_user_subtasks_time_tracking', 0));
$rq->execute(array('calendar_user_tasks', 'date_started'));
$rq->execute(array('calendar_project_tasks', 'date_started'));

View File

@@ -138,7 +138,6 @@ function version_68($pdo)
$result = $rq->fetch(PDO::FETCH_ASSOC);
$rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
$rq->execute(array('calendar_user_subtasks_forecast', isset($result['subtask_forecast']) && $result['subtask_forecast'] == 1 ? 1 : 0));
$rq->execute(array('calendar_user_subtasks_time_tracking', 0));
$rq->execute(array('calendar_user_tasks', 'date_started'));
$rq->execute(array('calendar_project_tasks', 'date_started'));

View File

@@ -41,7 +41,6 @@ class ClassProvider implements ServiceProviderInterface
'ProjectPermission',
'Subtask',
'SubtaskExport',
'SubtaskForecast',
'SubtaskTimeTracking',
'Swimlane',
'Task',
@@ -80,6 +79,9 @@ class ClassProvider implements ServiceProviderInterface
'Core\Cache' => array(
'MemoryCache',
),
'Core\Plugin' => array(
'Hook',
),
'Integration' => array(
'BitbucketWebhook',
'GithubWebhook',

View File

@@ -19,7 +19,7 @@
<li <?= $this->app->getRouterAction() === 'activity' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('My activity stream'), 'app', 'activity', array('user_id' => $user['id'])) ?>
</li>
<?= $this->hook->render('dashboard:sidebar') ?>
<?= $this->hook->render('template:dashboard:sidebar') ?>
</ul>
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>

View File

@@ -23,7 +23,6 @@
<h4><?= t('Subtasks time tracking') ?></h4>
<?= $this->form->checkbox('calendar_user_subtasks_time_tracking', t('Show subtasks based on the time tracking'), 1, $values['calendar_user_subtasks_time_tracking'] == 1) ?>
<?= $this->form->checkbox('calendar_user_subtasks_forecast', t('Show subtask estimates (forecast of future work)'), 1, $values['calendar_user_subtasks_forecast'] == 1) ?>
</div>
<div class="form-actions">

View File

@@ -34,7 +34,7 @@
<li>
<?= $this->url->link(t('Documentation'), 'doc', 'show') ?>
</li>
<?= $this->hook->render('config:sidebar') ?>
<?= $this->hook->render('template:config:sidebar') ?>
</ul>
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>

View File

@@ -13,7 +13,7 @@
<li <?= $this->app->getRouterAction() === 'summary' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('Daily project summary'), 'export', 'summary', array('project_id' => $project['id'])) ?>
</li>
<?= $this->hook->render('export:sidebar') ?>
<?= $this->hook->render('template:export:sidebar') ?>
</ul>
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>

View File

@@ -29,7 +29,7 @@
<title><?= isset($title) ? $this->e($title) : 'Kanboard' ?></title>
<?= $this->hook->render('layout:head') ?>
<?= $this->hook->render('template:layout:head') ?>
</head>
<body data-status-url="<?= $this->url->href('app', 'status') ?>"
data-login-url="<?= $this->url->href('auth', 'login') ?>"
@@ -40,7 +40,7 @@
<?php if (isset($no_layout) && $no_layout): ?>
<?= $content_for_layout ?>
<?php else: ?>
<?= $this->hook->render('layout:top') ?>
<?= $this->hook->render('template:layout:top') ?>
<?= $this->render('header', array(
'title' => $title,
'description' => isset($description) ? $description : '',
@@ -50,7 +50,7 @@
<?= $this->app->flashMessage() ?>
<?= $content_for_layout ?>
</section>
<?= $this->hook->render('layout:bottom') ?>
<?= $this->hook->render('template:layout:bottom') ?>
<?php endif ?>
</body>
</html>

View File

@@ -9,7 +9,7 @@
</li>
<?php endif ?>
<?= $this->hook->render('project:dropdown', array('project' => $project)) ?>
<?= $this->hook->render('template:project:dropdown', array('project' => $project)) ?>
<?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
<li>

View File

@@ -49,7 +49,7 @@
<?php endif ?>
<?php endif ?>
<?= $this->hook->render('project:sidebar') ?>
<?= $this->hook->render('template:project:sidebar') ?>
</ul>
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>

View File

@@ -25,6 +25,6 @@
<?= $this->url->link(t('Closed tasks'), 'projectuser', 'closed', $filter) ?>
</li>
<?= $this->hook->render('project-user:sidebar') ?>
<?= $this->hook->render('template:project-user:sidebar') ?>
</ul>
</div>

View File

@@ -19,7 +19,7 @@
</li>
<?php endif ?>
<?= $this->hook->render('task:sidebar:information') ?>
<?= $this->hook->render('template:task:sidebar:information') ?>
</ul>
<h2><?= t('Actions') ?></h2>
<ul>
@@ -69,7 +69,7 @@
</li>
<?php endif ?>
<?= $this->hook->render('task:sidebar:actions') ?>
<?= $this->hook->render('template:task:sidebar:actions') ?>
</ul>
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>

View File

@@ -21,7 +21,7 @@
</li>
<?php endif ?>
<?= $this->hook->render('user:sidebar:information') ?>
<?= $this->hook->render('template:user:sidebar:information') ?>
</ul>
<h2><?= t('Actions') ?></h2>
@@ -67,7 +67,7 @@
</li>
<?php endif ?>
<?= $this->hook->render('user:sidebar:actions', array('user' => $user)) ?>
<?= $this->hook->render('template:user:sidebar:actions', array('user' => $user)) ?>
<?php if ($this->user->isAdmin() && ! $this->user->isCurrentUser($user['id'])): ?>
<li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'remove' ? 'class="active"' : '' ?>>