Improve iCalendar feed to include tasks with start/end date and due date with a time

This commit is contained in:
Frederic Guillot
2017-11-08 14:50:02 -08:00
parent 5e98a66aeb
commit cafbd1f5a7
12 changed files with 101 additions and 144 deletions

View File

@@ -12,6 +12,7 @@ New features:
Improvements: Improvements:
* Improve iCalendar feed to include tasks with start/end date and due date with a time
* You can get an archive of Kanboard by using the download button in Github or the command git archive * You can get an archive of Kanboard by using the download button in Github or the command git archive
Version 1.0.48 (October 23, 2017) Version 1.0.48 (October 23, 2017)

View File

@@ -5,6 +5,7 @@ namespace Kanboard\Controller;
use Kanboard\Core\Controller\AccessForbiddenException; use Kanboard\Core\Controller\AccessForbiddenException;
use Kanboard\Core\Filter\QueryBuilder; use Kanboard\Core\Filter\QueryBuilder;
use Kanboard\Filter\TaskAssigneeFilter; use Kanboard\Filter\TaskAssigneeFilter;
use Kanboard\Filter\TaskDueDateRangeFilter;
use Kanboard\Filter\TaskProjectFilter; use Kanboard\Filter\TaskProjectFilter;
use Kanboard\Filter\TaskStatusFilter; use Kanboard\Filter\TaskStatusFilter;
use Kanboard\Model\TaskModel; use Kanboard\Model\TaskModel;
@@ -18,81 +19,97 @@ use Eluceo\iCal\Component\Calendar as iCalendar;
*/ */
class ICalendarController extends BaseController class ICalendarController extends BaseController
{ {
/**
* Get user iCalendar
*
* @access public
*/
public function user() public function user()
{ {
$token = $this->request->getStringParam('token'); $token = $this->request->getStringParam('token');
$user = $this->userModel->getByToken($token); $user = $this->userModel->getByToken($token);
// Token verification
if (empty($user)) { if (empty($user)) {
throw AccessForbiddenException::getInstance()->withoutLayout(); throw AccessForbiddenException::getInstance()->withoutLayout();
} }
// Common filter $startRange = $this->request->getStringParam('start', strtotime('-2 months'));
$queryBuilder = new QueryBuilder(); $endRange = $this->request->getStringParam('end', strtotime('+6 months'));
$queryBuilder
->withQuery($this->taskFinderModel->getICalQuery()) $startColumn = $this->configModel->get('calendar_project_tasks', 'date_started');
->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN))
->withFilter(new TaskAssigneeFilter($user['id']));
// Calendar properties
$calendar = new iCalendar('Kanboard'); $calendar = new iCalendar('Kanboard');
$calendar->setName($user['name'] ?: $user['username']); $calendar->setName($user['name'] ?: $user['username']);
$calendar->setDescription($user['name'] ?: $user['username']); $calendar->setDescription($user['name'] ?: $user['username']);
$calendar->setPublishedTTL('PT1H'); $calendar->setPublishedTTL('PT1H');
$this->renderCalendar($queryBuilder, $calendar); $queryDueDateOnly = QueryBuilder::create()
->withQuery($this->taskFinderModel->getICalQuery())
->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN))
->withFilter(new TaskDueDateRangeFilter(array($startRange, $endRange)))
->withFilter(new TaskAssigneeFilter($user['id']))
->getQuery();
$queryStartAndDueDate = QueryBuilder::create()
->withQuery($this->taskFinderModel->getICalQuery())
->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN))
->withFilter(new TaskAssigneeFilter($user['id']))
->getQuery()
->addCondition($this->getConditionForTasksWithStartAndDueDate($startRange, $endRange, $startColumn, 'date_due'));
$this->response->ical($this->taskICalFormatter
->setCalendar($calendar)
->addTasksWithDueDateOnly($queryDueDateOnly)
->addTasksWithStartAndDueDate($queryStartAndDueDate, $startColumn, 'date_due')
->format());
} }
/**
* Get project iCalendar
*
* @access public
*/
public function project() public function project()
{ {
$token = $this->request->getStringParam('token'); $token = $this->request->getStringParam('token');
$project = $this->projectModel->getByToken($token); $project = $this->projectModel->getByToken($token);
// Token verification
if (empty($project)) { if (empty($project)) {
throw AccessForbiddenException::getInstance()->withoutLayout(); throw AccessForbiddenException::getInstance()->withoutLayout();
} }
// Common filter $startRange = $this->request->getStringParam('start', strtotime('-2 months'));
$queryBuilder = new QueryBuilder(); $endRange = $this->request->getStringParam('end', strtotime('+6 months'));
$queryBuilder
->withQuery($this->taskFinderModel->getICalQuery()) $startColumn = $this->configModel->get('calendar_project_tasks', 'date_started');
->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN))
->withFilter(new TaskProjectFilter($project['id']));
// Calendar properties
$calendar = new iCalendar('Kanboard'); $calendar = new iCalendar('Kanboard');
$calendar->setName($project['name']); $calendar->setName($project['name']);
$calendar->setDescription($project['name']); $calendar->setDescription($project['name']);
$calendar->setPublishedTTL('PT1H'); $calendar->setPublishedTTL('PT1H');
$this->renderCalendar($queryBuilder, $calendar); $queryDueDateOnly = QueryBuilder::create()
->withQuery($this->taskFinderModel->getICalQuery())
->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN))
->withFilter(new TaskProjectFilter($project['id']))
->withFilter(new TaskDueDateRangeFilter(array($startRange, $endRange)))
->getQuery();
$queryStartAndDueDate = QueryBuilder::create()
->withQuery($this->taskFinderModel->getICalQuery())
->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN))
->withFilter(new TaskProjectFilter($project['id']))
->getQuery()
->addCondition($this->getConditionForTasksWithStartAndDueDate($startRange, $endRange, $startColumn, 'date_due'));
$this->response->ical($this->taskICalFormatter
->setCalendar($calendar)
->addTasksWithDueDateOnly($queryDueDateOnly)
->addTasksWithStartAndDueDate($queryStartAndDueDate, $startColumn, 'date_due')
->format());
} }
/** protected function getConditionForTasksWithStartAndDueDate($start_time, $end_time, $start_column, $end_column)
* Common method to render iCal events
*
* @access private
* @param QueryBuilder $queryBuilder
* @param iCalendar $calendar
*/
private function renderCalendar(QueryBuilder $queryBuilder, iCalendar $calendar)
{ {
$start = $this->request->getStringParam('start', strtotime('-2 month')); $start_column = $this->db->escapeIdentifier($start_column);
$end = $this->request->getStringParam('end', strtotime('+6 months')); $end_column = $this->db->escapeIdentifier($end_column);
$this->helper->ical->addTaskDateDueEvents($queryBuilder, $calendar, $start, $end); $conditions = array(
$this->response->ical($this->taskICalFormatter->setCalendar($calendar)->format()); "($start_column >= '$start_time' AND $start_column <= '$end_time')",
"($start_column <= '$start_time' AND $end_column >= '$start_time')",
"($start_column <= '$start_time' AND ($end_column = '0' OR $end_column IS NULL))",
);
return $start_column.' IS NOT NULL AND '.$start_column.' > 0 AND ('.implode(' OR ', $conditions).')';
} }
} }

View File

@@ -20,6 +20,18 @@ class QueryBuilder
*/ */
protected $query; protected $query;
/**
* Create a new class instance
*
* @static
* @access public
* @return static
*/
public static function create()
{
return new static();
}
/** /**
* Set the query * Set the query
* *

View File

@@ -19,7 +19,6 @@ use Pimple\Container;
* @property \Kanboard\Helper\FileHelper $file * @property \Kanboard\Helper\FileHelper $file
* @property \Kanboard\Helper\FormHelper $form * @property \Kanboard\Helper\FormHelper $form
* @property \Kanboard\Helper\HookHelper $hook * @property \Kanboard\Helper\HookHelper $hook
* @property \Kanboard\Helper\ICalHelper $ical
* @property \Kanboard\Helper\ModalHelper $modal * @property \Kanboard\Helper\ModalHelper $modal
* @property \Kanboard\Helper\ModelHelper $model * @property \Kanboard\Helper\ModelHelper $model
* @property \Kanboard\Helper\SubtaskHelper $subtask * @property \Kanboard\Helper\SubtaskHelper $subtask

View File

@@ -51,7 +51,7 @@ abstract class BaseFilter
* *
* @access public * @access public
* @param Table $query * @param Table $query
* @return \Kanboard\Core\Filter\FilterInterface * @return $this
*/ */
public function withQuery(Table $query) public function withQuery(Table $query)
{ {
@@ -64,7 +64,7 @@ abstract class BaseFilter
* *
* @access public * @access public
* @param string $value * @param string $value
* @return \Kanboard\Core\Filter\FilterInterface * @return $this
*/ */
public function withValue($value) public function withValue($value)
{ {

View File

@@ -32,6 +32,11 @@ class TaskDueDateRangeFilter extends BaseFilter implements FilterInterface
*/ */
public function apply() public function apply()
{ {
$this->query->beginOr();
$this->query->isNull(TaskModel::TABLE.'.date_started');
$this->query->eq(TaskModel::TABLE.'.date_started', 0);
$this->query->closeOr();
$this->query->gte(TaskModel::TABLE.'.date_due', is_numeric($this->value[0]) ? $this->value[0] : strtotime($this->value[0])); $this->query->gte(TaskModel::TABLE.'.date_due', is_numeric($this->value[0]) ? $this->value[0] : strtotime($this->value[0]));
$this->query->lte(TaskModel::TABLE.'.date_due', is_numeric($this->value[1]) ? $this->value[1] : strtotime($this->value[1])); $this->query->lte(TaskModel::TABLE.'.date_due', is_numeric($this->value[1]) ? $this->value[1] : strtotime($this->value[1]));
return $this; return $this;

View File

@@ -1,43 +0,0 @@
<?php
namespace Kanboard\Formatter;
/**
* Common class to handle calendar events
*
* @package formatter
* @author Frederic Guillot
*/
abstract class BaseTaskCalendarFormatter extends BaseFormatter
{
/**
* Column used for event start date
*
* @access protected
* @var string
*/
protected $startColumn = 'date_started';
/**
* Column used for event end date
*
* @access protected
* @var string
*/
protected $endColumn = 'date_completed';
/**
* Transform results to calendar events
*
* @access public
* @param string $start_column Column name for the start date
* @param string $end_column Column name for the end date
* @return $this
*/
public function setColumns($start_column, $end_column = '')
{
$this->startColumn = $start_column;
$this->endColumn = $end_column ?: $start_column;
return $this;
}
}

View File

@@ -8,6 +8,7 @@ use Eluceo\iCal\Component\Event;
use Eluceo\iCal\Property\Event\Attendees; use Eluceo\iCal\Property\Event\Attendees;
use Eluceo\iCal\Property\Event\Organizer; use Eluceo\iCal\Property\Event\Organizer;
use Kanboard\Core\Filter\FormatterInterface; use Kanboard\Core\Filter\FormatterInterface;
use PicoDb\Table;
/** /**
* iCal event formatter for tasks * iCal event formatter for tasks
@@ -15,15 +16,15 @@ use Kanboard\Core\Filter\FormatterInterface;
* @package formatter * @package formatter
* @author Frederic Guillot * @author Frederic Guillot
*/ */
class TaskICalFormatter extends BaseTaskCalendarFormatter implements FormatterInterface class TaskICalFormatter extends BaseFormatter implements FormatterInterface
{ {
/** /**
* Calendar object * Calendar object
* *
* @access private * @access protected
* @var \Eluceo\iCal\Component\Calendar * @var \Eluceo\iCal\Component\Calendar
*/ */
private $vCalendar; protected $vCalendar;
/** /**
* Get Ical events * Get Ical events
@@ -41,7 +42,7 @@ class TaskICalFormatter extends BaseTaskCalendarFormatter implements FormatterIn
* *
* @access public * @access public
* @param \Eluceo\iCal\Component\Calendar $vCalendar * @param \Eluceo\iCal\Component\Calendar $vCalendar
* @return FormatterInterface * @return $this
*/ */
public function setCalendar(Calendar $vCalendar) public function setCalendar(Calendar $vCalendar)
{ {
@@ -53,18 +54,21 @@ class TaskICalFormatter extends BaseTaskCalendarFormatter implements FormatterIn
* Transform results to iCal events * Transform results to iCal events
* *
* @access public * @access public
* @return FormatterInterface * @param Table $query
* @param string $startColumn
* @param string $endColumn
* @return $this
*/ */
public function addDateTimeEvents() public function addTasksWithStartAndDueDate(Table $query, $startColumn, $endColumn)
{ {
foreach ($this->query->findAll() as $task) { foreach ($query->findAll() as $task) {
$start = new DateTime; $start = new DateTime;
$start->setTimestamp($task[$this->startColumn]); $start->setTimestamp($task[$startColumn]);
$end = new DateTime; $end = new DateTime;
$end->setTimestamp($task[$this->endColumn] ?: time()); $end->setTimestamp($task[$endColumn] ?: time());
$vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-'.$this->startColumn.'-'.$this->endColumn); $vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-'.$startColumn.'-'.$endColumn);
$vEvent->setDtStart($start); $vEvent->setDtStart($start);
$vEvent->setDtEnd($end); $vEvent->setDtEnd($end);
@@ -78,18 +82,22 @@ class TaskICalFormatter extends BaseTaskCalendarFormatter implements FormatterIn
* Transform results to all day iCal events * Transform results to all day iCal events
* *
* @access public * @access public
* @return FormatterInterface * @param Table $query
* @return $this
*/ */
public function addFullDayEvents() public function addTasksWithDueDateOnly(Table $query)
{ {
foreach ($this->query->findAll() as $task) { foreach ($query->findAll() as $task) {
$date = new DateTime; $date = new DateTime;
$date->setTimestamp($task[$this->startColumn]); $date->setTimestamp($task['date_due']);
$vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-'.$this->startColumn); $vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-date_due');
$vEvent->setDtStart($date); $vEvent->setDtStart($date);
$vEvent->setDtEnd($date); $vEvent->setDtEnd($date);
$vEvent->setNoTime(true);
if ($date->format('Hi') === '0000') {
$vEvent->setNoTime(true);
}
$this->vCalendar->addComponent($vEvent); $this->vCalendar->addComponent($vEvent);
} }

View File

@@ -1,37 +0,0 @@
<?php
namespace Kanboard\Helper;
use Kanboard\Core\Base;
use Kanboard\Core\Filter\QueryBuilder;
use Kanboard\Filter\TaskDueDateRangeFilter;
use Eluceo\iCal\Component\Calendar as iCalendar;
/**
* ICal Helper
*
* @package helper
* @author Frederic Guillot
*/
class ICalHelper extends Base
{
/**
* Get formatted calendar task due events
*
* @access public
* @param QueryBuilder $queryBuilder
* @param iCalendar $calendar
* @param string $start
* @param string $end
*/
public function addTaskDateDueEvents(QueryBuilder $queryBuilder, iCalendar $calendar, $start, $end)
{
$queryBuilder->withFilter(new TaskDueDateRangeFilter(array($start, $end)));
$this->taskICalFormatter
->setColumns('date_due')
->setCalendar($calendar)
->withQuery($queryBuilder->getQuery())
->addFullDayEvents();
}
}

View File

@@ -26,7 +26,6 @@ class HelperProvider implements ServiceProviderInterface
$container['helper']->register('file', '\Kanboard\Helper\FileHelper'); $container['helper']->register('file', '\Kanboard\Helper\FileHelper');
$container['helper']->register('form', '\Kanboard\Helper\FormHelper'); $container['helper']->register('form', '\Kanboard\Helper\FormHelper');
$container['helper']->register('hook', '\Kanboard\Helper\HookHelper'); $container['helper']->register('hook', '\Kanboard\Helper\HookHelper');
$container['helper']->register('ical', '\Kanboard\Helper\ICalHelper');
$container['helper']->register('layout', '\Kanboard\Helper\LayoutHelper'); $container['helper']->register('layout', '\Kanboard\Helper\LayoutHelper');
$container['helper']->register('model', '\Kanboard\Helper\ModelHelper'); $container['helper']->register('model', '\Kanboard\Helper\ModelHelper');
$container['helper']->register('subtask', '\Kanboard\Helper\SubtaskHelper'); $container['helper']->register('subtask', '\Kanboard\Helper\SubtaskHelper');

View File

@@ -436,7 +436,6 @@ return array(
'Kanboard\\Filter\\TaskTitleFilter' => $baseDir . '/app/Filter/TaskTitleFilter.php', 'Kanboard\\Filter\\TaskTitleFilter' => $baseDir . '/app/Filter/TaskTitleFilter.php',
'Kanboard\\Filter\\UserNameFilter' => $baseDir . '/app/Filter/UserNameFilter.php', 'Kanboard\\Filter\\UserNameFilter' => $baseDir . '/app/Filter/UserNameFilter.php',
'Kanboard\\Formatter\\BaseFormatter' => $baseDir . '/app/Formatter/BaseFormatter.php', 'Kanboard\\Formatter\\BaseFormatter' => $baseDir . '/app/Formatter/BaseFormatter.php',
'Kanboard\\Formatter\\BaseTaskCalendarFormatter' => $baseDir . '/app/Formatter/BaseTaskCalendarFormatter.php',
'Kanboard\\Formatter\\BoardColumnFormatter' => $baseDir . '/app/Formatter/BoardColumnFormatter.php', 'Kanboard\\Formatter\\BoardColumnFormatter' => $baseDir . '/app/Formatter/BoardColumnFormatter.php',
'Kanboard\\Formatter\\BoardFormatter' => $baseDir . '/app/Formatter/BoardFormatter.php', 'Kanboard\\Formatter\\BoardFormatter' => $baseDir . '/app/Formatter/BoardFormatter.php',
'Kanboard\\Formatter\\BoardSwimlaneFormatter' => $baseDir . '/app/Formatter/BoardSwimlaneFormatter.php', 'Kanboard\\Formatter\\BoardSwimlaneFormatter' => $baseDir . '/app/Formatter/BoardSwimlaneFormatter.php',
@@ -470,7 +469,6 @@ return array(
'Kanboard\\Helper\\FileHelper' => $baseDir . '/app/Helper/FileHelper.php', 'Kanboard\\Helper\\FileHelper' => $baseDir . '/app/Helper/FileHelper.php',
'Kanboard\\Helper\\FormHelper' => $baseDir . '/app/Helper/FormHelper.php', 'Kanboard\\Helper\\FormHelper' => $baseDir . '/app/Helper/FormHelper.php',
'Kanboard\\Helper\\HookHelper' => $baseDir . '/app/Helper/HookHelper.php', 'Kanboard\\Helper\\HookHelper' => $baseDir . '/app/Helper/HookHelper.php',
'Kanboard\\Helper\\ICalHelper' => $baseDir . '/app/Helper/ICalHelper.php',
'Kanboard\\Helper\\LayoutHelper' => $baseDir . '/app/Helper/LayoutHelper.php', 'Kanboard\\Helper\\LayoutHelper' => $baseDir . '/app/Helper/LayoutHelper.php',
'Kanboard\\Helper\\MailHelper' => $baseDir . '/app/Helper/MailHelper.php', 'Kanboard\\Helper\\MailHelper' => $baseDir . '/app/Helper/MailHelper.php',
'Kanboard\\Helper\\ModalHelper' => $baseDir . '/app/Helper/ModalHelper.php', 'Kanboard\\Helper\\ModalHelper' => $baseDir . '/app/Helper/ModalHelper.php',

View File

@@ -572,7 +572,6 @@ class ComposerStaticInit6edea6294a88689e3f5c56484bb70c9b
'Kanboard\\Filter\\TaskTitleFilter' => __DIR__ . '/../..' . '/app/Filter/TaskTitleFilter.php', 'Kanboard\\Filter\\TaskTitleFilter' => __DIR__ . '/../..' . '/app/Filter/TaskTitleFilter.php',
'Kanboard\\Filter\\UserNameFilter' => __DIR__ . '/../..' . '/app/Filter/UserNameFilter.php', 'Kanboard\\Filter\\UserNameFilter' => __DIR__ . '/../..' . '/app/Filter/UserNameFilter.php',
'Kanboard\\Formatter\\BaseFormatter' => __DIR__ . '/../..' . '/app/Formatter/BaseFormatter.php', 'Kanboard\\Formatter\\BaseFormatter' => __DIR__ . '/../..' . '/app/Formatter/BaseFormatter.php',
'Kanboard\\Formatter\\BaseTaskCalendarFormatter' => __DIR__ . '/../..' . '/app/Formatter/BaseTaskCalendarFormatter.php',
'Kanboard\\Formatter\\BoardColumnFormatter' => __DIR__ . '/../..' . '/app/Formatter/BoardColumnFormatter.php', 'Kanboard\\Formatter\\BoardColumnFormatter' => __DIR__ . '/../..' . '/app/Formatter/BoardColumnFormatter.php',
'Kanboard\\Formatter\\BoardFormatter' => __DIR__ . '/../..' . '/app/Formatter/BoardFormatter.php', 'Kanboard\\Formatter\\BoardFormatter' => __DIR__ . '/../..' . '/app/Formatter/BoardFormatter.php',
'Kanboard\\Formatter\\BoardSwimlaneFormatter' => __DIR__ . '/../..' . '/app/Formatter/BoardSwimlaneFormatter.php', 'Kanboard\\Formatter\\BoardSwimlaneFormatter' => __DIR__ . '/../..' . '/app/Formatter/BoardSwimlaneFormatter.php',
@@ -606,7 +605,6 @@ class ComposerStaticInit6edea6294a88689e3f5c56484bb70c9b
'Kanboard\\Helper\\FileHelper' => __DIR__ . '/../..' . '/app/Helper/FileHelper.php', 'Kanboard\\Helper\\FileHelper' => __DIR__ . '/../..' . '/app/Helper/FileHelper.php',
'Kanboard\\Helper\\FormHelper' => __DIR__ . '/../..' . '/app/Helper/FormHelper.php', 'Kanboard\\Helper\\FormHelper' => __DIR__ . '/../..' . '/app/Helper/FormHelper.php',
'Kanboard\\Helper\\HookHelper' => __DIR__ . '/../..' . '/app/Helper/HookHelper.php', 'Kanboard\\Helper\\HookHelper' => __DIR__ . '/../..' . '/app/Helper/HookHelper.php',
'Kanboard\\Helper\\ICalHelper' => __DIR__ . '/../..' . '/app/Helper/ICalHelper.php',
'Kanboard\\Helper\\LayoutHelper' => __DIR__ . '/../..' . '/app/Helper/LayoutHelper.php', 'Kanboard\\Helper\\LayoutHelper' => __DIR__ . '/../..' . '/app/Helper/LayoutHelper.php',
'Kanboard\\Helper\\MailHelper' => __DIR__ . '/../..' . '/app/Helper/MailHelper.php', 'Kanboard\\Helper\\MailHelper' => __DIR__ . '/../..' . '/app/Helper/MailHelper.php',
'Kanboard\\Helper\\ModalHelper' => __DIR__ . '/../..' . '/app/Helper/ModalHelper.php', 'Kanboard\\Helper\\ModalHelper' => __DIR__ . '/../..' . '/app/Helper/ModalHelper.php',