From ada7a33402d9a700945f19ba69354db5e36df7dd Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 1 Apr 2017 14:35:41 -0400 Subject: [PATCH] First commit --- .gitignore | 1 + .travis.yml | 35 +++++++ Controller/CalendarController.php | 121 ++++++++++++++++++++++ Controller/ConfigController.php | 32 ++++++ Formatter/BaseTaskCalendarFormatter.php | 45 +++++++++ Formatter/TaskCalendarFormatter.php | 74 ++++++++++++++ Helper/CalendarHelper.php | 127 ++++++++++++++++++++++++ LICENSE | 21 ++++ Locale/fr_FR/translations.php | 3 + Makefile | 5 + Plugin.php | 55 ++++++++++ README.md | 26 +++++ Template/calendar/project.php | 6 ++ Template/calendar/user.php | 4 + Template/config/calendar.php | 36 +++++++ Template/config/sidebar.php | 3 + Template/dashboard/menu.php | 3 + Template/project/dropdown.php | 3 + Template/project_header/views.php | 3 + Test/PluginTest.php | 20 ++++ 20 files changed, 623 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Controller/CalendarController.php create mode 100644 Controller/ConfigController.php create mode 100644 Formatter/BaseTaskCalendarFormatter.php create mode 100644 Formatter/TaskCalendarFormatter.php create mode 100644 Helper/CalendarHelper.php create mode 100644 LICENSE create mode 100644 Locale/fr_FR/translations.php create mode 100644 Makefile create mode 100644 Plugin.php create mode 100644 README.md create mode 100644 Template/calendar/project.php create mode 100644 Template/calendar/user.php create mode 100644 Template/config/calendar.php create mode 100644 Template/config/sidebar.php create mode 100644 Template/dashboard/menu.php create mode 100644 Template/project/dropdown.php create mode 100644 Template/project_header/views.php create mode 100644 Test/PluginTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4c4ffc --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.zip diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c0922c6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +language: php +sudo: false + +php: + - 7.1 + - 7.0 + - 5.6 + - 5.5 + - 5.4 + - 5.3 + +env: + global: + - PLUGIN=Calendar + - KANBOARD_REPO=https://github.com/kanboard/kanboard.git + matrix: + - DB=sqlite + - DB=mysql + - DB=postgres + +matrix: + fast_finish: true + +install: + - git clone --depth 1 $KANBOARD_REPO + - ln -s $TRAVIS_BUILD_DIR kanboard/plugins/$PLUGIN + +before_script: + - cd kanboard + - phpenv config-add tests/php.ini + - composer install + - ls -la plugins/ + +script: + - phpunit -c tests/units.$DB.xml plugins/$PLUGIN/Test/ diff --git a/Controller/CalendarController.php b/Controller/CalendarController.php new file mode 100644 index 0000000..9db7cf9 --- /dev/null +++ b/Controller/CalendarController.php @@ -0,0 +1,121 @@ +getUser(); + + $this->response->html($this->helper->layout->app('Calendar:calendar/user', array( + 'user' => $user, + ))); + } + + /** + * Show calendar view for a project + * + * @access public + */ + public function project() + { + $project = $this->getProject(); + + $this->response->html($this->helper->layout->app('Calendar:calendar/project', array( + 'project' => $project, + 'title' => $project['name'], + 'description' => $this->helper->projectHeader->getDescription($project), + ))); + } + + /** + * Get tasks to display on the calendar (project view) + * + * @access public + */ + public function projectEvents() + { + $project_id = $this->request->getIntegerParam('project_id'); + $start = $this->request->getStringParam('start'); + $end = $this->request->getStringParam('end'); + $search = $this->userSession->getFilters($project_id); + $queryBuilder = $this->taskLexer->build($search)->withFilter(new TaskProjectFilter($project_id)); + + $events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end); + $events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end)); + + $events = $this->hook->merge('controller:calendar:project:events', $events, array( + 'project_id' => $project_id, + 'start' => $start, + 'end' => $end, + )); + + $this->response->json($events); + } + + /** + * Get tasks to display on the calendar (user view) + * + * @access public + */ + public function userEvents() + { + $user_id = $this->request->getIntegerParam('user_id'); + $start = $this->request->getStringParam('start'); + $end = $this->request->getStringParam('end'); + $queryBuilder = $this->taskQuery + ->withFilter(new TaskAssigneeFilter($user_id)) + ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN)); + + $events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end); + $events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end)); + + if ($this->configModel->get('calendar_user_subtasks_time_tracking') == 1) { + $events = array_merge($events, $this->helper->calendar->getSubtaskTimeTrackingEvents($user_id, $start, $end)); + } + + $events = $this->hook->merge('controller:calendar:user:events', $events, array( + 'user_id' => $user_id, + 'start' => $start, + 'end' => $end, + )); + + $this->response->json($events); + } + + /** + * Update task due date + * + * @access public + */ + public function save() + { + if ($this->request->isAjax() && $this->request->isPost()) { + $values = $this->request->getJson(); + + $this->taskModificationModel->update(array( + 'id' => $values['task_id'], + 'date_due' => substr($values['date_due'], 0, 10), + )); + } + } +} diff --git a/Controller/ConfigController.php b/Controller/ConfigController.php new file mode 100644 index 0000000..64e0e46 --- /dev/null +++ b/Controller/ConfigController.php @@ -0,0 +1,32 @@ +response->html($this->helper->layout->config('Calendar:config/calendar', array( + 'title' => t('Settings').' > '.t('Calendar settings'), + ))); + } + + public function save() + { + $values = $this->request->getValues(); + $values += array('calendar_user_subtasks_time_tracking' => 0); + + if ($this->configModel->save($values)) { + $this->flash->success(t('Settings saved successfully.')); + } else { + $this->flash->failure(t('Unable to save your settings.')); + } + + $this->response->redirect($this->helper->url->to('ConfigController', 'show', array('plugin' => 'Calendar'))); + } +} diff --git a/Formatter/BaseTaskCalendarFormatter.php b/Formatter/BaseTaskCalendarFormatter.php new file mode 100644 index 0000000..c5a3b8f --- /dev/null +++ b/Formatter/BaseTaskCalendarFormatter.php @@ -0,0 +1,45 @@ +startColumn = $start_column; + $this->endColumn = $end_column ?: $start_column; + return $this; + } +} diff --git a/Formatter/TaskCalendarFormatter.php b/Formatter/TaskCalendarFormatter.php new file mode 100644 index 0000000..b9e3682 --- /dev/null +++ b/Formatter/TaskCalendarFormatter.php @@ -0,0 +1,74 @@ +fullDay = true; + return $this; + } + + /** + * Transform tasks to calendar events + * + * @access public + * @return array + */ + public function format() + { + $events = array(); + + foreach ($this->query->findAll() as $task) { + $events[] = array( + 'timezoneParam' => $this->timezoneModel->getCurrentTimezone(), + 'id' => $task['id'], + 'title' => t('#%d', $task['id']).' '.$task['title'], + 'backgroundColor' => $this->colorModel->getBackgroundColor($task['color_id']), + 'borderColor' => $this->colorModel->getBorderColor($task['color_id']), + 'textColor' => 'black', + 'url' => $this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), + 'start' => date($this->getDateTimeFormat(), $task[$this->startColumn]), + 'end' => date($this->getDateTimeFormat(), $task[$this->endColumn] ?: time()), + 'editable' => $this->fullDay, + 'allday' => $this->fullDay, + ); + } + + return $events; + } + + /** + * Get DateTime format for event + * + * @access private + * @return string + */ + private function getDateTimeFormat() + { + return $this->fullDay ? 'Y-m-d' : 'Y-m-d\TH:i:s'; + } +} diff --git a/Helper/CalendarHelper.php b/Helper/CalendarHelper.php new file mode 100644 index 0000000..f3a6b17 --- /dev/null +++ b/Helper/CalendarHelper.php @@ -0,0 +1,127 @@ + $checkUrl, + 'saveUrl' => $saveUrl, + ); + + return '
'; + } + + /** + * Get formatted calendar task due events + * + * @access public + * @param QueryBuilder $queryBuilder + * @param string $start + * @param string $end + * @return array + */ + public function getTaskDateDueEvents(QueryBuilder $queryBuilder, $start, $end) + { + $formatter = $this->taskCalendarFormatter; + $formatter->setFullDay(); + $formatter->setColumns('date_due'); + + return $queryBuilder + ->withFilter(new TaskDueDateRangeFilter(array($start, $end))) + ->format($formatter); + } + + /** + * Get formatted calendar task events + * + * @access public + * @param QueryBuilder $queryBuilder + * @param string $start + * @param string $end + * @return array + */ + public function getTaskEvents(QueryBuilder $queryBuilder, $start, $end) + { + $startColumn = $this->configModel->get('calendar_project_tasks', 'date_started'); + + $queryBuilder->getQuery()->addCondition($this->getCalendarCondition( + $this->dateParser->getTimestampFromIsoFormat($start), + $this->dateParser->getTimestampFromIsoFormat($end), + $startColumn, + 'date_due' + )); + + $formatter = $this->taskCalendarFormatter; + $formatter->setColumns($startColumn, 'date_due'); + + return $queryBuilder->format($formatter); + } + + /** + * Get formatted calendar subtask time tracking events + * + * @access public + * @param integer $user_id + * @param string $start + * @param string $end + * @return array + */ + public function getSubtaskTimeTrackingEvents($user_id, $start, $end) + { + return $this->subtaskTimeTrackingCalendarFormatter + ->withQuery($this->subtaskTimeTrackingModel->getUserQuery($user_id) + ->addCondition($this->getCalendarCondition( + $this->dateParser->getTimestampFromIsoFormat($start), + $this->dateParser->getTimestampFromIsoFormat($end), + 'start', + 'end' + )) + ) + ->format(); + } + + /** + * Build SQL condition for a given time range + * + * @access public + * @param string $start_time Start timestamp + * @param string $end_time End timestamp + * @param string $start_column Start column name + * @param string $end_column End column name + * @return string + */ + public function getCalendarCondition($start_time, $end_time, $start_column, $end_column) + { + $start_column = $this->db->escapeIdentifier($start_column); + $end_column = $this->db->escapeIdentifier($end_column); + + $conditions = array( + "($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).')'; + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a19d63a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Frédéric Guillot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Locale/fr_FR/translations.php b/Locale/fr_FR/translations.php new file mode 100644 index 0000000..d5be5de --- /dev/null +++ b/Locale/fr_FR/translations.php @@ -0,0 +1,3 @@ +helper->register('calendar', '\Kanboard\Plugin\Calendar\Helper\CalendarHelper'); + + $this->container['taskCalendarFormatter'] = $this->container->factory(function ($c) { + return new TaskCalendarFormatter($c); + }); + + $this->template->hook->attach('template:dashboard:page-header:menu', 'Calendar:dashboard/menu'); + $this->template->hook->attach('template:project:dropdown', 'Calendar:project/dropdown'); + $this->template->hook->attach('template:project-header:view-switcher', 'Calendar:project_header/views'); + $this->template->hook->attach('template:config:sidebar', 'Calendar:config/sidebar'); + } + + public function onStartup() + { + Translator::load($this->languageModel->getCurrentLanguage(), __DIR__.'/Locale'); + } + + public function getPluginName() + { + return 'Calendar'; + } + + public function getPluginDescription() + { + return t('Calendar view for Kanboard'); + } + + public function getPluginAuthor() + { + return 'Frédéric Guillot'; + } + + public function getPluginVersion() + { + return '1.0.0'; + } + + public function getPluginHomepage() + { + return 'https://github.com/kanboard/plugin-calendar'; + } +} + diff --git a/README.md b/README.md new file mode 100644 index 0000000..0dfe464 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +Calendar Plugin +=============== + +Embedded calendar view for Kanboard. + +Author +------ + +- Frédéric Guillot +- License MIT + +Requirements +------------ + +- Kanboard >= 1.0.42 + +Installation +------------ + +You have the choice between 3 methods: + +1. Install the plugin from the Kanboard plugin manager in one click +2. Download the zip file and decompress everything under the directory `plugins/Calendar` +3. Clone this repository into the folder `plugins/Calendar` + +Note: Plugin folder is case-sensitive. diff --git a/Template/calendar/project.php b/Template/calendar/project.php new file mode 100644 index 0000000..f1a89d2 --- /dev/null +++ b/Template/calendar/project.php @@ -0,0 +1,6 @@ +projectHeader->render($project, 'CalendarController', 'project', false, 'Calendar') ?> + +calendar->render( + $this->url->href('CalendarController', 'projectEvents', array('project_id' => $project['id'], 'plugin' => 'Calendar')), + $this->url->href('CalendarController', 'save', array('project_id' => $project['id'], 'plugin' => 'Calendar')) +) ?> diff --git a/Template/calendar/user.php b/Template/calendar/user.php new file mode 100644 index 0000000..74f00cc --- /dev/null +++ b/Template/calendar/user.php @@ -0,0 +1,4 @@ +calendar->render( + $this->url->href('CalendarController', 'userEvents', array('user_id' => $user['id'], 'plugin' => 'Calendar')), + $this->url->href('CalendarController', 'save', array('plugin' => 'Calendar')) +) ?> diff --git a/Template/config/calendar.php b/Template/config/calendar.php new file mode 100644 index 0000000..8d5a489 --- /dev/null +++ b/Template/config/calendar.php @@ -0,0 +1,36 @@ + +
+ + form->csrf() ?> + +
+ + form->radios('calendar_project_tasks', array( + 'date_creation' => t('Show tasks based on the creation date'), + 'date_started' => t('Show tasks based on the start date'), + ), + $values + ) ?> +
+ +
+ + form->radios('calendar_user_tasks', array( + 'date_creation' => t('Show tasks based on the creation date'), + 'date_started' => t('Show tasks based on the start date'), + ), + $values + ) ?> +
+ +
+ + form->checkbox('calendar_user_subtasks_time_tracking', t('Show subtasks based on the time tracking'), 1, $values['calendar_user_subtasks_time_tracking'] == 1) ?> +
+ +
+ +
+
diff --git a/Template/config/sidebar.php b/Template/config/sidebar.php new file mode 100644 index 0000000..040f480 --- /dev/null +++ b/Template/config/sidebar.php @@ -0,0 +1,3 @@ +
  • app->checkMenuSelection('ConfigController', 'show', 'Calendar') ?>> + url->link(t('Calendar settings'), 'ConfigController', 'show', array('plugin' => 'Calendar')) ?> +
  • \ No newline at end of file diff --git a/Template/dashboard/menu.php b/Template/dashboard/menu.php new file mode 100644 index 0000000..5558cc8 --- /dev/null +++ b/Template/dashboard/menu.php @@ -0,0 +1,3 @@ +
  • + modal->medium('calendar', t('My calendar'), 'CalendarController', 'user', array('plugin' => 'Calendar')) ?> +
  • diff --git a/Template/project/dropdown.php b/Template/project/dropdown.php new file mode 100644 index 0000000..539f72e --- /dev/null +++ b/Template/project/dropdown.php @@ -0,0 +1,3 @@ +
  • + url->icon('calendar', t('Calendar'), 'CalendarController', 'project', array('project_id' => $project['id'], 'plugin' => 'Calendar')) ?> +
  • \ No newline at end of file diff --git a/Template/project_header/views.php b/Template/project_header/views.php new file mode 100644 index 0000000..01c246e --- /dev/null +++ b/Template/project_header/views.php @@ -0,0 +1,3 @@ +
  • app->checkMenuSelection('CalendarController') ?>> + url->icon('calendar', t('Calendar'), 'CalendarController', 'project', array('project_id' => $project['id'], 'search' => $filters['search'], 'plugin' => 'Calendar'), false, 'view-calendar', t('Keyboard shortcut: "%s"', 'v c')) ?> +
  • \ No newline at end of file diff --git a/Test/PluginTest.php b/Test/PluginTest.php new file mode 100644 index 0000000..c6dd3fc --- /dev/null +++ b/Test/PluginTest.php @@ -0,0 +1,20 @@ +container); + $this->assertSame(null, $plugin->initialize()); + $this->assertSame(null, $plugin->onStartup()); + $this->assertNotEmpty($plugin->getPluginName()); + $this->assertNotEmpty($plugin->getPluginDescription()); + $this->assertNotEmpty($plugin->getPluginAuthor()); + $this->assertNotEmpty($plugin->getPluginVersion()); + $this->assertNotEmpty($plugin->getPluginHomepage()); + } +}