summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog5
-rw-r--r--app/Controller/Calendar.php15
-rw-r--r--app/Controller/Config.php2
-rw-r--r--app/Core/Base.php1
-rw-r--r--app/Core/Plugin/Hook.php70
-rw-r--r--app/Helper/Hook.php10
-rw-r--r--app/Model/SubtaskForecast.php124
-rw-r--r--app/Schema/Mysql.php1
-rw-r--r--app/Schema/Postgres.php1
-rw-r--r--app/Schema/Sqlite.php1
-rw-r--r--app/ServiceProvider/ClassProvider.php4
-rw-r--r--app/Template/app/sidebar.php2
-rw-r--r--app/Template/config/calendar.php1
-rw-r--r--app/Template/config/sidebar.php2
-rw-r--r--app/Template/export/sidebar.php2
-rw-r--r--app/Template/layout.php6
-rw-r--r--app/Template/project/dropdown.php2
-rw-r--r--app/Template/project/sidebar.php2
-rw-r--r--app/Template/project_user/sidebar.php2
-rw-r--r--app/Template/task/sidebar.php4
-rw-r--r--app/Template/user/sidebar.php4
-rw-r--r--doc/plugins.markdown84
-rw-r--r--tests/units/Core/Plugin/HookTest.php62
-rw-r--r--tests/units/Helper/HookHelperTest.php40
24 files changed, 275 insertions, 172 deletions
diff --git a/ChangeLog b/ChangeLog
index 4d0140db..abb90de0 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -10,9 +10,10 @@ New features:
* Added API procedures: getMyOverdueTasks, getOverdueTasksByProject and GetMyProjects
* 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:
diff --git a/app/Controller/Calendar.php b/app/Controller/Calendar.php
index 8a24d705..5ac92622 100644
--- a/app/Controller/Calendar.php
+++ b/app/Controller/Calendar.php
@@ -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);
}
diff --git a/app/Controller/Config.php b/app/Controller/Config.php
index 6f14cc31..790bdcd3 100644
--- a/app/Controller/Config.php
+++ b/app/Controller/Config.php
@@ -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;
}
diff --git a/app/Core/Base.php b/app/Core/Base.php
index 5ed8f40a..2dec4b29 100644
--- a/app/Core/Base.php
+++ b/app/Core/Base.php
@@ -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
diff --git a/app/Core/Plugin/Hook.php b/app/Core/Plugin/Hook.php
new file mode 100644
index 00000000..4fb55569
--- /dev/null
+++ b/app/Core/Plugin/Hook.php
@@ -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;
+ }
+}
diff --git a/app/Helper/Hook.php b/app/Helper/Hook.php
index 77756757..d7fe3d34 100644
--- a/app/Helper/Hook.php
+++ b/app/Helper/Hook.php
@@ -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;
}
}
diff --git a/app/Model/SubtaskForecast.php b/app/Model/SubtaskForecast.php
deleted file mode 100644
index 263aa27a..00000000
--- a/app/Model/SubtaskForecast.php
+++ /dev/null
@@ -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;
- }
-}
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index e5dff0d5..23a7a90a 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -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'));
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index e7422e8c..cd4c295e 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -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'));
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index b76e902a..175a583c 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -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'));
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index 899574e9..ee5ddbe4 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -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',
diff --git a/app/Template/app/sidebar.php b/app/Template/app/sidebar.php
index f4a455f8..c1de0dbe 100644
--- a/app/Template/app/sidebar.php
+++ b/app/Template/app/sidebar.php
@@ -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>
diff --git a/app/Template/config/calendar.php b/app/Template/config/calendar.php
index 1cc985c8..25f4b43d 100644
--- a/app/Template/config/calendar.php
+++ b/app/Template/config/calendar.php
@@ -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">
diff --git a/app/Template/config/sidebar.php b/app/Template/config/sidebar.php
index 083da283..ed4f01e7 100644
--- a/app/Template/config/sidebar.php
+++ b/app/Template/config/sidebar.php
@@ -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>
diff --git a/app/Template/export/sidebar.php b/app/Template/export/sidebar.php
index 7e39a5af..44448520 100644
--- a/app/Template/export/sidebar.php
+++ b/app/Template/export/sidebar.php
@@ -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>
diff --git a/app/Template/layout.php b/app/Template/layout.php
index 934fb62c..49ac2a08 100644
--- a/app/Template/layout.php
+++ b/app/Template/layout.php
@@ -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>
diff --git a/app/Template/project/dropdown.php b/app/Template/project/dropdown.php
index ee1ca3f7..96b6a43a 100644
--- a/app/Template/project/dropdown.php
+++ b/app/Template/project/dropdown.php
@@ -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>
diff --git a/app/Template/project/sidebar.php b/app/Template/project/sidebar.php
index 84bbb6b1..482a95d2 100644
--- a/app/Template/project/sidebar.php
+++ b/app/Template/project/sidebar.php
@@ -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>
diff --git a/app/Template/project_user/sidebar.php b/app/Template/project_user/sidebar.php
index 98219a87..b81ba14a 100644
--- a/app/Template/project_user/sidebar.php
+++ b/app/Template/project_user/sidebar.php
@@ -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> \ No newline at end of file
diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php
index cf0e9f76..9ee1e7df 100644
--- a/app/Template/task/sidebar.php
+++ b/app/Template/task/sidebar.php
@@ -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>
diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php
index 80fe8684..6a4e9c47 100644
--- a/app/Template/user/sidebar.php
+++ b/app/Template/user/sidebar.php
@@ -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"' : '' ?>>
diff --git a/doc/plugins.markdown b/doc/plugins.markdown
index 4388f7c6..c8222a81 100644
--- a/doc/plugins.markdown
+++ b/doc/plugins.markdown
@@ -53,7 +53,7 @@ class Plugin extends Plugin\Base
{
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
--------------
-Available methods from `Plugin\Base`:
+Available methods from `Core\Plugin\Base`:
- `initialize()`: Executed when the plugin is loaded
- `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);
```
+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
--------------
@@ -87,7 +141,7 @@ Template hooks allow to add new content in existing templates.
Example to add new content in the dashboard sidebar:
```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.
@@ -106,18 +160,18 @@ Template name without prefix are core templates.
List of template hooks:
-- `dashboard:sidebar`
-- `config:sidebar`
-- `export:sidebar`
-- `layout:head`
-- `layout:top`
-- `layout:bottom`
-- `project:dropdown`
-- `project-user:sidebar`
-- `task:sidebar:information`
-- `task:sidebar:actions`
-- `user:sidebar:information`
-- `user:sidebar:actions`
+- `template:dashboard:sidebar`
+- `template:config:sidebar`
+- `template:export:sidebar`
+- `template:layout:head`
+- `template:layout:top`
+- `template:layout:bottom`
+- `template:project:dropdown`
+- `template:project-user:sidebar`
+- `template:task:sidebar:information`
+- `template:task:sidebar:actions`
+- `template:user:sidebar:information`
+- `template:user:sidebar:actions`
Other template hooks can be added if necessary, just ask on the issue tracker.
diff --git a/tests/units/Core/Plugin/HookTest.php b/tests/units/Core/Plugin/HookTest.php
new file mode 100644
index 00000000..95434ce4
--- /dev/null
+++ b/tests/units/Core/Plugin/HookTest.php
@@ -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);
+ }
+}
diff --git a/tests/units/Helper/HookHelperTest.php b/tests/units/Helper/HookHelperTest.php
new file mode 100644
index 00000000..6661c90b
--- /dev/null
+++ b/tests/units/Helper/HookHelperTest.php
@@ -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'));
+ }
+} \ No newline at end of file