Move subtask forecast to a plugin
Plugin repo: https://github.com/kanboard/plugin-subtask-forecast
This commit is contained in:
@@ -10,9 +10,10 @@ New features:
|
|||||||
* Added API procedures: getMyOverdueTasks, getOverdueTasksByProject and GetMyProjects
|
* Added API procedures: getMyOverdueTasks, getOverdueTasksByProject and GetMyProjects
|
||||||
* Added user API access for procedure getProjectActivity()
|
* Added user API access for procedure getProjectActivity()
|
||||||
|
|
||||||
Breaking changes:
|
Core functionalities moved to plugins:
|
||||||
|
|
||||||
* Budget planning is now a plugin and it's not part of core
|
* Budget planning: https://github.com/kanboard/plugin-budget
|
||||||
|
* SubtaskForecast: https://github.com/kanboard/plugin-subtask-forecast
|
||||||
|
|
||||||
Improvements:
|
Improvements:
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ class Calendar extends Base
|
|||||||
// Tasks with due date
|
// Tasks with due date
|
||||||
$events = array_merge($events, $filter->copy()->filterByDueDateRange($start, $end)->toAllDayCalendarEvents());
|
$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);
|
$this->response->json($events);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,10 +89,11 @@ class Calendar extends Base
|
|||||||
$events = array_merge($events, $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end));
|
$events = array_merge($events, $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subtask estimates
|
$events = $this->hook->merge('controller:calendar:user:events', $events, array(
|
||||||
if ($this->config->get('calendar_user_subtasks_forecast') == 1) {
|
'user_id' => $user_id,
|
||||||
$events = array_merge($events, $this->subtaskForecast->getCalendarEvents($user_id, $end));
|
'start' => $start,
|
||||||
}
|
'end' => $end,
|
||||||
|
));
|
||||||
|
|
||||||
$this->response->json($events);
|
$this->response->json($events);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class Config extends Base
|
|||||||
$values += array('integration_slack_webhook' => 0, 'integration_hipchat' => 0, 'integration_gravatar' => 0, 'integration_jabber' => 0);
|
$values += array('integration_slack_webhook' => 0, 'integration_hipchat' => 0, 'integration_gravatar' => 0, 'integration_jabber' => 0);
|
||||||
break;
|
break;
|
||||||
case 'calendar':
|
case 'calendar':
|
||||||
$values += array('calendar_user_subtasks_forecast' => 0, 'calendar_user_subtasks_time_tracking' => 0);
|
$values += array('calendar_user_subtasks_time_tracking' => 0);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ use Pimple\Container;
|
|||||||
* @property \Model\ProjectPermission $projectPermission
|
* @property \Model\ProjectPermission $projectPermission
|
||||||
* @property \Model\Subtask $subtask
|
* @property \Model\Subtask $subtask
|
||||||
* @property \Model\SubtaskExport $subtaskExport
|
* @property \Model\SubtaskExport $subtaskExport
|
||||||
* @property \Model\SubtaskForecast $subtaskForecast
|
|
||||||
* @property \Model\SubtaskTimeTracking $subtaskTimeTracking
|
* @property \Model\SubtaskTimeTracking $subtaskTimeTracking
|
||||||
* @property \Model\Swimlane $swimlane
|
* @property \Model\Swimlane $swimlane
|
||||||
* @property \Model\Task $task
|
* @property \Model\Task $task
|
||||||
|
|||||||
70
app/Core/Plugin/Hook.php
Normal file
70
app/Core/Plugin/Hook.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,6 @@ namespace Helper;
|
|||||||
*/
|
*/
|
||||||
class Hook extends \Core\Base
|
class Hook extends \Core\Base
|
||||||
{
|
{
|
||||||
private $hooks = array();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render all attached hooks
|
* Render all attached hooks
|
||||||
*
|
*
|
||||||
@@ -24,10 +22,8 @@ class Hook extends \Core\Base
|
|||||||
{
|
{
|
||||||
$buffer = '';
|
$buffer = '';
|
||||||
|
|
||||||
foreach ($this->hooks as $name => $template) {
|
foreach ($this->hook->getListeners($hook) as $template) {
|
||||||
if ($hook === $name) {
|
$buffer .= $this->template->render($template, $variables);
|
||||||
$buffer .= $this->template->render($template, $variables);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $buffer;
|
return $buffer;
|
||||||
@@ -43,7 +39,7 @@ class Hook extends \Core\Base
|
|||||||
*/
|
*/
|
||||||
public function attach($hook, $template)
|
public function attach($hook, $template)
|
||||||
{
|
{
|
||||||
$this->hooks[$hook] = $template;
|
$this->hook->on($hook, $template);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -166,7 +166,6 @@ function version_69($pdo)
|
|||||||
$result = $rq->fetch(PDO::FETCH_ASSOC);
|
$result = $rq->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
|
$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_subtasks_time_tracking', 0));
|
||||||
$rq->execute(array('calendar_user_tasks', 'date_started'));
|
$rq->execute(array('calendar_user_tasks', 'date_started'));
|
||||||
$rq->execute(array('calendar_project_tasks', 'date_started'));
|
$rq->execute(array('calendar_project_tasks', 'date_started'));
|
||||||
|
|||||||
@@ -161,7 +161,6 @@ function version_50($pdo)
|
|||||||
$result = $rq->fetch(PDO::FETCH_ASSOC);
|
$result = $rq->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
|
$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_subtasks_time_tracking', 0));
|
||||||
$rq->execute(array('calendar_user_tasks', 'date_started'));
|
$rq->execute(array('calendar_user_tasks', 'date_started'));
|
||||||
$rq->execute(array('calendar_project_tasks', 'date_started'));
|
$rq->execute(array('calendar_project_tasks', 'date_started'));
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ function version_68($pdo)
|
|||||||
$result = $rq->fetch(PDO::FETCH_ASSOC);
|
$result = $rq->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
|
$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_subtasks_time_tracking', 0));
|
||||||
$rq->execute(array('calendar_user_tasks', 'date_started'));
|
$rq->execute(array('calendar_user_tasks', 'date_started'));
|
||||||
$rq->execute(array('calendar_project_tasks', 'date_started'));
|
$rq->execute(array('calendar_project_tasks', 'date_started'));
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ class ClassProvider implements ServiceProviderInterface
|
|||||||
'ProjectPermission',
|
'ProjectPermission',
|
||||||
'Subtask',
|
'Subtask',
|
||||||
'SubtaskExport',
|
'SubtaskExport',
|
||||||
'SubtaskForecast',
|
|
||||||
'SubtaskTimeTracking',
|
'SubtaskTimeTracking',
|
||||||
'Swimlane',
|
'Swimlane',
|
||||||
'Task',
|
'Task',
|
||||||
@@ -80,6 +79,9 @@ class ClassProvider implements ServiceProviderInterface
|
|||||||
'Core\Cache' => array(
|
'Core\Cache' => array(
|
||||||
'MemoryCache',
|
'MemoryCache',
|
||||||
),
|
),
|
||||||
|
'Core\Plugin' => array(
|
||||||
|
'Hook',
|
||||||
|
),
|
||||||
'Integration' => array(
|
'Integration' => array(
|
||||||
'BitbucketWebhook',
|
'BitbucketWebhook',
|
||||||
'GithubWebhook',
|
'GithubWebhook',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<li <?= $this->app->getRouterAction() === 'activity' ? 'class="active"' : '' ?>>
|
<li <?= $this->app->getRouterAction() === 'activity' ? 'class="active"' : '' ?>>
|
||||||
<?= $this->url->link(t('My activity stream'), 'app', 'activity', array('user_id' => $user['id'])) ?>
|
<?= $this->url->link(t('My activity stream'), 'app', 'activity', array('user_id' => $user['id'])) ?>
|
||||||
</li>
|
</li>
|
||||||
<?= $this->hook->render('dashboard:sidebar') ?>
|
<?= $this->hook->render('template:dashboard:sidebar') ?>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
|
<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>
|
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
|
|
||||||
<h4><?= t('Subtasks time tracking') ?></h4>
|
<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_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>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<?= $this->url->link(t('Documentation'), 'doc', 'show') ?>
|
<?= $this->url->link(t('Documentation'), 'doc', 'show') ?>
|
||||||
</li>
|
</li>
|
||||||
<?= $this->hook->render('config:sidebar') ?>
|
<?= $this->hook->render('template:config:sidebar') ?>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
|
<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>
|
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<li <?= $this->app->getRouterAction() === 'summary' ? 'class="active"' : '' ?>>
|
<li <?= $this->app->getRouterAction() === 'summary' ? 'class="active"' : '' ?>>
|
||||||
<?= $this->url->link(t('Daily project summary'), 'export', 'summary', array('project_id' => $project['id'])) ?>
|
<?= $this->url->link(t('Daily project summary'), 'export', 'summary', array('project_id' => $project['id'])) ?>
|
||||||
</li>
|
</li>
|
||||||
<?= $this->hook->render('export:sidebar') ?>
|
<?= $this->hook->render('template:export:sidebar') ?>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
|
<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>
|
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
<title><?= isset($title) ? $this->e($title) : 'Kanboard' ?></title>
|
<title><?= isset($title) ? $this->e($title) : 'Kanboard' ?></title>
|
||||||
|
|
||||||
<?= $this->hook->render('layout:head') ?>
|
<?= $this->hook->render('template:layout:head') ?>
|
||||||
</head>
|
</head>
|
||||||
<body data-status-url="<?= $this->url->href('app', 'status') ?>"
|
<body data-status-url="<?= $this->url->href('app', 'status') ?>"
|
||||||
data-login-url="<?= $this->url->href('auth', 'login') ?>"
|
data-login-url="<?= $this->url->href('auth', 'login') ?>"
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<?php if (isset($no_layout) && $no_layout): ?>
|
<?php if (isset($no_layout) && $no_layout): ?>
|
||||||
<?= $content_for_layout ?>
|
<?= $content_for_layout ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?= $this->hook->render('layout:top') ?>
|
<?= $this->hook->render('template:layout:top') ?>
|
||||||
<?= $this->render('header', array(
|
<?= $this->render('header', array(
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
'description' => isset($description) ? $description : '',
|
'description' => isset($description) ? $description : '',
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<?= $this->app->flashMessage() ?>
|
<?= $this->app->flashMessage() ?>
|
||||||
<?= $content_for_layout ?>
|
<?= $content_for_layout ?>
|
||||||
</section>
|
</section>
|
||||||
<?= $this->hook->render('layout:bottom') ?>
|
<?= $this->hook->render('template:layout:bottom') ?>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<?php endif ?>
|
<?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'])): ?>
|
<?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
|
||||||
<?= $this->hook->render('project:sidebar') ?>
|
<?= $this->hook->render('template:project:sidebar') ?>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
|
<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>
|
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
|
||||||
|
|||||||
@@ -25,6 +25,6 @@
|
|||||||
<?= $this->url->link(t('Closed tasks'), 'projectuser', 'closed', $filter) ?>
|
<?= $this->url->link(t('Closed tasks'), 'projectuser', 'closed', $filter) ?>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<?= $this->hook->render('project-user:sidebar') ?>
|
<?= $this->hook->render('template:project-user:sidebar') ?>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
|
||||||
<?= $this->hook->render('task:sidebar:information') ?>
|
<?= $this->hook->render('template:task:sidebar:information') ?>
|
||||||
</ul>
|
</ul>
|
||||||
<h2><?= t('Actions') ?></h2>
|
<h2><?= t('Actions') ?></h2>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
|
||||||
<?= $this->hook->render('task:sidebar:actions') ?>
|
<?= $this->hook->render('template:task:sidebar:actions') ?>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
|
<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>
|
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
|
||||||
<?= $this->hook->render('user:sidebar:information') ?>
|
<?= $this->hook->render('template:user:sidebar:information') ?>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2><?= t('Actions') ?></h2>
|
<h2><?= t('Actions') ?></h2>
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<?php endif ?>
|
<?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'])): ?>
|
<?php if ($this->user->isAdmin() && ! $this->user->isCurrentUser($user['id'])): ?>
|
||||||
<li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'remove' ? 'class="active"' : '' ?>>
|
<li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'remove' ? 'class="active"' : '' ?>>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class Plugin extends Plugin\Base
|
|||||||
{
|
{
|
||||||
public function initialize()
|
public function initialize()
|
||||||
{
|
{
|
||||||
$this->template->hook->attach('layout:head', 'theme:layout/head');
|
$this->template->hook->attach('template:layout:head', 'theme:layout/head');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -65,7 +65,7 @@ The only required method is `initialize()`. This method is called for each reque
|
|||||||
Plugin methods
|
Plugin methods
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
Available methods from `Plugin\Base`:
|
Available methods from `Core\Plugin\Base`:
|
||||||
|
|
||||||
- `initialize()`: Executed when the plugin is loaded
|
- `initialize()`: Executed when the plugin is loaded
|
||||||
- `getClasses()`: Return all classes that should be stored in the dependency injection container
|
- `getClasses()`: Return all classes that should be stored in the dependency injection container
|
||||||
@@ -79,6 +79,60 @@ This example will fetch the user #123:
|
|||||||
$this->user->getById(123);
|
$this->user->getById(123);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Application Hooks
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Hooks can extend, replace, filter data or change the default behavior. Each hook is identified with a unique name, example: `controller:calendar:user:events`
|
||||||
|
|
||||||
|
### Listen on hook events
|
||||||
|
|
||||||
|
In your `initialize()` method you need to call the method `on()` of the class `Core\Plugin\Hook`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$this->hook->on('hook_name', $callable);
|
||||||
|
```
|
||||||
|
|
||||||
|
The first argument is the name of the hook and the second is a PHP callable.
|
||||||
|
|
||||||
|
### Merge hooks
|
||||||
|
|
||||||
|
"Merge hooks" act in the same way as the function `array_merge`. The hook callback must return an array. This array will be merged with the default one.
|
||||||
|
|
||||||
|
Example to add events in the user calendar:
|
||||||
|
|
||||||
|
```php
|
||||||
|
class Plugin extends Base
|
||||||
|
{
|
||||||
|
public function initialize()
|
||||||
|
{
|
||||||
|
$container = $this->container;
|
||||||
|
|
||||||
|
$this->hook->on('controller:calendar:user:events', function($user_id, $start, $end) use ($container) {
|
||||||
|
$model = new SubtaskForecast($container);
|
||||||
|
return $model->getCalendarEvents($user_id, $end); // Return new events
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
List of merge hooks:
|
||||||
|
|
||||||
|
#### controller:calendar:project:events
|
||||||
|
|
||||||
|
- Add more events to the project calendar
|
||||||
|
- Arguments:
|
||||||
|
- `$project_id` (integer)
|
||||||
|
- `$start` Calendar start date (string, ISO-8601 format)
|
||||||
|
- `$end` Calendar` end date (string, ISO-8601 format)
|
||||||
|
|
||||||
|
#### controller:calendar:user:events
|
||||||
|
|
||||||
|
- Add more events to the user calendar
|
||||||
|
- Arguments:
|
||||||
|
- `$user_id` (integer)
|
||||||
|
- `$start` Calendar start date (string, ISO-8601 format)
|
||||||
|
- `$end` Calendar end date (string, ISO-8601 format)
|
||||||
|
|
||||||
Template hooks
|
Template hooks
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
@@ -87,7 +141,7 @@ Template hooks allow to add new content in existing templates.
|
|||||||
Example to add new content in the dashboard sidebar:
|
Example to add new content in the dashboard sidebar:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$this->template->hook->attach('dashboard:sidebar', 'myplugin:dashboard/sidebar');
|
$this->template->hook->attach('template:dashboard:sidebar', 'myplugin:dashboard/sidebar');
|
||||||
```
|
```
|
||||||
|
|
||||||
This call is usually defined in the `initialize()` method.
|
This call is usually defined in the `initialize()` method.
|
||||||
@@ -106,18 +160,18 @@ Template name without prefix are core templates.
|
|||||||
|
|
||||||
List of template hooks:
|
List of template hooks:
|
||||||
|
|
||||||
- `dashboard:sidebar`
|
- `template:dashboard:sidebar`
|
||||||
- `config:sidebar`
|
- `template:config:sidebar`
|
||||||
- `export:sidebar`
|
- `template:export:sidebar`
|
||||||
- `layout:head`
|
- `template:layout:head`
|
||||||
- `layout:top`
|
- `template:layout:top`
|
||||||
- `layout:bottom`
|
- `template:layout:bottom`
|
||||||
- `project:dropdown`
|
- `template:project:dropdown`
|
||||||
- `project-user:sidebar`
|
- `template:project-user:sidebar`
|
||||||
- `task:sidebar:information`
|
- `template:task:sidebar:information`
|
||||||
- `task:sidebar:actions`
|
- `template:task:sidebar:actions`
|
||||||
- `user:sidebar:information`
|
- `template:user:sidebar:information`
|
||||||
- `user:sidebar:actions`
|
- `template:user:sidebar:actions`
|
||||||
|
|
||||||
Other template hooks can be added if necessary, just ask on the issue tracker.
|
Other template hooks can be added if necessary, just ask on the issue tracker.
|
||||||
|
|
||||||
|
|||||||
62
tests/units/Core/Plugin/HookTest.php
Normal file
62
tests/units/Core/Plugin/HookTest.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__.'/../../Base.php';
|
||||||
|
|
||||||
|
use Core\Plugin\Hook;
|
||||||
|
|
||||||
|
class HookTest extends Base
|
||||||
|
{
|
||||||
|
public function testGetListeners()
|
||||||
|
{
|
||||||
|
$h = new Hook;
|
||||||
|
$this->assertEmpty($h->getListeners('myhook'));
|
||||||
|
|
||||||
|
$h->on('myhook', 'A');
|
||||||
|
$h->on('myhook', 'B');
|
||||||
|
|
||||||
|
$this->assertEquals(array('A', 'B'), $h->getListeners('myhook'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMergeWithNoBinding()
|
||||||
|
{
|
||||||
|
$h = new Hook;
|
||||||
|
$values = array('A', 'B');
|
||||||
|
|
||||||
|
$result = $h->merge('myhook', $values, array('p' => 'c'));
|
||||||
|
$this->assertEquals($values, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMergeWithBindings()
|
||||||
|
{
|
||||||
|
$h = new Hook;
|
||||||
|
$values = array('A', 'B');
|
||||||
|
$expected = array('A', 'B', 'c', 'D');
|
||||||
|
|
||||||
|
$h->on('myhook', function($p) {
|
||||||
|
return array($p);
|
||||||
|
});
|
||||||
|
|
||||||
|
$h->on('myhook', function() {
|
||||||
|
return array('D');
|
||||||
|
});
|
||||||
|
|
||||||
|
$result = $h->merge('myhook', $values, array('p' => 'c'));
|
||||||
|
$this->assertEquals($expected, $result);
|
||||||
|
$this->assertEquals($expected, $values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMergeWithBindingButReturningBadData()
|
||||||
|
{
|
||||||
|
$h = new Hook;
|
||||||
|
$values = array('A', 'B');
|
||||||
|
$expected = array('A', 'B');
|
||||||
|
|
||||||
|
$h->on('myhook', function() {
|
||||||
|
return 'string';
|
||||||
|
});
|
||||||
|
|
||||||
|
$result = $h->merge('myhook', $values);
|
||||||
|
$this->assertEquals($expected, $result);
|
||||||
|
$this->assertEquals($expected, $values);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
tests/units/Helper/HookHelperTest.php
Normal file
40
tests/units/Helper/HookHelperTest.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__.'/../Base.php';
|
||||||
|
|
||||||
|
use Helper\Hook;
|
||||||
|
|
||||||
|
class HookHelperTest extends Base
|
||||||
|
{
|
||||||
|
public function testMultipleHooks()
|
||||||
|
{
|
||||||
|
$this->container['template'] = $this
|
||||||
|
->getMockBuilder('\Core\Template')
|
||||||
|
->setConstructorArgs(array($this->container))
|
||||||
|
->setMethods(array('render'))
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$this->container['template']
|
||||||
|
->expects($this->at(0))
|
||||||
|
->method('render')
|
||||||
|
->with(
|
||||||
|
$this->equalTo('tpl1'),
|
||||||
|
$this->equalTo(array())
|
||||||
|
)
|
||||||
|
->will($this->returnValue('tpl1_content'));
|
||||||
|
|
||||||
|
$this->container['template']
|
||||||
|
->expects($this->at(1))
|
||||||
|
->method('render')
|
||||||
|
->with(
|
||||||
|
$this->equalTo('tpl2'),
|
||||||
|
$this->equalTo(array())
|
||||||
|
)
|
||||||
|
->will($this->returnValue('tpl2_content'));
|
||||||
|
|
||||||
|
$h = new Hook($this->container);
|
||||||
|
$h->attach('test', 'tpl1');
|
||||||
|
$h->attach('test', 'tpl2');
|
||||||
|
$this->assertEquals('tpl1_contenttpl2_content', $h->render('test'));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user