mirror of
https://github.com/YunoHost-Apps/kanboard_ynh.git
synced 2024-09-03 19:36:17 +02:00
Fix update to kanboard v1.0.18 (some file were out of vcs)
This commit is contained in:
parent
ad4eff4f91
commit
161eebeaab
642 changed files with 61370 additions and 0 deletions
9
sources/.htaccess
Normal file
9
sources/.htaccess
Normal file
|
@ -0,0 +1,9 @@
|
|||
<IfModule mod_rewrite.c>
|
||||
Options -MultiViews
|
||||
|
||||
SetEnv HTTP_MOD_REWRITE On
|
||||
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [QSA,L]
|
||||
</IfModule>
|
143
sources/ChangeLog
Normal file
143
sources/ChangeLog
Normal file
|
@ -0,0 +1,143 @@
|
|||
Version 1.0.18
|
||||
--------------
|
||||
|
||||
New features:
|
||||
|
||||
* Include documentation in the application
|
||||
* Add Gitlab authentication
|
||||
* Add users and categories filters on the board
|
||||
* Add hide/show columns
|
||||
* Add Gantt chart for projects and tasks
|
||||
* Add new role "Project Administrator"
|
||||
* Add login bruteforce protection with captcha and account lockdown
|
||||
* Add new api procedures: getDefaultTaskColor(), getDefaultTaskColors() and getColorList()
|
||||
* Add user api access
|
||||
* Add config parameter to define session duration
|
||||
* Add config parameter to disable/enable RememberMe authentication
|
||||
* Add start/end date for projects
|
||||
* Add new automated action to change task color based on the task link
|
||||
* Add milestone marker in board task
|
||||
* Add search in task title when using an integer only input
|
||||
* Add Portuguese (European) translation
|
||||
* Add Norwegian translation
|
||||
|
||||
Improvements:
|
||||
|
||||
* Add handle to move tasks on touch devices
|
||||
* Improve file attachments tooltip on the board
|
||||
* Adjust automatically the height of the placeholder during drag and drop
|
||||
* Show all tasks when using no search criteria
|
||||
* Add column vertical scrolling
|
||||
* Set dynamically column height based on viewport size
|
||||
* Enable support for Github Enterprise when using Github Authentication
|
||||
* Update iCalendar library to display organizer name
|
||||
* Improve sidebar menus
|
||||
* Add no referrer policy in meta tags
|
||||
* Run automated unit tests with Sqlite/Mysql/Postgres on Travis-ci
|
||||
* Add Makefile and remove the scripts directory
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Wrong template name for subtasks tooltip due to previous refactoring
|
||||
* Fix broken url for closed tasks in project view
|
||||
* Fix permission issue when changing the url manually
|
||||
* Fix bug task estimate is reseted when using subtask timer
|
||||
* Fix screenshot feature with Firefox 40
|
||||
* Fix bug when uploading files with cyrilic characters
|
||||
|
||||
Version 1.0.17
|
||||
--------------
|
||||
|
||||
New features:
|
||||
|
||||
* Added url rewrite and new routes
|
||||
* Added new search engine with advanced syntax
|
||||
* Added global search section
|
||||
* Added search form on the dashboard
|
||||
* Added new dashboard layout
|
||||
* Added new layout for board/calendar/list views
|
||||
* Added filters helper for search forms
|
||||
* Added settings option to disable subtask timer
|
||||
* Added settings option to include or exclude closed tasks into CFD
|
||||
* Added settings option to define the default task color
|
||||
* Added new config option to disable automatic creation of LDAP accounts
|
||||
* Added loading icon on board view
|
||||
* Prompt user when moving or duplicate a task to another project
|
||||
* Added current values when moving/duplicate a task to another project and add a loading icon
|
||||
* Added memory consumption in debug log
|
||||
* Added form to create remote user
|
||||
* Added edit form for user authentication
|
||||
* Added config option to hide login form
|
||||
* Display OAuth2 urls on integration page
|
||||
* Added keyboard shortcuts to switch between board/calendar/list view
|
||||
* Added keyboard shortcut to focus on the search box
|
||||
* Added Slack channel override
|
||||
* Added new report: Lead and cycle time for projects
|
||||
* Added new report: Average time spent into each column
|
||||
* Added task analytics
|
||||
* Added icon to set automatically the start date
|
||||
* Added datetime picker for start date
|
||||
|
||||
Improvements:
|
||||
|
||||
* Updated documentation
|
||||
* Display user initials when tasks are in collapsed mode
|
||||
* Show title in tooltip for collapsed tasks
|
||||
* Improve alert box fadeout to avoid an empty space
|
||||
* Set focus on the dropdown for category popover
|
||||
* Make escape keyboard shorcut global
|
||||
* Check the box remember me by default
|
||||
* Store redirect login url in session instead of using url parameter
|
||||
* Update Gitlab webhook
|
||||
* Do not rewrite remember me cookie for each request
|
||||
* Set the assignee as organizer for ical events
|
||||
* Increase date range for ics export
|
||||
* Reduce spacing on cards
|
||||
* Move board collapse/expand mode to server side to avoid board flickering
|
||||
* Use ajax requests for board collapse/expand
|
||||
* Do not set anchor for the default swimlane on the link back to board
|
||||
* Replace timeserie axis to category axis for charts
|
||||
* Hide task age in compact mode
|
||||
* Improve quick-add subtasks form
|
||||
* Reduce the size of the filter box for smaller screen
|
||||
* Added icon to hide/show sidebar
|
||||
* Update GitLab logo
|
||||
* Improve Dockerfile
|
||||
|
||||
Translations:
|
||||
|
||||
* Added Czech translation
|
||||
* Updated Spanish translation
|
||||
* Updated German Translation
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Screenshot dropdown: unexpected scroll down on the board view and focus lost when clicking on the drop zone
|
||||
* No creator when duplicating a task
|
||||
* Avoid the creation of multiple subtask timer for the same task and user
|
||||
|
||||
Code refactoring:
|
||||
|
||||
* Split task controller into smaller classes
|
||||
* Remove method Category::getBoardCategories()
|
||||
* Rewrite movePosition() to improve performances
|
||||
* Refactoring of Github and Google authentication
|
||||
|
||||
Breaking changes:
|
||||
|
||||
* New OAuth url for Google and Github authentication
|
||||
|
||||
API:
|
||||
|
||||
* Add urls in api response for tasks and projects
|
||||
|
||||
Other:
|
||||
|
||||
* Added automated Docker build
|
||||
* Remove edit recurrence from the task menu on the board
|
||||
* Switch to MIT License instead of AGPLv3
|
||||
|
||||
Version 1.0.0 to 1.0.16
|
||||
-----------------------
|
||||
|
||||
* See commit history and website news
|
84
sources/app/Action/TaskAssignColorLink.php
Normal file
84
sources/app/Action/TaskAssignColorLink.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
namespace Action;
|
||||
|
||||
use Model\TaskLink;
|
||||
|
||||
/**
|
||||
* Assign a color to a specific task link
|
||||
*
|
||||
* @package action
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class TaskAssignColorLink extends Base
|
||||
{
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getCompatibleEvents()
|
||||
{
|
||||
return array(
|
||||
TaskLink::EVENT_CREATE_UPDATE,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the action (defined by the user)
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getActionRequiredParameters()
|
||||
{
|
||||
return array(
|
||||
'color_id' => t('Color'),
|
||||
'link_id' => t('Link type'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the event
|
||||
*
|
||||
* @access public
|
||||
* @return string[]
|
||||
*/
|
||||
public function getEventRequiredParameters()
|
||||
{
|
||||
return array(
|
||||
'task_id',
|
||||
'link_id',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the action (change the task color)
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Event data dictionary
|
||||
* @return bool True if the action was executed or false when not executed
|
||||
*/
|
||||
public function doAction(array $data)
|
||||
{
|
||||
$values = array(
|
||||
'id' => $data['task_id'],
|
||||
'color_id' => $this->getParam('color_id'),
|
||||
);
|
||||
|
||||
return $this->taskModification->update($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the event data meet the action condition
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Event data dictionary
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRequiredCondition(array $data)
|
||||
{
|
||||
return $data['link_id'] == $this->getParam('link_id');
|
||||
}
|
||||
}
|
97
sources/app/Action/TaskEmail.php
Normal file
97
sources/app/Action/TaskEmail.php
Normal file
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace Action;
|
||||
|
||||
use Model\Task;
|
||||
|
||||
/**
|
||||
* Email a task to someone
|
||||
*
|
||||
* @package action
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class TaskEmail extends Base
|
||||
{
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getCompatibleEvents()
|
||||
{
|
||||
return array(
|
||||
Task::EVENT_MOVE_COLUMN,
|
||||
Task::EVENT_CLOSE,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the action (defined by the user)
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getActionRequiredParameters()
|
||||
{
|
||||
return array(
|
||||
'column_id' => t('Column'),
|
||||
'user_id' => t('User that will receive the email'),
|
||||
'subject' => t('Email subject'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the event
|
||||
*
|
||||
* @access public
|
||||
* @return string[]
|
||||
*/
|
||||
public function getEventRequiredParameters()
|
||||
{
|
||||
return array(
|
||||
'task_id',
|
||||
'column_id',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the action (move the task to another column)
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Event data dictionary
|
||||
* @return bool True if the action was executed or false when not executed
|
||||
*/
|
||||
public function doAction(array $data)
|
||||
{
|
||||
$user = $this->user->getById($this->getParam('user_id'));
|
||||
|
||||
if (! empty($user['email'])) {
|
||||
|
||||
$task = $this->taskFinder->getDetails($data['task_id']);
|
||||
|
||||
$this->emailClient->send(
|
||||
$user['email'],
|
||||
$user['name'] ?: $user['username'],
|
||||
$this->getParam('subject'),
|
||||
$this->template->render('notification/task_create', array('task' => $task, 'application_url' => $this->config->get('application_url')))
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the event data meet the action condition
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Event data dictionary
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRequiredCondition(array $data)
|
||||
{
|
||||
return $data['column_id'] == $this->getParam('column_id');
|
||||
}
|
||||
}
|
89
sources/app/Action/TaskMoveColumnCategoryChange.php
Normal file
89
sources/app/Action/TaskMoveColumnCategoryChange.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace Action;
|
||||
|
||||
use Model\Task;
|
||||
|
||||
/**
|
||||
* Move a task to another column when the category is changed
|
||||
*
|
||||
* @package action
|
||||
* @author Francois Ferrand
|
||||
*/
|
||||
class TaskMoveColumnCategoryChange extends Base
|
||||
{
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getCompatibleEvents()
|
||||
{
|
||||
return array(
|
||||
Task::EVENT_UPDATE,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the action (defined by the user)
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getActionRequiredParameters()
|
||||
{
|
||||
return array(
|
||||
'dest_column_id' => t('Destination column'),
|
||||
'category_id' => t('Category'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the event
|
||||
*
|
||||
* @access public
|
||||
* @return string[]
|
||||
*/
|
||||
public function getEventRequiredParameters()
|
||||
{
|
||||
return array(
|
||||
'task_id',
|
||||
'column_id',
|
||||
'project_id',
|
||||
'category_id',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the action (move the task to another column)
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Event data dictionary
|
||||
* @return bool True if the action was executed or false when not executed
|
||||
*/
|
||||
public function doAction(array $data)
|
||||
{
|
||||
$original_task = $this->taskFinder->getById($data['task_id']);
|
||||
|
||||
return $this->taskPosition->movePosition(
|
||||
$data['project_id'],
|
||||
$data['task_id'],
|
||||
$this->getParam('dest_column_id'),
|
||||
$original_task['position'],
|
||||
$original_task['swimlane_id']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the event data meet the action condition
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Event data dictionary
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRequiredCondition(array $data)
|
||||
{
|
||||
return $data['column_id'] != $this->getParam('dest_column_id') && $data['category_id'] == $this->getParam('category_id');
|
||||
}
|
||||
}
|
83
sources/app/Action/TaskUpdateStartDate.php
Normal file
83
sources/app/Action/TaskUpdateStartDate.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace Action;
|
||||
|
||||
use Model\Task;
|
||||
|
||||
/**
|
||||
* Set the start date of task
|
||||
*
|
||||
* @package action
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class TaskUpdateStartDate extends Base
|
||||
{
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getCompatibleEvents()
|
||||
{
|
||||
return array(
|
||||
Task::EVENT_MOVE_COLUMN,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the action (defined by the user)
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getActionRequiredParameters()
|
||||
{
|
||||
return array(
|
||||
'column_id' => t('Column'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the event
|
||||
*
|
||||
* @access public
|
||||
* @return string[]
|
||||
*/
|
||||
public function getEventRequiredParameters()
|
||||
{
|
||||
return array(
|
||||
'task_id',
|
||||
'column_id',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the action (set the task color)
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Event data dictionary
|
||||
* @return bool True if the action was executed or false when not executed
|
||||
*/
|
||||
public function doAction(array $data)
|
||||
{
|
||||
$values = array(
|
||||
'id' => $data['task_id'],
|
||||
'date_started' => time(),
|
||||
);
|
||||
|
||||
return $this->taskModification->update($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the event data meet the action condition
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Event data dictionary
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRequiredCondition(array $data)
|
||||
{
|
||||
return $data['column_id'] == $this->getParam('column_id');
|
||||
}
|
||||
}
|
98
sources/app/Api/Action.php
Normal file
98
sources/app/Api/Action.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
/**
|
||||
* Action API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Action extends \Core\Base
|
||||
{
|
||||
public function getAvailableActions()
|
||||
{
|
||||
return $this->action->getAvailableActions();
|
||||
}
|
||||
|
||||
public function getAvailableActionEvents()
|
||||
{
|
||||
return $this->action->getAvailableEvents();
|
||||
}
|
||||
|
||||
public function getCompatibleActionEvents($action_name)
|
||||
{
|
||||
return $this->action->getCompatibleEvents($action_name);
|
||||
}
|
||||
|
||||
public function removeAction($action_id)
|
||||
{
|
||||
return $this->action->remove($action_id);
|
||||
}
|
||||
|
||||
public function getActions($project_id)
|
||||
{
|
||||
$actions = $this->action->getAllByProject($project_id);
|
||||
|
||||
foreach ($actions as $index => $action) {
|
||||
|
||||
$params = array();
|
||||
|
||||
foreach($action['params'] as $param) {
|
||||
$params[$param['name']] = $param['value'];
|
||||
}
|
||||
|
||||
$actions[$index]['params'] = $params;
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
public function createAction($project_id, $event_name, $action_name, $params)
|
||||
{
|
||||
$values = array(
|
||||
'project_id' => $project_id,
|
||||
'event_name' => $event_name,
|
||||
'action_name' => $action_name,
|
||||
'params' => $params,
|
||||
);
|
||||
|
||||
list($valid,) = $this->action->validateCreation($values);
|
||||
|
||||
if (! $valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the action exists
|
||||
$actions = $this->action->getAvailableActions();
|
||||
|
||||
if (! isset($actions[$action_name])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the event
|
||||
$action = $this->action->load($action_name, $project_id, $event_name);
|
||||
|
||||
if (! in_array($event_name, $action->getCompatibleEvents())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$required_params = $action->getActionRequiredParameters();
|
||||
|
||||
// Check missing parameters
|
||||
foreach($required_params as $param => $value) {
|
||||
if (! isset($params[$param])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check extra parameters
|
||||
foreach($params as $param => $value) {
|
||||
if (! isset($required_params[$param])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->action->create($values);
|
||||
}
|
||||
}
|
37
sources/app/Api/App.php
Normal file
37
sources/app/Api/App.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
/**
|
||||
* App API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class App extends \Core\Base
|
||||
{
|
||||
public function getTimezone()
|
||||
{
|
||||
return $this->config->get('application_timezone');
|
||||
}
|
||||
|
||||
public function getVersion()
|
||||
{
|
||||
return APP_VERSION;
|
||||
}
|
||||
|
||||
public function getDefaultTaskColor()
|
||||
{
|
||||
return $this->color->getDefaultColor();
|
||||
}
|
||||
|
||||
public function getDefaultTaskColors()
|
||||
{
|
||||
return $this->color->getDefaultColors();
|
||||
}
|
||||
|
||||
public function getColorList()
|
||||
{
|
||||
return $this->color->getList();
|
||||
}
|
||||
}
|
40
sources/app/Api/Auth.php
Normal file
40
sources/app/Api/Auth.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
use JsonRPC\AuthenticationFailure;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Base class
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Auth extends Base
|
||||
{
|
||||
/**
|
||||
* Check api credentials
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param string $class
|
||||
* @param string $method
|
||||
*/
|
||||
public function checkCredentials($username, $password, $class, $method)
|
||||
{
|
||||
$this->container['dispatcher']->dispatch('api.bootstrap', new Event);
|
||||
|
||||
if ($username !== 'jsonrpc' && ! $this->authentication->hasCaptcha($username) && $this->authentication->authenticate($username, $password)) {
|
||||
$this->checkProcedurePermission(true, $method);
|
||||
$this->userSession->refresh($this->user->getByUsername($username));
|
||||
}
|
||||
else if ($username === 'jsonrpc' && $password === $this->config->get('api_token')) {
|
||||
$this->checkProcedurePermission(false, $method);
|
||||
}
|
||||
else {
|
||||
throw new AuthenticationFailure('Wrong credentials');
|
||||
}
|
||||
}
|
||||
}
|
113
sources/app/Api/Base.php
Normal file
113
sources/app/Api/Base.php
Normal file
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
use JsonRPC\AuthenticationFailure;
|
||||
use JsonRPC\AccessDeniedException;
|
||||
|
||||
/**
|
||||
* Base class
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
abstract class Base extends \Core\Base
|
||||
{
|
||||
private $user_allowed_procedures = array(
|
||||
'getMe',
|
||||
'getMyDashboard',
|
||||
'getMyActivityStream',
|
||||
'createMyPrivateProject',
|
||||
'getMyProjectsList',
|
||||
);
|
||||
|
||||
private $both_allowed_procedures = array(
|
||||
'getTimezone',
|
||||
'getVersion',
|
||||
'getDefaultTaskColor',
|
||||
'getDefaultTaskColors',
|
||||
'getColorList',
|
||||
'getProjectById',
|
||||
'getTask',
|
||||
'getTaskByReference',
|
||||
'getAllTasks',
|
||||
'openTask',
|
||||
'closeTask',
|
||||
'moveTaskPosition',
|
||||
'createTask',
|
||||
'updateTask',
|
||||
'getBoard',
|
||||
);
|
||||
|
||||
public function checkProcedurePermission($is_user, $procedure)
|
||||
{
|
||||
$is_both_procedure = in_array($procedure, $this->both_allowed_procedures);
|
||||
$is_user_procedure = in_array($procedure, $this->user_allowed_procedures);
|
||||
|
||||
if ($is_user && ! $is_both_procedure && ! $is_user_procedure) {
|
||||
throw new AccessDeniedException('Permission denied');
|
||||
}
|
||||
else if (! $is_user && ! $is_both_procedure && $is_user_procedure) {
|
||||
throw new AccessDeniedException('Permission denied');
|
||||
}
|
||||
}
|
||||
|
||||
public function checkProjectPermission($project_id)
|
||||
{
|
||||
if ($this->userSession->isLogged() && ! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) {
|
||||
throw new AccessDeniedException('Permission denied');
|
||||
}
|
||||
}
|
||||
|
||||
public function checkTaskPermission($task_id)
|
||||
{
|
||||
if ($this->userSession->isLogged()) {
|
||||
$this->checkProjectPermission($this->taskFinder->getProjectId($task_id));
|
||||
}
|
||||
}
|
||||
|
||||
protected function formatTask($task)
|
||||
{
|
||||
if (! empty($task)) {
|
||||
$task['url'] = $this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), '', true);
|
||||
$task['color'] = $this->color->getColorProperties($task['color_id']);
|
||||
}
|
||||
|
||||
return $task;
|
||||
}
|
||||
|
||||
protected function formatTasks($tasks)
|
||||
{
|
||||
if (! empty($tasks)) {
|
||||
foreach ($tasks as &$task) {
|
||||
$task = $this->formatTask($task);
|
||||
}
|
||||
}
|
||||
|
||||
return $tasks;
|
||||
}
|
||||
|
||||
protected function formatProject($project)
|
||||
{
|
||||
if (! empty($project)) {
|
||||
$project['url'] = array(
|
||||
'board' => $this->helper->url->to('board', 'show', array('project_id' => $project['id']), '', true),
|
||||
'calendar' => $this->helper->url->to('calendar', 'show', array('project_id' => $project['id']), '', true),
|
||||
'list' => $this->helper->url->to('listing', 'show', array('project_id' => $project['id']), '', true),
|
||||
);
|
||||
}
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
protected function formatProjects($projects)
|
||||
{
|
||||
if (! empty($projects)) {
|
||||
foreach ($projects as &$project) {
|
||||
$project = $this->formatProject($project);
|
||||
}
|
||||
}
|
||||
|
||||
return $projects;
|
||||
}
|
||||
}
|
53
sources/app/Api/Board.php
Normal file
53
sources/app/Api/Board.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
/**
|
||||
* Board API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Board extends Base
|
||||
{
|
||||
public function getBoard($project_id)
|
||||
{
|
||||
$this->checkProjectPermission($project_id);
|
||||
return $this->board->getBoard($project_id);
|
||||
}
|
||||
|
||||
public function getColumns($project_id)
|
||||
{
|
||||
return $this->board->getColumns($project_id);
|
||||
}
|
||||
|
||||
public function getColumn($column_id)
|
||||
{
|
||||
return $this->board->getColumn($column_id);
|
||||
}
|
||||
|
||||
public function moveColumnUp($project_id, $column_id)
|
||||
{
|
||||
return $this->board->moveUp($project_id, $column_id);
|
||||
}
|
||||
|
||||
public function moveColumnDown($project_id, $column_id)
|
||||
{
|
||||
return $this->board->moveDown($project_id, $column_id);
|
||||
}
|
||||
|
||||
public function updateColumn($column_id, $title, $task_limit = 0, $description = '')
|
||||
{
|
||||
return $this->board->updateColumn($column_id, $title, $task_limit, $description);
|
||||
}
|
||||
|
||||
public function addColumn($project_id, $title, $task_limit = 0, $description = '')
|
||||
{
|
||||
return $this->board->addColumn($project_id, $title, $task_limit, $description);
|
||||
}
|
||||
|
||||
public function removeColumn($column_id)
|
||||
{
|
||||
return $this->board->removeColumn($column_id);
|
||||
}
|
||||
}
|
49
sources/app/Api/Category.php
Normal file
49
sources/app/Api/Category.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
/**
|
||||
* Category API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Category extends \Core\Base
|
||||
{
|
||||
public function getCategory($category_id)
|
||||
{
|
||||
return $this->category->getById($category_id);
|
||||
}
|
||||
|
||||
public function getAllCategories($project_id)
|
||||
{
|
||||
return $this->category->getAll($project_id);
|
||||
}
|
||||
|
||||
public function removeCategory($category_id)
|
||||
{
|
||||
return $this->category->remove($category_id);
|
||||
}
|
||||
|
||||
public function createCategory($project_id, $name)
|
||||
{
|
||||
$values = array(
|
||||
'project_id' => $project_id,
|
||||
'name' => $name,
|
||||
);
|
||||
|
||||
list($valid,) = $this->category->validateCreation($values);
|
||||
return $valid ? $this->category->create($values) : false;
|
||||
}
|
||||
|
||||
public function updateCategory($id, $name)
|
||||
{
|
||||
$values = array(
|
||||
'id' => $id,
|
||||
'name' => $name,
|
||||
);
|
||||
|
||||
list($valid,) = $this->category->validateModification($values);
|
||||
return $valid && $this->category->update($values);
|
||||
}
|
||||
}
|
51
sources/app/Api/Comment.php
Normal file
51
sources/app/Api/Comment.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
/**
|
||||
* Comment API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Comment extends \Core\Base
|
||||
{
|
||||
public function getComment($comment_id)
|
||||
{
|
||||
return $this->comment->getById($comment_id);
|
||||
}
|
||||
|
||||
public function getAllComments($task_id)
|
||||
{
|
||||
return $this->comment->getAll($task_id);
|
||||
}
|
||||
|
||||
public function removeComment($comment_id)
|
||||
{
|
||||
return $this->comment->remove($comment_id);
|
||||
}
|
||||
|
||||
public function createComment($task_id, $user_id, $content)
|
||||
{
|
||||
$values = array(
|
||||
'task_id' => $task_id,
|
||||
'user_id' => $user_id,
|
||||
'comment' => $content,
|
||||
);
|
||||
|
||||
list($valid,) = $this->comment->validateCreation($values);
|
||||
|
||||
return $valid ? $this->comment->create($values) : false;
|
||||
}
|
||||
|
||||
public function updateComment($id, $content)
|
||||
{
|
||||
$values = array(
|
||||
'id' => $id,
|
||||
'comment' => $content,
|
||||
);
|
||||
|
||||
list($valid,) = $this->comment->validateModification($values);
|
||||
return $valid && $this->comment->update($values);
|
||||
}
|
||||
}
|
53
sources/app/Api/File.php
Normal file
53
sources/app/Api/File.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
/**
|
||||
* File API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class File extends \Core\Base
|
||||
{
|
||||
public function getFile($file_id)
|
||||
{
|
||||
return $this->file->getById($file_id);
|
||||
}
|
||||
|
||||
public function getAllFiles($task_id)
|
||||
{
|
||||
return $this->file->getAll($task_id);
|
||||
}
|
||||
|
||||
public function downloadFile($file_id)
|
||||
{
|
||||
$file = $this->file->getById($file_id);
|
||||
|
||||
if (! empty($file)) {
|
||||
|
||||
$filename = FILES_DIR.$file['path'];
|
||||
|
||||
if (file_exists($filename)) {
|
||||
return base64_encode(file_get_contents($filename));
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function createFile($project_id, $task_id, $filename, $blob)
|
||||
{
|
||||
return $this->file->uploadContent($project_id, $task_id, $filename, $blob);
|
||||
}
|
||||
|
||||
public function removeFile($file_id)
|
||||
{
|
||||
return $this->file->remove($file_id);
|
||||
}
|
||||
|
||||
public function removeAllFiles($task_id)
|
||||
{
|
||||
return $this->file->removeAll($task_id);
|
||||
}
|
||||
}
|
111
sources/app/Api/Link.php
Normal file
111
sources/app/Api/Link.php
Normal file
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
/**
|
||||
* Link API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Link extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Get a link by id
|
||||
*
|
||||
* @access public
|
||||
* @param integer $link_id Link id
|
||||
* @return array
|
||||
*/
|
||||
public function getLinkById($link_id)
|
||||
{
|
||||
return $this->link->getById($link_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a link by name
|
||||
*
|
||||
* @access public
|
||||
* @param string $label
|
||||
* @return array
|
||||
*/
|
||||
public function getLinkByLabel($label)
|
||||
{
|
||||
return $this->link->getByLabel($label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the opposite link id
|
||||
*
|
||||
* @access public
|
||||
* @param integer $link_id Link id
|
||||
* @return integer
|
||||
*/
|
||||
public function getOppositeLinkId($link_id)
|
||||
{
|
||||
return $this->link->getOppositeLinkId($link_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all links
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getAllLinks()
|
||||
{
|
||||
return $this->link->getAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new link label
|
||||
*
|
||||
* @access public
|
||||
* @param string $label
|
||||
* @param string $opposite_label
|
||||
* @return boolean|integer
|
||||
*/
|
||||
public function createLink($label, $opposite_label = '')
|
||||
{
|
||||
$values = array(
|
||||
'label' => $label,
|
||||
'opposite_label' => $opposite_label,
|
||||
);
|
||||
|
||||
list($valid,) = $this->link->validateCreation($values);
|
||||
return $valid ? $this->link->create($label, $opposite_label) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a link
|
||||
*
|
||||
* @access public
|
||||
* @param integer $link_id
|
||||
* @param integer $opposite_link_id
|
||||
* @param string $label
|
||||
* @return boolean
|
||||
*/
|
||||
public function updateLink($link_id, $opposite_link_id, $label)
|
||||
{
|
||||
$values = array(
|
||||
'id' => $link_id,
|
||||
'opposite_id' => $opposite_link_id,
|
||||
'label' => $label,
|
||||
);
|
||||
|
||||
list($valid,) = $this->link->validateModification($values);
|
||||
return $valid && $this->link->update($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a link a the relation to its opposite
|
||||
*
|
||||
* @access public
|
||||
* @param integer $link_id
|
||||
* @return boolean
|
||||
*/
|
||||
public function removeLink($link_id)
|
||||
{
|
||||
return $this->link->remove($link_id);
|
||||
}
|
||||
}
|
55
sources/app/Api/Me.php
Normal file
55
sources/app/Api/Me.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
use Model\Subtask as SubtaskModel;
|
||||
use Model\Task as TaskModel;
|
||||
|
||||
/**
|
||||
* Me API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Me extends Base
|
||||
{
|
||||
public function getMe()
|
||||
{
|
||||
return $this->session['user'];
|
||||
}
|
||||
|
||||
public function getMyDashboard()
|
||||
{
|
||||
$user_id = $this->userSession->getId();
|
||||
$projects = $this->project->getQueryColumnStats($this->projectPermission->getActiveMemberProjectIds($user_id))->findAll();
|
||||
$tasks = $this->taskFinder->getUserQuery($user_id)->findAll();
|
||||
|
||||
return array(
|
||||
'projects' => $this->formatProjects($projects),
|
||||
'tasks' => $this->formatTasks($tasks),
|
||||
'subtasks' => $this->subtask->getUserQuery($user_id, array(SubTaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS))->findAll(),
|
||||
);
|
||||
}
|
||||
|
||||
public function getMyActivityStream()
|
||||
{
|
||||
return $this->projectActivity->getProjects($this->projectPermission->getActiveMemberProjectIds($this->userSession->getId()), 100);
|
||||
}
|
||||
|
||||
public function createMyPrivateProject($name, $description = null)
|
||||
{
|
||||
$values = array(
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'is_private' => 1,
|
||||
);
|
||||
|
||||
list($valid,) = $this->project->validateCreation($values);
|
||||
return $valid ? $this->project->create($values, $this->userSession->getId(), true) : false;
|
||||
}
|
||||
|
||||
public function getMyProjectsList()
|
||||
{
|
||||
return $this->projectPermission->getMemberProjects($this->userSession->getId());
|
||||
}
|
||||
}
|
86
sources/app/Api/Project.php
Normal file
86
sources/app/Api/Project.php
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
/**
|
||||
* Project API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Project extends Base
|
||||
{
|
||||
public function getProjectById($project_id)
|
||||
{
|
||||
$this->checkProjectPermission($project_id);
|
||||
return $this->formatProject($this->project->getById($project_id));
|
||||
}
|
||||
|
||||
public function getProjectByName($name)
|
||||
{
|
||||
return $this->formatProject($this->project->getByName($name));
|
||||
}
|
||||
|
||||
public function getAllProjects()
|
||||
{
|
||||
return $this->formatProjects($this->project->getAll());
|
||||
}
|
||||
|
||||
public function removeProject($project_id)
|
||||
{
|
||||
return $this->project->remove($project_id);
|
||||
}
|
||||
|
||||
public function enableProject($project_id)
|
||||
{
|
||||
return $this->project->enable($project_id);
|
||||
}
|
||||
|
||||
public function disableProject($project_id)
|
||||
{
|
||||
return $this->project->disable($project_id);
|
||||
}
|
||||
|
||||
public function enableProjectPublicAccess($project_id)
|
||||
{
|
||||
return $this->project->enablePublicAccess($project_id);
|
||||
}
|
||||
|
||||
public function disableProjectPublicAccess($project_id)
|
||||
{
|
||||
return $this->project->disablePublicAccess($project_id);
|
||||
}
|
||||
|
||||
public function getProjectActivities(array $project_ids)
|
||||
{
|
||||
return $this->projectActivity->getProjects($project_ids);
|
||||
}
|
||||
|
||||
public function getProjectActivity($project_id)
|
||||
{
|
||||
return $this->projectActivity->getProject($project_id);
|
||||
}
|
||||
|
||||
public function createProject($name, $description = null)
|
||||
{
|
||||
$values = array(
|
||||
'name' => $name,
|
||||
'description' => $description
|
||||
);
|
||||
|
||||
list($valid,) = $this->project->validateCreation($values);
|
||||
return $valid ? $this->project->create($values) : false;
|
||||
}
|
||||
|
||||
public function updateProject($id, $name, $description = null)
|
||||
{
|
||||
$values = array(
|
||||
'id' => $id,
|
||||
'name' => $name,
|
||||
'description' => $description
|
||||
);
|
||||
|
||||
list($valid,) = $this->project->validateModification($values);
|
||||
return $valid && $this->project->update($values);
|
||||
}
|
||||
}
|
27
sources/app/Api/ProjectPermission.php
Normal file
27
sources/app/Api/ProjectPermission.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
/**
|
||||
* ProjectPermission API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ProjectPermission extends \Core\Base
|
||||
{
|
||||
public function getMembers($project_id)
|
||||
{
|
||||
return $this->projectPermission->getMembers($project_id);
|
||||
}
|
||||
|
||||
public function revokeUser($project_id, $user_id)
|
||||
{
|
||||
return $this->projectPermission->revokeMember($project_id, $user_id);
|
||||
}
|
||||
|
||||
public function allowUser($project_id, $user_id)
|
||||
{
|
||||
return $this->projectPermission->addMember($project_id, $user_id);
|
||||
}
|
||||
}
|
64
sources/app/Api/Subtask.php
Normal file
64
sources/app/Api/Subtask.php
Normal file
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
/**
|
||||
* Subtask API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Subtask extends \Core\Base
|
||||
{
|
||||
public function getSubtask($subtask_id)
|
||||
{
|
||||
return $this->subtask->getById($subtask_id);
|
||||
}
|
||||
|
||||
public function getAllSubtasks($task_id)
|
||||
{
|
||||
return $this->subtask->getAll($task_id);
|
||||
}
|
||||
|
||||
public function removeSubtask($subtask_id)
|
||||
{
|
||||
return $this->subtask->remove($subtask_id);
|
||||
}
|
||||
|
||||
public function createSubtask($task_id, $title, $user_id = 0, $time_estimated = 0, $time_spent = 0, $status = 0)
|
||||
{
|
||||
$values = array(
|
||||
'title' => $title,
|
||||
'task_id' => $task_id,
|
||||
'user_id' => $user_id,
|
||||
'time_estimated' => $time_estimated,
|
||||
'time_spent' => $time_spent,
|
||||
'status' => $status,
|
||||
);
|
||||
|
||||
list($valid,) = $this->subtask->validateCreation($values);
|
||||
return $valid ? $this->subtask->create($values) : false;
|
||||
}
|
||||
|
||||
public function updateSubtask($id, $task_id, $title = null, $user_id = null, $time_estimated = null, $time_spent = null, $status = null)
|
||||
{
|
||||
$values = array(
|
||||
'id' => $id,
|
||||
'task_id' => $task_id,
|
||||
'title' => $title,
|
||||
'user_id' => $user_id,
|
||||
'time_estimated' => $time_estimated,
|
||||
'time_spent' => $time_spent,
|
||||
'status' => $status,
|
||||
);
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
if (is_null($value)) {
|
||||
unset($values[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
list($valid,) = $this->subtask->validateApiModification($values);
|
||||
return $valid && $this->subtask->update($values);
|
||||
}
|
||||
}
|
77
sources/app/Api/Swimlane.php
Normal file
77
sources/app/Api/Swimlane.php
Normal file
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
/**
|
||||
* Swimlane API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Swimlane extends \Core\Base
|
||||
{
|
||||
public function getActiveSwimlanes($project_id)
|
||||
{
|
||||
return $this->swimlane->getSwimlanes($project_id);
|
||||
}
|
||||
|
||||
public function getAllSwimlanes($project_id)
|
||||
{
|
||||
return $this->swimlane->getAll($project_id);
|
||||
}
|
||||
|
||||
public function getSwimlaneById($swimlane_id)
|
||||
{
|
||||
return $this->swimlane->getById($swimlane_id);
|
||||
}
|
||||
|
||||
public function getSwimlaneByName($project_id, $name)
|
||||
{
|
||||
return $this->swimlane->getByName($project_id, $name);
|
||||
}
|
||||
|
||||
public function getSwimlane($swimlane_id)
|
||||
{
|
||||
return $this->swimlane->getById($swimlane_id);
|
||||
}
|
||||
|
||||
public function getDefaultSwimlane($project_id)
|
||||
{
|
||||
return $this->swimlane->getDefault($project_id);
|
||||
}
|
||||
|
||||
public function addSwimlane($project_id, $name)
|
||||
{
|
||||
return $this->swimlane->create($project_id, $name);
|
||||
}
|
||||
|
||||
public function updateSwimlane($swimlane_id, $name)
|
||||
{
|
||||
return $this->swimlane->rename($swimlane_id, $name);
|
||||
}
|
||||
|
||||
public function removeSwimlane($project_id, $swimlane_id)
|
||||
{
|
||||
return $this->swimlane->remove($project_id, $swimlane_id);
|
||||
}
|
||||
|
||||
public function disableSwimlane($project_id, $swimlane_id)
|
||||
{
|
||||
return $this->swimlane->disable($project_id, $swimlane_id);
|
||||
}
|
||||
|
||||
public function enableSwimlane($project_id, $swimlane_id)
|
||||
{
|
||||
return $this->swimlane->enable($project_id, $swimlane_id);
|
||||
}
|
||||
|
||||
public function moveSwimlaneUp($project_id, $swimlane_id)
|
||||
{
|
||||
return $this->swimlane->moveUp($project_id, $swimlane_id);
|
||||
}
|
||||
|
||||
public function moveSwimlaneDown($project_id, $swimlane_id)
|
||||
{
|
||||
return $this->swimlane->moveDown($project_id, $swimlane_id);
|
||||
}
|
||||
}
|
128
sources/app/Api/Task.php
Normal file
128
sources/app/Api/Task.php
Normal file
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
use Model\Task as TaskModel;
|
||||
|
||||
/**
|
||||
* Task API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Task extends Base
|
||||
{
|
||||
public function getTask($task_id)
|
||||
{
|
||||
$this->checkTaskPermission($task_id);
|
||||
return $this->formatTask($this->taskFinder->getById($task_id));
|
||||
}
|
||||
|
||||
public function getTaskByReference($project_id, $reference)
|
||||
{
|
||||
$this->checkProjectPermission($project_id);
|
||||
return $this->formatTask($this->taskFinder->getByReference($project_id, $reference));
|
||||
}
|
||||
|
||||
public function getAllTasks($project_id, $status_id = TaskModel::STATUS_OPEN)
|
||||
{
|
||||
$this->checkProjectPermission($project_id);
|
||||
return $this->formatTasks($this->taskFinder->getAll($project_id, $status_id));
|
||||
}
|
||||
|
||||
public function getOverdueTasks()
|
||||
{
|
||||
return $this->taskFinder->getOverdueTasks();
|
||||
}
|
||||
|
||||
public function openTask($task_id)
|
||||
{
|
||||
$this->checkTaskPermission($task_id);
|
||||
return $this->taskStatus->open($task_id);
|
||||
}
|
||||
|
||||
public function closeTask($task_id)
|
||||
{
|
||||
$this->checkTaskPermission($task_id);
|
||||
return $this->taskStatus->close($task_id);
|
||||
}
|
||||
|
||||
public function removeTask($task_id)
|
||||
{
|
||||
return $this->task->remove($task_id);
|
||||
}
|
||||
|
||||
public function moveTaskPosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0)
|
||||
{
|
||||
$this->checkProjectPermission($project_id);
|
||||
return $this->taskPosition->movePosition($project_id, $task_id, $column_id, $position, $swimlane_id);
|
||||
}
|
||||
|
||||
public function createTask($title, $project_id, $color_id = '', $column_id = 0, $owner_id = 0, $creator_id = 0,
|
||||
$date_due = '', $description = '', $category_id = 0, $score = 0, $swimlane_id = 0,
|
||||
$recurrence_status = 0, $recurrence_trigger = 0, $recurrence_factor = 0, $recurrence_timeframe = 0,
|
||||
$recurrence_basedate = 0, $reference = '')
|
||||
{
|
||||
$this->checkProjectPermission($project_id);
|
||||
|
||||
$values = array(
|
||||
'title' => $title,
|
||||
'project_id' => $project_id,
|
||||
'color_id' => $color_id,
|
||||
'column_id' => $column_id,
|
||||
'owner_id' => $owner_id,
|
||||
'creator_id' => $creator_id,
|
||||
'date_due' => $date_due,
|
||||
'description' => $description,
|
||||
'category_id' => $category_id,
|
||||
'score' => $score,
|
||||
'swimlane_id' => $swimlane_id,
|
||||
'recurrence_status' => $recurrence_status,
|
||||
'recurrence_trigger' => $recurrence_trigger,
|
||||
'recurrence_factor' => $recurrence_factor,
|
||||
'recurrence_timeframe' => $recurrence_timeframe,
|
||||
'recurrence_basedate' => $recurrence_basedate,
|
||||
'reference' => $reference,
|
||||
);
|
||||
|
||||
list($valid,) = $this->taskValidator->validateCreation($values);
|
||||
|
||||
return $valid ? $this->taskCreation->create($values) : false;
|
||||
}
|
||||
|
||||
public function updateTask($id, $title = null, $project_id = null, $color_id = null, $owner_id = null,
|
||||
$creator_id = null, $date_due = null, $description = null, $category_id = null, $score = null,
|
||||
$recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null,
|
||||
$recurrence_timeframe = null, $recurrence_basedate = null, $reference = null)
|
||||
{
|
||||
$this->checkTaskPermission($id);
|
||||
|
||||
$values = array(
|
||||
'id' => $id,
|
||||
'title' => $title,
|
||||
'project_id' => $project_id,
|
||||
'color_id' => $color_id,
|
||||
'owner_id' => $owner_id,
|
||||
'creator_id' => $creator_id,
|
||||
'date_due' => $date_due,
|
||||
'description' => $description,
|
||||
'category_id' => $category_id,
|
||||
'score' => $score,
|
||||
'recurrence_status' => $recurrence_status,
|
||||
'recurrence_trigger' => $recurrence_trigger,
|
||||
'recurrence_factor' => $recurrence_factor,
|
||||
'recurrence_timeframe' => $recurrence_timeframe,
|
||||
'recurrence_basedate' => $recurrence_basedate,
|
||||
'reference' => $reference,
|
||||
);
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
if (is_null($value)) {
|
||||
unset($values[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
list($valid) = $this->taskValidator->validateApiModification($values);
|
||||
return $valid && $this->taskModification->update($values);
|
||||
}
|
||||
}
|
77
sources/app/Api/TaskLink.php
Normal file
77
sources/app/Api/TaskLink.php
Normal file
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
/**
|
||||
* TaskLink API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class TaskLink extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Get a task link
|
||||
*
|
||||
* @access public
|
||||
* @param integer $task_link_id Task link id
|
||||
* @return array
|
||||
*/
|
||||
public function getTaskLinkById($task_link_id)
|
||||
{
|
||||
return $this->taskLink->getById($task_link_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all links attached to a task
|
||||
*
|
||||
* @access public
|
||||
* @param integer $task_id Task id
|
||||
* @return array
|
||||
*/
|
||||
public function getAllTaskLinks($task_id)
|
||||
{
|
||||
return $this->taskLink->getAll($task_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new link
|
||||
*
|
||||
* @access public
|
||||
* @param integer $task_id Task id
|
||||
* @param integer $opposite_task_id Opposite task id
|
||||
* @param integer $link_id Link id
|
||||
* @return integer Task link id
|
||||
*/
|
||||
public function createTaskLink($task_id, $opposite_task_id, $link_id)
|
||||
{
|
||||
return $this->taskLink->create($task_id, $opposite_task_id, $link_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a task link
|
||||
*
|
||||
* @access public
|
||||
* @param integer $task_link_id Task link id
|
||||
* @param integer $task_id Task id
|
||||
* @param integer $opposite_task_id Opposite task id
|
||||
* @param integer $link_id Link id
|
||||
* @return boolean
|
||||
*/
|
||||
public function updateTaskLink($task_link_id, $task_id, $opposite_task_id, $link_id)
|
||||
{
|
||||
return $this->taskLink->update($task_link_id, $task_id, $opposite_task_id, $link_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a link between two tasks
|
||||
*
|
||||
* @access public
|
||||
* @param integer $task_link_id
|
||||
* @return boolean
|
||||
*/
|
||||
public function removeTaskLink($task_link_id)
|
||||
{
|
||||
return $this->taskLink->remove($task_link_id);
|
||||
}
|
||||
}
|
87
sources/app/Api/User.php
Normal file
87
sources/app/Api/User.php
Normal file
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
namespace Api;
|
||||
|
||||
use Auth\Ldap;
|
||||
|
||||
/**
|
||||
* User API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class User extends \Core\Base
|
||||
{
|
||||
public function getUser($user_id)
|
||||
{
|
||||
return $this->user->getById($user_id);
|
||||
}
|
||||
|
||||
public function getAllUsers()
|
||||
{
|
||||
return $this->user->getAll();
|
||||
}
|
||||
|
||||
public function removeUser($user_id)
|
||||
{
|
||||
return $this->user->remove($user_id);
|
||||
}
|
||||
|
||||
public function createUser($username, $password, $name = '', $email = '', $is_admin = 0, $is_project_admin = 0)
|
||||
{
|
||||
$values = array(
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'confirmation' => $password,
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'is_admin' => $is_admin,
|
||||
'is_project_admin' => $is_project_admin,
|
||||
);
|
||||
|
||||
list($valid,) = $this->user->validateCreation($values);
|
||||
return $valid ? $this->user->create($values) : false;
|
||||
}
|
||||
|
||||
public function createLdapUser($username = '', $email = '', $is_admin = 0, $is_project_admin = 0)
|
||||
{
|
||||
$ldap = new Ldap($this->container);
|
||||
$user = $ldap->lookup($username, $email);
|
||||
|
||||
if (! $user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$values = array(
|
||||
'username' => $user['username'],
|
||||
'name' => $user['name'],
|
||||
'email' => $user['email'],
|
||||
'is_ldap_user' => 1,
|
||||
'is_admin' => $is_admin,
|
||||
'is_project_admin' => $is_project_admin,
|
||||
);
|
||||
|
||||
return $this->user->create($values);
|
||||
}
|
||||
|
||||
public function updateUser($id, $username = null, $name = null, $email = null, $is_admin = null, $is_project_admin = null)
|
||||
{
|
||||
$values = array(
|
||||
'id' => $id,
|
||||
'username' => $username,
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'is_admin' => $is_admin,
|
||||
'is_project_admin' => $is_project_admin,
|
||||
);
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
if (is_null($value)) {
|
||||
unset($values[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
list($valid,) = $this->user->validateApiModification($values);
|
||||
return $valid && $this->user->update($values);
|
||||
}
|
||||
}
|
122
sources/app/Auth/Github.php
Normal file
122
sources/app/Auth/Github.php
Normal file
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
namespace Auth;
|
||||
|
||||
use Event\AuthEvent;
|
||||
|
||||
/**
|
||||
* Github backend
|
||||
*
|
||||
* @package auth
|
||||
*/
|
||||
class Github extends Base
|
||||
{
|
||||
/**
|
||||
* Backend name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AUTH_NAME = 'Github';
|
||||
|
||||
/**
|
||||
* OAuth2 instance
|
||||
*
|
||||
* @access private
|
||||
* @var \Core\OAuth2
|
||||
*/
|
||||
private $service;
|
||||
|
||||
/**
|
||||
* Authenticate a Github user
|
||||
*
|
||||
* @access public
|
||||
* @param string $github_id Github user id
|
||||
* @return boolean
|
||||
*/
|
||||
public function authenticate($github_id)
|
||||
{
|
||||
$user = $this->user->getByGithubId($github_id);
|
||||
|
||||
if (! empty($user)) {
|
||||
$this->userSession->refresh($user);
|
||||
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink a Github account for a given user
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
* @return boolean
|
||||
*/
|
||||
public function unlink($user_id)
|
||||
{
|
||||
return $this->user->update(array(
|
||||
'id' => $user_id,
|
||||
'github_id' => '',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user table based on the Github profile information
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
* @param array $profile Github profile
|
||||
* @return boolean
|
||||
*/
|
||||
public function updateUser($user_id, array $profile)
|
||||
{
|
||||
$user = $this->user->getById($user_id);
|
||||
|
||||
return $this->user->update(array(
|
||||
'id' => $user_id,
|
||||
'github_id' => $profile['id'],
|
||||
'email' => empty($user['email']) ? $profile['email'] : $user['email'],
|
||||
'name' => empty($user['name']) ? $profile['name'] : $user['name'],
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth2 configured service
|
||||
*
|
||||
* @access public
|
||||
* @return \Core\OAuth2
|
||||
*/
|
||||
public function getService()
|
||||
{
|
||||
if (empty($this->service)) {
|
||||
$this->service = $this->oauth->createService(
|
||||
GITHUB_CLIENT_ID,
|
||||
GITHUB_CLIENT_SECRET,
|
||||
$this->helper->url->to('oauth', 'github', array(), '', true),
|
||||
GITHUB_OAUTH_AUTHORIZE_URL,
|
||||
GITHUB_OAUTH_TOKEN_URL,
|
||||
array()
|
||||
);
|
||||
}
|
||||
|
||||
return $this->service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Github profile
|
||||
*
|
||||
* @access public
|
||||
* @param string $code
|
||||
* @return array
|
||||
*/
|
||||
public function getProfile($code)
|
||||
{
|
||||
$this->getService()->getAccessToken($code);
|
||||
|
||||
return $this->httpClient->getJson(
|
||||
GITHUB_API_URL.'user',
|
||||
array($this->getService()->getAuthorizationHeader())
|
||||
);
|
||||
}
|
||||
}
|
122
sources/app/Auth/Gitlab.php
Normal file
122
sources/app/Auth/Gitlab.php
Normal file
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
namespace Auth;
|
||||
|
||||
use Event\AuthEvent;
|
||||
|
||||
/**
|
||||
* Gitlab backend
|
||||
*
|
||||
* @package auth
|
||||
*/
|
||||
class Gitlab extends Base
|
||||
{
|
||||
/**
|
||||
* Backend name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AUTH_NAME = 'Gitlab';
|
||||
|
||||
/**
|
||||
* OAuth2 instance
|
||||
*
|
||||
* @access private
|
||||
* @var \Core\OAuth2
|
||||
*/
|
||||
private $service;
|
||||
|
||||
/**
|
||||
* Authenticate a Gitlab user
|
||||
*
|
||||
* @access public
|
||||
* @param string $gitlab_id Gitlab user id
|
||||
* @return boolean
|
||||
*/
|
||||
public function authenticate($gitlab_id)
|
||||
{
|
||||
$user = $this->user->getByGitlabId($gitlab_id);
|
||||
|
||||
if (! empty($user)) {
|
||||
$this->userSession->refresh($user);
|
||||
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink a Gitlab account for a given user
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
* @return boolean
|
||||
*/
|
||||
public function unlink($user_id)
|
||||
{
|
||||
return $this->user->update(array(
|
||||
'id' => $user_id,
|
||||
'gitlab_id' => '',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user table based on the Gitlab profile information
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
* @param array $profile Gitlab profile
|
||||
* @return boolean
|
||||
*/
|
||||
public function updateUser($user_id, array $profile)
|
||||
{
|
||||
$user = $this->user->getById($user_id);
|
||||
|
||||
return $this->user->update(array(
|
||||
'id' => $user_id,
|
||||
'gitlab_id' => $profile['id'],
|
||||
'email' => empty($user['email']) ? $profile['email'] : $user['email'],
|
||||
'name' => empty($user['name']) ? $profile['name'] : $user['name'],
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth2 configured service
|
||||
*
|
||||
* @access public
|
||||
* @return \Core\OAuth2
|
||||
*/
|
||||
public function getService()
|
||||
{
|
||||
if (empty($this->service)) {
|
||||
$this->service = $this->oauth->createService(
|
||||
GITLAB_CLIENT_ID,
|
||||
GITLAB_CLIENT_SECRET,
|
||||
$this->helper->url->to('oauth', 'gitlab', array(), '', true),
|
||||
GITLAB_OAUTH_AUTHORIZE_URL,
|
||||
GITLAB_OAUTH_TOKEN_URL,
|
||||
array()
|
||||
);
|
||||
}
|
||||
|
||||
return $this->service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Gitlab profile
|
||||
*
|
||||
* @access public
|
||||
* @param string $code
|
||||
* @return array
|
||||
*/
|
||||
public function getProfile($code)
|
||||
{
|
||||
$this->getService()->getAccessToken($code);
|
||||
|
||||
return $this->httpClient->getJson(
|
||||
GITLAB_API_URL.'user',
|
||||
array($this->getService()->getAuthorizationHeader())
|
||||
);
|
||||
}
|
||||
}
|
82
sources/app/Console/LocaleComparator.php
Normal file
82
sources/app/Console/LocaleComparator.php
Normal file
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace Console;
|
||||
|
||||
use RecursiveIteratorIterator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class LocaleComparator extends Base
|
||||
{
|
||||
const REF_LOCALE = 'fr_FR';
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('locale:compare')
|
||||
->setDescription('Compare application translations with the '.self::REF_LOCALE.' locale');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$strings = array();
|
||||
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('app'));
|
||||
$it->rewind();
|
||||
|
||||
while ($it->valid()) {
|
||||
|
||||
if (! $it->isDot() && substr($it->key(), -4) === '.php') {
|
||||
$strings = array_merge($strings, $this->search($it->key()));
|
||||
}
|
||||
|
||||
$it->next();
|
||||
}
|
||||
|
||||
$this->compare(array_unique($strings));
|
||||
}
|
||||
|
||||
public function show(array $strings)
|
||||
{
|
||||
foreach ($strings as $string) {
|
||||
echo " '".str_replace("'", "\'", $string)."' => '',".PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
public function compare(array $strings)
|
||||
{
|
||||
$reference_file = 'app/Locale/'.self::REF_LOCALE.'/translations.php';
|
||||
$reference = include $reference_file;
|
||||
|
||||
echo str_repeat('#', 70).PHP_EOL;
|
||||
echo 'MISSING STRINGS'.PHP_EOL;
|
||||
echo str_repeat('#', 70).PHP_EOL;
|
||||
$this->show(array_diff($strings, array_keys($reference)));
|
||||
|
||||
echo str_repeat('#', 70).PHP_EOL;
|
||||
echo 'USELESS STRINGS'.PHP_EOL;
|
||||
echo str_repeat('#', 70).PHP_EOL;
|
||||
$this->show(array_diff(array_keys($reference), $strings));
|
||||
}
|
||||
|
||||
public function search($filename)
|
||||
{
|
||||
$content = file_get_contents($filename);
|
||||
$strings = array();
|
||||
|
||||
if (preg_match_all('/\b[et]\((\'\K.*?\') *[\)\,]/', $content, $matches) && isset($matches[1])) {
|
||||
$strings = $matches[1];
|
||||
}
|
||||
|
||||
if (preg_match_all('/\bdt\((\'\K.*?\') *[\)\,]/', $content, $matches) && isset($matches[1])) {
|
||||
$strings = array_merge($strings, $matches[1]);
|
||||
}
|
||||
|
||||
array_walk($strings, function(&$value) {
|
||||
$value = trim($value, "'");
|
||||
$value = str_replace("\'", "'", $value);
|
||||
});
|
||||
|
||||
return $strings;
|
||||
}
|
||||
}
|
57
sources/app/Console/LocaleSync.php
Normal file
57
sources/app/Console/LocaleSync.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace Console;
|
||||
|
||||
use DirectoryIterator;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class LocaleSync extends Base
|
||||
{
|
||||
const REF_LOCALE = 'fr_FR';
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('locale:sync')
|
||||
->setDescription('Synchronize all translations based on the '.self::REF_LOCALE.' locale');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$reference_file = 'app/Locale/'.self::REF_LOCALE.'/translations.php';
|
||||
$reference = include $reference_file;
|
||||
|
||||
foreach (new DirectoryIterator('app/Locale') as $fileInfo) {
|
||||
|
||||
if (! $fileInfo->isDot() && $fileInfo->isDir() && $fileInfo->getFilename() !== self::REF_LOCALE) {
|
||||
|
||||
$filename = 'app/Locale/'.$fileInfo->getFilename().'/translations.php';
|
||||
echo $fileInfo->getFilename().' ('.$filename.')'.PHP_EOL;
|
||||
|
||||
file_put_contents($filename, $this->updateFile($reference, $filename));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function updateFile(array $reference, $outdated_file)
|
||||
{
|
||||
$outdated = include $outdated_file;
|
||||
|
||||
$output = '<?php'.PHP_EOL.PHP_EOL;
|
||||
$output .= 'return array('.PHP_EOL;
|
||||
|
||||
foreach ($reference as $key => $value) {
|
||||
|
||||
if (! empty($outdated[$key])) {
|
||||
$output .= " '".str_replace("'", "\'", $key)."' => '".str_replace("'", "\'", $outdated[$key])."',\n";
|
||||
}
|
||||
else {
|
||||
$output .= " // '".str_replace("'", "\'", $key)."' => '',\n";
|
||||
}
|
||||
}
|
||||
|
||||
$output .= ");\n";
|
||||
return $output;
|
||||
}
|
||||
}
|
34
sources/app/Console/ProjectDailyColumnStatsExport.php
Normal file
34
sources/app/Console/ProjectDailyColumnStatsExport.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace Console;
|
||||
|
||||
use Core\Tool;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ProjectDailyColumnStatsExport extends Base
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('export:daily-project-column-stats')
|
||||
->setDescription('Daily project column stats CSV export (number of tasks per column and per day)')
|
||||
->addArgument('project_id', InputArgument::REQUIRED, 'Project id')
|
||||
->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)')
|
||||
->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$data = $this->projectDailyColumnStats->getAggregatedMetrics(
|
||||
$input->getArgument('project_id'),
|
||||
$input->getArgument('start_date'),
|
||||
$input->getArgument('end_date')
|
||||
);
|
||||
|
||||
if (is_array($data)) {
|
||||
Tool::csv($data);
|
||||
}
|
||||
}
|
||||
}
|
28
sources/app/Console/ProjectDailyStatsCalculation.php
Normal file
28
sources/app/Console/ProjectDailyStatsCalculation.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Console;
|
||||
|
||||
use Model\Project;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ProjectDailyStatsCalculation extends Base
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('projects:daily-stats')
|
||||
->setDescription('Calculate daily statistics for all projects');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$projects = $this->project->getAllByStatus(Project::ACTIVE);
|
||||
|
||||
foreach ($projects as $project) {
|
||||
$output->writeln('Run calculation for '.$project['name']);
|
||||
$this->projectDailyColumnStats->updateTotals($project['id'], date('Y-m-d'));
|
||||
$this->projectDailyStats->updateTotals($project['id'], date('Y-m-d'));
|
||||
}
|
||||
}
|
||||
}
|
45
sources/app/Controller/Activity.php
Normal file
45
sources/app/Controller/Activity.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
/**
|
||||
* Activity stream
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Activity extends Base
|
||||
{
|
||||
/**
|
||||
* Activity page for a project
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function project()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
|
||||
$this->response->html($this->template->layout('activity/project', array(
|
||||
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
|
||||
'events' => $this->projectActivity->getProject($project['id']),
|
||||
'project' => $project,
|
||||
'title' => t('%s\'s activity', $project['name'])
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display task activities
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function task()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
$this->response->html($this->taskLayout('activity/task', array(
|
||||
'title' => $task['title'],
|
||||
'task' => $task,
|
||||
'events' => $this->projectActivity->getTask($task['id']),
|
||||
)));
|
||||
}
|
||||
}
|
85
sources/app/Controller/Auth.php
Normal file
85
sources/app/Controller/Auth.php
Normal file
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Gregwar\Captcha\CaptchaBuilder;
|
||||
|
||||
/**
|
||||
* Authentication controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Auth extends Base
|
||||
{
|
||||
/**
|
||||
* Display the form login
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function login(array $values = array(), array $errors = array())
|
||||
{
|
||||
if ($this->userSession->isLogged()) {
|
||||
$this->response->redirect($this->helper->url->to('app', 'index'));
|
||||
}
|
||||
|
||||
$this->response->html($this->template->layout('auth/index', array(
|
||||
'captcha' => isset($values['username']) && $this->authentication->hasCaptcha($values['username']),
|
||||
'errors' => $errors,
|
||||
'values' => $values,
|
||||
'no_layout' => true,
|
||||
'title' => t('Login')
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check credentials
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function check()
|
||||
{
|
||||
$values = $this->request->getValues();
|
||||
list($valid, $errors) = $this->authentication->validateForm($values);
|
||||
|
||||
if ($valid) {
|
||||
|
||||
if (! empty($this->session['login_redirect']) && ! filter_var($this->session['login_redirect'], FILTER_VALIDATE_URL)) {
|
||||
$redirect = $this->session['login_redirect'];
|
||||
unset($this->session['login_redirect']);
|
||||
$this->response->redirect($redirect);
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('app', 'index'));
|
||||
}
|
||||
|
||||
$this->login($values, $errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout and destroy session
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
$this->authentication->backend('rememberMe')->destroy($this->userSession->getId());
|
||||
$this->session->close();
|
||||
$this->response->redirect($this->helper->url->to('auth', 'login'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display captcha image
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function captcha()
|
||||
{
|
||||
$this->response->contentType('image/jpeg');
|
||||
|
||||
$builder = new CaptchaBuilder;
|
||||
$builder->build();
|
||||
$this->session['captcha'] = $builder->getPhrase();
|
||||
$builder->output();
|
||||
}
|
||||
}
|
170
sources/app/Controller/Column.php
Normal file
170
sources/app/Controller/Column.php
Normal file
|
@ -0,0 +1,170 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
/**
|
||||
* Column controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Column extends Base
|
||||
{
|
||||
/**
|
||||
* Display columns list
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function index(array $values = array(), array $errors = array())
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$columns = $this->board->getColumns($project['id']);
|
||||
|
||||
foreach ($columns as $column) {
|
||||
$values['title['.$column['id'].']'] = $column['title'];
|
||||
$values['description['.$column['id'].']'] = $column['description'];
|
||||
$values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null;
|
||||
}
|
||||
|
||||
$this->response->html($this->projectLayout('column/index', array(
|
||||
'errors' => $errors,
|
||||
'values' => $values + array('project_id' => $project['id']),
|
||||
'columns' => $columns,
|
||||
'project' => $project,
|
||||
'title' => t('Edit board')
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and add a new column
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$columns = $this->board->getColumnsList($project['id']);
|
||||
$data = $this->request->getValues();
|
||||
$values = array();
|
||||
|
||||
foreach ($columns as $column_id => $column_title) {
|
||||
$values['title['.$column_id.']'] = $column_title;
|
||||
}
|
||||
|
||||
list($valid, $errors) = $this->board->validateCreation($data);
|
||||
|
||||
if ($valid) {
|
||||
|
||||
if ($this->board->addColumn($project['id'], $data['title'], $data['task_limit'], $data['description'])) {
|
||||
$this->session->flash(t('Board updated successfully.'));
|
||||
$this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
|
||||
}
|
||||
else {
|
||||
$this->session->flashError(t('Unable to update this board.'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->index($values, $errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a form to edit a column
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function edit(array $values = array(), array $errors = array())
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$column = $this->board->getColumn($this->request->getIntegerParam('column_id'));
|
||||
|
||||
$this->response->html($this->projectLayout('column/edit', array(
|
||||
'errors' => $errors,
|
||||
'values' => $values ?: $column,
|
||||
'project' => $project,
|
||||
'column' => $column,
|
||||
'title' => t('Edit column "%s"', $column['title'])
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and update a column
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$values = $this->request->getValues();
|
||||
|
||||
list($valid, $errors) = $this->board->validateModification($values);
|
||||
|
||||
if ($valid) {
|
||||
|
||||
if ($this->board->updateColumn($values['id'], $values['title'], $values['task_limit'], $values['description'])) {
|
||||
$this->session->flash(t('Board updated successfully.'));
|
||||
$this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
|
||||
}
|
||||
else {
|
||||
$this->session->flashError(t('Unable to update this board.'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->edit($values, $errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a column up or down
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function move()
|
||||
{
|
||||
$this->checkCSRFParam();
|
||||
$project = $this->getProject();
|
||||
$column_id = $this->request->getIntegerParam('column_id');
|
||||
$direction = $this->request->getStringParam('direction');
|
||||
|
||||
if ($direction === 'up' || $direction === 'down') {
|
||||
$this->board->{'move'.$direction}($project['id'], $column_id);
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm column suppression
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function confirm()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
|
||||
$this->response->html($this->projectLayout('column/remove', array(
|
||||
'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')),
|
||||
'project' => $project,
|
||||
'title' => t('Remove a column from a board')
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a column
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function remove()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$this->checkCSRFParam();
|
||||
$column = $this->board->getColumn($this->request->getIntegerParam('column_id'));
|
||||
|
||||
if (! empty($column) && $this->board->removeColumn($column['id'])) {
|
||||
$this->session->flash(t('Column removed successfully.'));
|
||||
}
|
||||
else {
|
||||
$this->session->flashError(t('Unable to remove this column.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
|
||||
}
|
||||
}
|
51
sources/app/Controller/Doc.php
Normal file
51
sources/app/Controller/Doc.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Parsedown;
|
||||
|
||||
/**
|
||||
* Documentation controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Doc extends Base
|
||||
{
|
||||
private function readFile($filename)
|
||||
{
|
||||
$url = $this->helper->url;
|
||||
$data = file_get_contents($filename);
|
||||
list($title,, $content) = explode("\n", $data, 3);
|
||||
|
||||
$replaceUrl = function (array $matches) use ($url) {
|
||||
return '('.$url->to('doc', 'show', array('file' => str_replace('.markdown', '', $matches[1]))).')';
|
||||
};
|
||||
|
||||
$content = preg_replace_callback('/\((.*.markdown)\)/', $replaceUrl, $data);
|
||||
|
||||
return array(
|
||||
'content' => Parsedown::instance()->text($content),
|
||||
'title' => $title !== 'Documentation' ? t('Documentation: %s', $title) : $title,
|
||||
);
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
$filename = $this->request->getStringParam('file', 'index');
|
||||
|
||||
if (! preg_match('/^[a-z0-9\-]+/', $filename)) {
|
||||
$filename = 'index';
|
||||
}
|
||||
|
||||
$filename = __DIR__.'/../../doc/'.$filename.'.markdown';
|
||||
|
||||
if (! file_exists($filename)) {
|
||||
$filename = __DIR__.'/../../doc/index.markdown';
|
||||
}
|
||||
|
||||
$this->response->html($this->template->layout('doc/show', $this->readFile($filename) + array(
|
||||
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
|
||||
)));
|
||||
}
|
||||
}
|
56
sources/app/Controller/Feed.php
Normal file
56
sources/app/Controller/Feed.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
/**
|
||||
* Atom/RSS Feed controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Feed extends Base
|
||||
{
|
||||
/**
|
||||
* RSS feed for a user
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
$token = $this->request->getStringParam('token');
|
||||
$user = $this->user->getByToken($token);
|
||||
|
||||
// Token verification
|
||||
if (empty($user)) {
|
||||
$this->forbidden(true);
|
||||
}
|
||||
|
||||
$projects = $this->projectPermission->getActiveMemberProjects($user['id']);
|
||||
|
||||
$this->response->xml($this->template->render('feed/user', array(
|
||||
'events' => $this->projectActivity->getProjects(array_keys($projects)),
|
||||
'user' => $user,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* RSS feed for a project
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function project()
|
||||
{
|
||||
$token = $this->request->getStringParam('token');
|
||||
$project = $this->project->getByToken($token);
|
||||
|
||||
// Token verification
|
||||
if (empty($project)) {
|
||||
$this->forbidden(true);
|
||||
}
|
||||
|
||||
$this->response->xml($this->template->render('feed/project', array(
|
||||
'events' => $this->projectActivity->getProject($project['id']),
|
||||
'project' => $project,
|
||||
)));
|
||||
}
|
||||
}
|
151
sources/app/Controller/Gantt.php
Normal file
151
sources/app/Controller/Gantt.php
Normal file
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\Task as TaskModel;
|
||||
|
||||
/**
|
||||
* Gantt controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Gantt extends Base
|
||||
{
|
||||
/**
|
||||
* Show Gantt chart for all projects
|
||||
*/
|
||||
public function projects()
|
||||
{
|
||||
if ($this->userSession->isAdmin()) {
|
||||
$project_ids = $this->project->getAllIds();
|
||||
}
|
||||
else {
|
||||
$project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId());
|
||||
}
|
||||
|
||||
$this->response->html($this->template->layout('gantt/projects', array(
|
||||
'projects' => $this->project->getGanttBars($project_ids),
|
||||
'title' => t('Gantt chart for all projects'),
|
||||
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save new project start date and end date
|
||||
*/
|
||||
public function saveProjectDate()
|
||||
{
|
||||
$values = $this->request->getJson();
|
||||
|
||||
$result = $this->project->update(array(
|
||||
'id' => $values['id'],
|
||||
'start_date' => $this->dateParser->getIsoDate(strtotime($values['start'])),
|
||||
'end_date' => $this->dateParser->getIsoDate(strtotime($values['end'])),
|
||||
));
|
||||
|
||||
if (! $result) {
|
||||
$this->response->json(array('message' => 'Unable to save project'), 400);
|
||||
}
|
||||
|
||||
$this->response->json(array('message' => 'OK'), 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Gantt chart for one project
|
||||
*/
|
||||
public function project()
|
||||
{
|
||||
$params = $this->getProjectFilters('gantt', 'project');
|
||||
$filter = $this->taskFilter->search($params['filters']['search'])->filterByProject($params['project']['id']);
|
||||
$sorting = $this->request->getStringParam('sorting', 'board');
|
||||
|
||||
if ($sorting === 'date') {
|
||||
$filter->getQuery()->asc(TaskModel::TABLE.'.date_started')->asc(TaskModel::TABLE.'.date_creation');
|
||||
}
|
||||
else {
|
||||
$filter->getQuery()->asc('column_position')->asc(TaskModel::TABLE.'.position');
|
||||
}
|
||||
|
||||
$this->response->html($this->template->layout('gantt/project', $params + array(
|
||||
'users_list' => $this->projectPermission->getMemberList($params['project']['id'], false),
|
||||
'sorting' => $sorting,
|
||||
'tasks' => $filter->toGanttBars(),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save new task start date and due date
|
||||
*/
|
||||
public function saveTaskDate()
|
||||
{
|
||||
$this->getProject();
|
||||
$values = $this->request->getJson();
|
||||
|
||||
$result = $this->taskModification->update(array(
|
||||
'id' => $values['id'],
|
||||
'date_started' => strtotime($values['start']),
|
||||
'date_due' => strtotime($values['end']),
|
||||
));
|
||||
|
||||
if (! $result) {
|
||||
$this->response->json(array('message' => 'Unable to save task'), 400);
|
||||
}
|
||||
|
||||
$this->response->json(array('message' => 'OK'), 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified form to create a new task
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function task(array $values = array(), array $errors = array())
|
||||
{
|
||||
$project = $this->getProject();
|
||||
|
||||
$this->response->html($this->template->render('gantt/task_creation', array(
|
||||
'errors' => $errors,
|
||||
'values' => $values + array(
|
||||
'project_id' => $project['id'],
|
||||
'column_id' => $this->board->getFirstColumn($project['id']),
|
||||
'position' => 1
|
||||
),
|
||||
'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true),
|
||||
'colors_list' => $this->color->getList(),
|
||||
'categories_list' => $this->category->getList($project['id']),
|
||||
'swimlanes_list' => $this->swimlane->getList($project['id'], false, true),
|
||||
'date_format' => $this->config->get('application_date_format'),
|
||||
'date_formats' => $this->dateParser->getAvailableFormats(),
|
||||
'title' => $project['name'].' > '.t('New task')
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and save a new task
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function saveTask()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$values = $this->request->getValues();
|
||||
|
||||
list($valid, $errors) = $this->taskValidator->validateCreation($values);
|
||||
|
||||
if ($valid) {
|
||||
|
||||
$task_id = $this->taskCreation->create($values);
|
||||
|
||||
if ($task_id !== false) {
|
||||
$this->session->flash(t('Task created successfully.'));
|
||||
$this->response->redirect($this->helper->url->to('gantt', 'project', array('project_id' => $project['id'])));
|
||||
}
|
||||
else {
|
||||
$this->session->flashError(t('Unable to create your task.'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->task($values, $errors);
|
||||
}
|
||||
}
|
98
sources/app/Controller/Ical.php
Normal file
98
sources/app/Controller/Ical.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\TaskFilter;
|
||||
use Eluceo\iCal\Component\Calendar as iCalendar;
|
||||
|
||||
/**
|
||||
* iCalendar controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Ical extends Base
|
||||
{
|
||||
/**
|
||||
* Get user iCalendar
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
$token = $this->request->getStringParam('token');
|
||||
$user = $this->user->getByToken($token);
|
||||
|
||||
// Token verification
|
||||
if (empty($user)) {
|
||||
$this->forbidden(true);
|
||||
}
|
||||
|
||||
// Common filter
|
||||
$filter = $this->taskFilter
|
||||
->create()
|
||||
->filterByOwner($user['id']);
|
||||
|
||||
// Calendar properties
|
||||
$calendar = new iCalendar('Kanboard');
|
||||
$calendar->setName($user['name'] ?: $user['username']);
|
||||
$calendar->setDescription($user['name'] ?: $user['username']);
|
||||
$calendar->setPublishedTTL('PT1H');
|
||||
|
||||
$this->renderCalendar($filter, $calendar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project iCalendar
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function project()
|
||||
{
|
||||
$token = $this->request->getStringParam('token');
|
||||
$project = $this->project->getByToken($token);
|
||||
|
||||
// Token verification
|
||||
if (empty($project)) {
|
||||
$this->forbidden(true);
|
||||
}
|
||||
|
||||
// Common filter
|
||||
$filter = $this->taskFilter
|
||||
->create()
|
||||
->filterByProject($project['id']);
|
||||
|
||||
// Calendar properties
|
||||
$calendar = new iCalendar('Kanboard');
|
||||
$calendar->setName($project['name']);
|
||||
$calendar->setDescription($project['name']);
|
||||
$calendar->setPublishedTTL('PT1H');
|
||||
|
||||
$this->renderCalendar($filter, $calendar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Common method to render iCal events
|
||||
*
|
||||
* @access private
|
||||
*/
|
||||
private function renderCalendar(TaskFilter $filter, iCalendar $calendar)
|
||||
{
|
||||
$start = $this->request->getStringParam('start', strtotime('-2 month'));
|
||||
$end = $this->request->getStringParam('end', strtotime('+6 months'));
|
||||
|
||||
// Tasks
|
||||
if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') {
|
||||
$filter->copy()->filterByCreationDateRange($start, $end)->addDateTimeIcalEvents('date_creation', 'date_completed', $calendar);
|
||||
}
|
||||
else {
|
||||
$filter->copy()->filterByStartDateRange($start, $end)->addDateTimeIcalEvents('date_started', 'date_completed', $calendar);
|
||||
}
|
||||
|
||||
// Tasks with due date
|
||||
$filter->copy()->filterByDueDateRange($start, $end)->addAllDayIcalEvents('date_due', $calendar);
|
||||
|
||||
$this->response->contentType('text/calendar; charset=utf-8');
|
||||
echo $calendar->render();
|
||||
}
|
||||
}
|
37
sources/app/Controller/Listing.php
Normal file
37
sources/app/Controller/Listing.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\Task as TaskModel;
|
||||
|
||||
/**
|
||||
* List view controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Listing extends Base
|
||||
{
|
||||
/**
|
||||
* Show list view for projects
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function show()
|
||||
{
|
||||
$params = $this->getProjectFilters('listing', 'show');
|
||||
$query = $this->taskFilter->search($params['filters']['search'])->filterByProject($params['project']['id'])->getQuery();
|
||||
|
||||
$paginator = $this->paginator
|
||||
->setUrl('listing', 'show', array('project_id' => $params['project']['id']))
|
||||
->setMax(30)
|
||||
->setOrder(TaskModel::TABLE.'.id')
|
||||
->setDirection('DESC')
|
||||
->setQuery($query)
|
||||
->calculate();
|
||||
|
||||
$this->response->html($this->template->layout('listing/show', $params + array(
|
||||
'paginator' => $paginator,
|
||||
)));
|
||||
}
|
||||
}
|
133
sources/app/Controller/Oauth.php
Normal file
133
sources/app/Controller/Oauth.php
Normal file
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
/**
|
||||
* OAuth controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Oauth extends Base
|
||||
{
|
||||
/**
|
||||
* Link or authenticate a Google account
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function google()
|
||||
{
|
||||
$this->step1('google');
|
||||
}
|
||||
|
||||
/**
|
||||
* Link or authenticate a Github account
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function github()
|
||||
{
|
||||
$this->step1('github');
|
||||
}
|
||||
|
||||
/**
|
||||
* Link or authenticate a Gitlab account
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function gitlab()
|
||||
{
|
||||
$this->step1('gitlab');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink external account
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function unlink($backend = '')
|
||||
{
|
||||
$backend = $this->request->getStringParam('backend', $backend);
|
||||
$this->checkCSRFParam();
|
||||
|
||||
if ($this->authentication->backend($backend)->unlink($this->userSession->getId())) {
|
||||
$this->session->flash(t('Your external account is not linked anymore to your profile.'));
|
||||
}
|
||||
else {
|
||||
$this->session->flashError(t('Unable to unlink your external account.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the provider if no code received
|
||||
*
|
||||
* @access private
|
||||
*/
|
||||
private function step1($backend)
|
||||
{
|
||||
$code = $this->request->getStringParam('code');
|
||||
|
||||
if (! empty($code)) {
|
||||
$this->step2($backend, $code);
|
||||
}
|
||||
else {
|
||||
$this->response->redirect($this->authentication->backend($backend)->getService()->getAuthorizationUrl());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Link or authenticate the user
|
||||
*
|
||||
* @access private
|
||||
*/
|
||||
private function step2($backend, $code)
|
||||
{
|
||||
$profile = $this->authentication->backend($backend)->getProfile($code);
|
||||
|
||||
if ($this->userSession->isLogged()) {
|
||||
$this->link($backend, $profile);
|
||||
}
|
||||
|
||||
$this->authenticate($backend, $profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link the account
|
||||
*
|
||||
* @access private
|
||||
*/
|
||||
private function link($backend, $profile)
|
||||
{
|
||||
if (empty($profile)) {
|
||||
$this->session->flashError(t('External authentication failed'));
|
||||
}
|
||||
else {
|
||||
$this->session->flash(t('Your external account is linked to your profile successfully.'));
|
||||
$this->authentication->backend($backend)->updateUser($this->userSession->getId(), $profile);
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the account
|
||||
*
|
||||
* @access private
|
||||
*/
|
||||
private function authenticate($backend, $profile)
|
||||
{
|
||||
if (! empty($profile) && $this->authentication->backend($backend)->authenticate($profile['id'])) {
|
||||
$this->response->redirect($this->helper->url->to('app', 'index'));
|
||||
}
|
||||
else {
|
||||
$this->response->html($this->template->layout('auth/index', array(
|
||||
'errors' => array('login' => t('External authentication failed')),
|
||||
'values' => array(),
|
||||
'no_layout' => true,
|
||||
'title' => t('Login')
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
134
sources/app/Controller/Projectuser.php
Normal file
134
sources/app/Controller/Projectuser.php
Normal file
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\User as UserModel;
|
||||
use Model\Task as TaskModel;
|
||||
|
||||
/**
|
||||
* Project User overview
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Projectuser extends Base
|
||||
{
|
||||
/**
|
||||
* Common layout for users overview views
|
||||
*
|
||||
* @access private
|
||||
* @param string $template Template name
|
||||
* @param array $params Template parameters
|
||||
* @return string
|
||||
*/
|
||||
private function layout($template, array $params)
|
||||
{
|
||||
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
|
||||
$params['content_for_sublayout'] = $this->template->render($template, $params);
|
||||
$params['filter'] = array('user_id' => $params['user_id']);
|
||||
|
||||
return $this->template->layout('project_user/layout', $params);
|
||||
}
|
||||
|
||||
private function common()
|
||||
{
|
||||
$user_id = $this->request->getIntegerParam('user_id', UserModel::EVERYBODY_ID);
|
||||
|
||||
if ($this->userSession->isAdmin()) {
|
||||
$project_ids = $this->project->getAllIds();
|
||||
}
|
||||
else {
|
||||
$project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId());
|
||||
}
|
||||
|
||||
return array($user_id, $project_ids, $this->user->getList(true));
|
||||
}
|
||||
|
||||
private function role($is_owner, $action, $title, $title_user)
|
||||
{
|
||||
list($user_id, $project_ids, $users) = $this->common();
|
||||
|
||||
$query = $this->projectPermission->getQueryByRole($project_ids, $is_owner)->callback(array($this->project, 'applyColumnStats'));
|
||||
|
||||
if ($user_id !== UserModel::EVERYBODY_ID) {
|
||||
$query->eq(UserModel::TABLE.'.id', $user_id);
|
||||
$title = t($title_user, $users[$user_id]);
|
||||
}
|
||||
|
||||
$paginator = $this->paginator
|
||||
->setUrl('projectuser', $action, array('user_id' => $user_id))
|
||||
->setMax(30)
|
||||
->setOrder('projects.name')
|
||||
->setQuery($query)
|
||||
->calculate();
|
||||
|
||||
$this->response->html($this->layout('project_user/roles', array(
|
||||
'paginator' => $paginator,
|
||||
'title' => $title,
|
||||
'user_id' => $user_id,
|
||||
'users' => $users,
|
||||
)));
|
||||
}
|
||||
|
||||
private function tasks($is_active, $action, $title, $title_user)
|
||||
{
|
||||
list($user_id, $project_ids, $users) = $this->common();
|
||||
|
||||
$query = $this->taskFinder->getProjectUserOverviewQuery($project_ids, $is_active);
|
||||
|
||||
if ($user_id !== UserModel::EVERYBODY_ID) {
|
||||
$query->eq(TaskModel::TABLE.'.owner_id', $user_id);
|
||||
$title = t($title_user, $users[$user_id]);
|
||||
}
|
||||
|
||||
$paginator = $this->paginator
|
||||
->setUrl('projectuser', $action, array('user_id' => $user_id))
|
||||
->setMax(50)
|
||||
->setOrder(TaskModel::TABLE.'.id')
|
||||
->setQuery($query)
|
||||
->calculate();
|
||||
|
||||
$this->response->html($this->layout('project_user/tasks', array(
|
||||
'paginator' => $paginator,
|
||||
'title' => $title,
|
||||
'user_id' => $user_id,
|
||||
'users' => $users,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the list of project managers
|
||||
*
|
||||
*/
|
||||
public function managers()
|
||||
{
|
||||
$this->role(1, 'managers', t('People who are project managers'), 'Projects where "%s" is manager');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the list of project members
|
||||
*
|
||||
*/
|
||||
public function members()
|
||||
{
|
||||
$this->role(0, 'members', t('People who are project members'), 'Projects where "%s" is member');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the list of open taks
|
||||
*
|
||||
*/
|
||||
public function opens()
|
||||
{
|
||||
$this->tasks(TaskModel::STATUS_OPEN, 'opens', t('Open tasks'), 'Open tasks assigned to "%s"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the list of closed tasks
|
||||
*
|
||||
*/
|
||||
public function closed()
|
||||
{
|
||||
$this->tasks(TaskModel::STATUS_CLOSED, 'closed', t('Closed tasks'), 'Closed tasks assigned to "%s"');
|
||||
}
|
||||
}
|
51
sources/app/Controller/Search.php
Normal file
51
sources/app/Controller/Search.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
/**
|
||||
* Search controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Search extends Base
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$projects = $this->projectPermission->getAllowedProjects($this->userSession->getId());
|
||||
$search = urldecode($this->request->getStringParam('search'));
|
||||
$nb_tasks = 0;
|
||||
|
||||
$paginator = $this->paginator
|
||||
->setUrl('search', 'index', array('search' => $search))
|
||||
->setMax(30)
|
||||
->setOrder('tasks.id')
|
||||
->setDirection('DESC');
|
||||
|
||||
if ($search !== '') {
|
||||
|
||||
$query = $this
|
||||
->taskFilter
|
||||
->search($search)
|
||||
->filterByProjects(array_keys($projects))
|
||||
->getQuery();
|
||||
|
||||
$paginator
|
||||
->setQuery($query)
|
||||
->calculate();
|
||||
|
||||
$nb_tasks = $paginator->getTotal();
|
||||
}
|
||||
|
||||
$this->response->html($this->template->layout('search/index', array(
|
||||
'board_selector' => $projects,
|
||||
'values' => array(
|
||||
'search' => $search,
|
||||
'controller' => 'search',
|
||||
'action' => 'index',
|
||||
),
|
||||
'paginator' => $paginator,
|
||||
'title' => t('Search tasks').($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '')
|
||||
)));
|
||||
}
|
||||
}
|
83
sources/app/Controller/Taskcreation.php
Normal file
83
sources/app/Controller/Taskcreation.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
/**
|
||||
* Task Creation controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Taskcreation extends Base
|
||||
{
|
||||
/**
|
||||
* Display a form to create a new task
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function create(array $values = array(), array $errors = array())
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$method = $this->request->isAjax() ? 'render' : 'layout';
|
||||
$swimlanes_list = $this->swimlane->getList($project['id'], false, true);
|
||||
|
||||
if (empty($values)) {
|
||||
|
||||
$values = array(
|
||||
'swimlane_id' => $this->request->getIntegerParam('swimlane_id', key($swimlanes_list)),
|
||||
'column_id' => $this->request->getIntegerParam('column_id'),
|
||||
'color_id' => $this->request->getStringParam('color_id', $this->color->getDefaultColor()),
|
||||
'owner_id' => $this->request->getIntegerParam('owner_id'),
|
||||
'another_task' => $this->request->getIntegerParam('another_task'),
|
||||
);
|
||||
}
|
||||
|
||||
$this->response->html($this->template->$method('task_creation/form', array(
|
||||
'ajax' => $this->request->isAjax(),
|
||||
'errors' => $errors,
|
||||
'values' => $values + array('project_id' => $project['id']),
|
||||
'columns_list' => $this->board->getColumnsList($project['id']),
|
||||
'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true),
|
||||
'colors_list' => $this->color->getList(),
|
||||
'categories_list' => $this->category->getList($project['id']),
|
||||
'swimlanes_list' => $swimlanes_list,
|
||||
'date_format' => $this->config->get('application_date_format'),
|
||||
'date_formats' => $this->dateParser->getAvailableFormats(),
|
||||
'title' => $project['name'].' > '.t('New task')
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and save a new task
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$values = $this->request->getValues();
|
||||
|
||||
list($valid, $errors) = $this->taskValidator->validateCreation($values);
|
||||
|
||||
if ($valid) {
|
||||
|
||||
if ($this->taskCreation->create($values)) {
|
||||
$this->session->flash(t('Task created successfully.'));
|
||||
|
||||
if (isset($values['another_task']) && $values['another_task'] == 1) {
|
||||
unset($values['title']);
|
||||
unset($values['description']);
|
||||
$this->response->redirect($this->helper->url->to('taskcreation', 'create', $values));
|
||||
}
|
||||
else {
|
||||
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project['id'])));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->session->flashError(t('Unable to create your task.'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->create($values, $errors);
|
||||
}
|
||||
}
|
145
sources/app/Controller/Taskduplication.php
Normal file
145
sources/app/Controller/Taskduplication.php
Normal file
|
@ -0,0 +1,145 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
/**
|
||||
* Task Duplication controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Taskduplication extends Base
|
||||
{
|
||||
/**
|
||||
* Duplicate a task
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function duplicate()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
if ($this->request->getStringParam('confirmation') === 'yes') {
|
||||
|
||||
$this->checkCSRFParam();
|
||||
$task_id = $this->taskDuplication->duplicate($task['id']);
|
||||
|
||||
if ($task_id > 0) {
|
||||
$this->session->flash(t('Task created successfully.'));
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to create this task.'));
|
||||
$this->response->redirect($this->helper->url->to('taskduplication', 'duplicate', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
|
||||
}
|
||||
}
|
||||
|
||||
$this->response->html($this->taskLayout('task_duplication/duplicate', array(
|
||||
'task' => $task,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a task to another project
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function move()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
if ($this->request->isPost()) {
|
||||
|
||||
$values = $this->request->getValues();
|
||||
list($valid,) = $this->taskValidator->validateProjectModification($values);
|
||||
|
||||
if ($valid && $this->taskDuplication->moveToProject($task['id'],
|
||||
$values['project_id'],
|
||||
$values['swimlane_id'],
|
||||
$values['column_id'],
|
||||
$values['category_id'],
|
||||
$values['owner_id'])) {
|
||||
|
||||
$this->session->flash(t('Task updated successfully.'));
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $values['project_id'], 'task_id' => $task['id'])));
|
||||
}
|
||||
|
||||
$this->session->flashError(t('Unable to update your task.'));
|
||||
}
|
||||
|
||||
$this->chooseDestination($task, 'task_duplication/move');
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate a task to another project
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function copy()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
if ($this->request->isPost()) {
|
||||
|
||||
$values = $this->request->getValues();
|
||||
list($valid,) = $this->taskValidator->validateProjectModification($values);
|
||||
|
||||
if ($valid && $this->taskDuplication->duplicateToProject($task['id'],
|
||||
$values['project_id'],
|
||||
$values['swimlane_id'],
|
||||
$values['column_id'],
|
||||
$values['category_id'],
|
||||
$values['owner_id'])) {
|
||||
|
||||
$this->session->flash(t('Task created successfully.'));
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
|
||||
}
|
||||
|
||||
$this->session->flashError(t('Unable to create your task.'));
|
||||
}
|
||||
|
||||
$this->chooseDestination($task, 'task_duplication/copy');
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose destination when move/copy task to another project
|
||||
*
|
||||
* @access private
|
||||
* @param array $task
|
||||
* @param string $template
|
||||
*/
|
||||
private function chooseDestination(array $task, $template)
|
||||
{
|
||||
$values = array();
|
||||
$projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId());
|
||||
|
||||
unset($projects_list[$task['project_id']]);
|
||||
|
||||
if (! empty($projects_list)) {
|
||||
$dst_project_id = $this->request->getIntegerParam('dst_project_id', key($projects_list));
|
||||
|
||||
$swimlanes_list = $this->swimlane->getList($dst_project_id, false, true);
|
||||
$columns_list = $this->board->getColumnsList($dst_project_id);
|
||||
$categories_list = $this->category->getList($dst_project_id);
|
||||
$users_list = $this->projectPermission->getMemberList($dst_project_id);
|
||||
|
||||
$values = $this->taskDuplication->checkDestinationProjectValues($task);
|
||||
$values['project_id'] = $dst_project_id;
|
||||
}
|
||||
else {
|
||||
$swimlanes_list = array();
|
||||
$columns_list = array();
|
||||
$categories_list = array();
|
||||
$users_list = array();
|
||||
}
|
||||
|
||||
$this->response->html($this->taskLayout($template, array(
|
||||
'values' => $values,
|
||||
'task' => $task,
|
||||
'projects_list' => $projects_list,
|
||||
'swimlanes_list' => $swimlanes_list,
|
||||
'columns_list' => $columns_list,
|
||||
'categories_list' => $categories_list,
|
||||
'users_list' => $users_list,
|
||||
)));
|
||||
}
|
||||
}
|
212
sources/app/Controller/Taskmodification.php
Normal file
212
sources/app/Controller/Taskmodification.php
Normal file
|
@ -0,0 +1,212 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
/**
|
||||
* Task Modification controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Taskmodification extends Base
|
||||
{
|
||||
/**
|
||||
* Set automatically the start date
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function start()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$this->taskModification->update(array('id' => $task['id'], 'date_started' => time()));
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update time tracking information
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function time()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$values = $this->request->getValues();
|
||||
|
||||
list($valid,) = $this->taskValidator->validateTimeModification($values);
|
||||
|
||||
if ($valid && $this->taskModification->update($values)) {
|
||||
$this->session->flash(t('Task updated successfully.'));
|
||||
}
|
||||
else {
|
||||
$this->session->flashError(t('Unable to update your task.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit description form
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function description()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
|
||||
|
||||
if ($this->request->isPost()) {
|
||||
|
||||
$values = $this->request->getValues();
|
||||
|
||||
list($valid, $errors) = $this->taskValidator->validateDescriptionCreation($values);
|
||||
|
||||
if ($valid) {
|
||||
|
||||
if ($this->taskModification->update($values)) {
|
||||
$this->session->flash(t('Task updated successfully.'));
|
||||
}
|
||||
else {
|
||||
$this->session->flashError(t('Unable to update your task.'));
|
||||
}
|
||||
|
||||
if ($ajax) {
|
||||
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
|
||||
}
|
||||
else {
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$values = $task;
|
||||
$errors = array();
|
||||
}
|
||||
|
||||
$params = array(
|
||||
'values' => $values,
|
||||
'errors' => $errors,
|
||||
'task' => $task,
|
||||
'ajax' => $ajax,
|
||||
);
|
||||
|
||||
if ($ajax) {
|
||||
$this->response->html($this->template->render('task_modification/edit_description', $params));
|
||||
}
|
||||
else {
|
||||
$this->response->html($this->taskLayout('task_modification/edit_description', $params));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a form to edit a task
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function edit(array $values = array(), array $errors = array())
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$ajax = $this->request->isAjax();
|
||||
|
||||
if (empty($values)) {
|
||||
$values = $task;
|
||||
}
|
||||
|
||||
$this->dateParser->format($values, array('date_due'));
|
||||
|
||||
$params = array(
|
||||
'values' => $values,
|
||||
'errors' => $errors,
|
||||
'task' => $task,
|
||||
'users_list' => $this->projectPermission->getMemberList($task['project_id']),
|
||||
'colors_list' => $this->color->getList(),
|
||||
'categories_list' => $this->category->getList($task['project_id']),
|
||||
'date_format' => $this->config->get('application_date_format'),
|
||||
'date_formats' => $this->dateParser->getAvailableFormats(),
|
||||
'ajax' => $ajax,
|
||||
);
|
||||
|
||||
if ($ajax) {
|
||||
$this->response->html($this->template->render('task_modification/edit_task', $params));
|
||||
}
|
||||
else {
|
||||
$this->response->html($this->taskLayout('task_modification/edit_task', $params));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and update a task
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$values = $this->request->getValues();
|
||||
|
||||
list($valid, $errors) = $this->taskValidator->validateModification($values);
|
||||
|
||||
if ($valid) {
|
||||
|
||||
if ($this->taskModification->update($values)) {
|
||||
$this->session->flash(t('Task updated successfully.'));
|
||||
|
||||
if ($this->request->getIntegerParam('ajax')) {
|
||||
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
|
||||
}
|
||||
else {
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->session->flashError(t('Unable to update your task.'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->edit($values, $errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit recurrence form
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function recurrence()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
if ($this->request->isPost()) {
|
||||
|
||||
$values = $this->request->getValues();
|
||||
|
||||
list($valid, $errors) = $this->taskValidator->validateEditRecurrence($values);
|
||||
|
||||
if ($valid) {
|
||||
|
||||
if ($this->taskModification->update($values)) {
|
||||
$this->session->flash(t('Task updated successfully.'));
|
||||
}
|
||||
else {
|
||||
$this->session->flashError(t('Unable to update your task.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$values = $task;
|
||||
$errors = array();
|
||||
}
|
||||
|
||||
$params = array(
|
||||
'values' => $values,
|
||||
'errors' => $errors,
|
||||
'task' => $task,
|
||||
'recurrence_status_list' => $this->task->getRecurrenceStatusList(),
|
||||
'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(),
|
||||
'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(),
|
||||
'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(),
|
||||
);
|
||||
|
||||
$this->response->html($this->taskLayout('task_modification/edit_recurrence', $params));
|
||||
}
|
||||
}
|
79
sources/app/Controller/Taskstatus.php
Normal file
79
sources/app/Controller/Taskstatus.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
/**
|
||||
* Task Status controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Taskstatus extends Base
|
||||
{
|
||||
/**
|
||||
* Close a task
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$redirect = $this->request->getStringParam('redirect');
|
||||
|
||||
if ($this->request->getStringParam('confirmation') === 'yes') {
|
||||
|
||||
$this->checkCSRFParam();
|
||||
|
||||
if ($this->taskStatus->close($task['id'])) {
|
||||
$this->session->flash(t('Task closed successfully.'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to close this task.'));
|
||||
}
|
||||
|
||||
if ($redirect === 'board') {
|
||||
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
|
||||
}
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$this->response->html($this->template->render('task_status/close', array(
|
||||
'task' => $task,
|
||||
'redirect' => $redirect,
|
||||
)));
|
||||
}
|
||||
|
||||
$this->response->html($this->taskLayout('task_status/close', array(
|
||||
'task' => $task,
|
||||
'redirect' => $redirect,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a task
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function open()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
if ($this->request->getStringParam('confirmation') === 'yes') {
|
||||
|
||||
$this->checkCSRFParam();
|
||||
|
||||
if ($this->taskStatus->open($task['id'])) {
|
||||
$this->session->flash(t('Task opened successfully.'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to open this task.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
|
||||
}
|
||||
|
||||
$this->response->html($this->taskLayout('task_status/open', array(
|
||||
'task' => $task,
|
||||
)));
|
||||
}
|
||||
}
|
35
sources/app/Controller/Timer.php
Normal file
35
sources/app/Controller/Timer.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace Controller;
|
||||
|
||||
/**
|
||||
* Time Tracking controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Timer extends Base
|
||||
{
|
||||
/**
|
||||
* Start/stop timer for subtasks
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function subtask()
|
||||
{
|
||||
$project_id = $this->request->getIntegerParam('project_id');
|
||||
$task_id = $this->request->getIntegerParam('task_id');
|
||||
$subtask_id = $this->request->getIntegerParam('subtask_id');
|
||||
$timer = $this->request->getStringParam('timer');
|
||||
|
||||
if ($timer === 'start') {
|
||||
$this->subtaskTimeTracking->logStartTime($subtask_id, $this->userSession->getId());
|
||||
}
|
||||
else if ($timer === 'stop') {
|
||||
$this->subtaskTimeTracking->logEndTime($subtask_id, $this->userSession->getId());
|
||||
$this->subtaskTimeTracking->updateTaskTimeTracking($task_id);
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $project_id, 'task_id' => $task_id)).'#subtasks');
|
||||
}
|
||||
}
|
118
sources/app/Core/Base.php
Normal file
118
sources/app/Core/Base.php
Normal file
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace Core;
|
||||
|
||||
use Pimple\Container;
|
||||
|
||||
/**
|
||||
* Base class
|
||||
*
|
||||
* @package core
|
||||
* @author Frederic Guillot
|
||||
*
|
||||
* @property \Core\Helper $helper
|
||||
* @property \Core\EmailClient $emailClient
|
||||
* @property \Core\HttpClient $httpClient
|
||||
* @property \Core\Paginator $paginator
|
||||
* @property \Core\Request $request
|
||||
* @property \Core\Session $session
|
||||
* @property \Core\Template $template
|
||||
* @property \Core\MemoryCache $memoryCache
|
||||
* @property \Core\OAuth2 $oauth
|
||||
* @property \Core\Router $router
|
||||
* @property \Core\Lexer $lexer
|
||||
* @property \Integration\BitbucketWebhook $bitbucketWebhook
|
||||
* @property \Integration\GithubWebhook $githubWebhook
|
||||
* @property \Integration\GitlabWebhook $gitlabWebhook
|
||||
* @property \Integration\HipchatWebhook $hipchatWebhook
|
||||
* @property \Integration\Jabber $jabber
|
||||
* @property \Integration\Mailgun $mailgun
|
||||
* @property \Integration\Postmark $postmark
|
||||
* @property \Integration\Sendgrid $sendgrid
|
||||
* @property \Integration\SlackWebhook $slackWebhook
|
||||
* @property \Integration\Smtp $smtp
|
||||
* @property \Model\Acl $acl
|
||||
* @property \Model\Action $action
|
||||
* @property \Model\Authentication $authentication
|
||||
* @property \Model\Board $board
|
||||
* @property \Model\Budget $budget
|
||||
* @property \Model\Category $category
|
||||
* @property \Model\Color $color
|
||||
* @property \Model\Comment $comment
|
||||
* @property \Model\Config $config
|
||||
* @property \Model\Currency $currency
|
||||
* @property \Model\DateParser $dateParser
|
||||
* @property \Model\File $file
|
||||
* @property \Model\HourlyRate $hourlyRate
|
||||
* @property \Model\LastLogin $lastLogin
|
||||
* @property \Model\Link $link
|
||||
* @property \Model\Notification $notification
|
||||
* @property \Model\Project $project
|
||||
* @property \Model\ProjectActivity $projectActivity
|
||||
* @property \Model\ProjectAnalytic $projectAnalytic
|
||||
* @property \Model\ProjectDuplication $projectDuplication
|
||||
* @property \Model\ProjectDailyColumnStats $projectDailyColumnStats
|
||||
* @property \Model\ProjectDailyStats $projectDailyStats
|
||||
* @property \Model\ProjectIntegration $projectIntegration
|
||||
* @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
|
||||
* @property \Model\TaskAnalytic $taskAnalytic
|
||||
* @property \Model\TaskCreation $taskCreation
|
||||
* @property \Model\TaskDuplication $taskDuplication
|
||||
* @property \Model\TaskExport $taskExport
|
||||
* @property \Model\TaskFinder $taskFinder
|
||||
* @property \Model\TaskFilter $taskFilter
|
||||
* @property \Model\TaskLink $taskLink
|
||||
* @property \Model\TaskModification $taskModification
|
||||
* @property \Model\TaskPermission $taskPermission
|
||||
* @property \Model\TaskPosition $taskPosition
|
||||
* @property \Model\TaskStatus $taskStatus
|
||||
* @property \Model\TaskValidator $taskValidator
|
||||
* @property \Model\Timetable $timetable
|
||||
* @property \Model\TimetableDay $timetableDay
|
||||
* @property \Model\TimetableExtra $timetableExtra
|
||||
* @property \Model\TimetableOff $timetableOff
|
||||
* @property \Model\TimetableWeek $timetableWeek
|
||||
* @property \Model\Transition $transition
|
||||
* @property \Model\User $user
|
||||
* @property \Model\UserSession $userSession
|
||||
* @property \Model\Webhook $webhook
|
||||
*/
|
||||
abstract class Base
|
||||
{
|
||||
/**
|
||||
* Container instance
|
||||
*
|
||||
* @access protected
|
||||
* @var \Pimple\Container
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param \Pimple\Container $container
|
||||
*/
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load automatically models
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Model name
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($name)
|
||||
{
|
||||
return $this->container[$name];
|
||||
}
|
||||
}
|
49
sources/app/Core/EmailClient.php
Normal file
49
sources/app/Core/EmailClient.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace Core;
|
||||
|
||||
/**
|
||||
* Mail client
|
||||
*
|
||||
* @package core
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class EmailClient extends Base
|
||||
{
|
||||
/**
|
||||
* Send a HTML email
|
||||
*
|
||||
* @access public
|
||||
* @param string $email
|
||||
* @param string $name
|
||||
* @param string $subject
|
||||
* @param string $html
|
||||
*/
|
||||
public function send($email, $name, $subject, $html)
|
||||
{
|
||||
$this->container['logger']->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')');
|
||||
|
||||
$start_time = microtime(true);
|
||||
$author = 'Kanboard';
|
||||
|
||||
if (Session::isOpen() && $this->userSession->isLogged()) {
|
||||
$author = e('%s via Kanboard', $this->user->getFullname($this->session['user']));
|
||||
}
|
||||
|
||||
switch (MAIL_TRANSPORT) {
|
||||
case 'sendgrid':
|
||||
$this->sendgrid->sendEmail($email, $name, $subject, $html, $author);
|
||||
break;
|
||||
case 'mailgun':
|
||||
$this->mailgun->sendEmail($email, $name, $subject, $html, $author);
|
||||
break;
|
||||
case 'postmark':
|
||||
$this->postmark->sendEmail($email, $name, $subject, $html, $author);
|
||||
break;
|
||||
default:
|
||||
$this->smtp->sendEmail($email, $name, $subject, $html, $author);
|
||||
}
|
||||
|
||||
$this->container['logger']->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds');
|
||||
}
|
||||
}
|
162
sources/app/Core/Lexer.php
Normal file
162
sources/app/Core/Lexer.php
Normal file
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
namespace Core;
|
||||
|
||||
/**
|
||||
* Lexer
|
||||
*
|
||||
* @package core
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Lexer
|
||||
{
|
||||
/**
|
||||
* Current position
|
||||
*
|
||||
* @access private
|
||||
* @var integer
|
||||
*/
|
||||
private $offset = 0;
|
||||
|
||||
/**
|
||||
* Token map
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $tokenMap = array(
|
||||
"/^(assignee:)/" => 'T_ASSIGNEE',
|
||||
"/^(color:)/" => 'T_COLOR',
|
||||
"/^(due:)/" => 'T_DUE',
|
||||
"/^(updated:)/" => 'T_UPDATED',
|
||||
"/^(modified:)/" => 'T_UPDATED',
|
||||
"/^(created:)/" => 'T_CREATED',
|
||||
"/^(status:)/" => 'T_STATUS',
|
||||
"/^(description:)/" => 'T_DESCRIPTION',
|
||||
"/^(category:)/" => 'T_CATEGORY',
|
||||
"/^(column:)/" => 'T_COLUMN',
|
||||
"/^(project:)/" => 'T_PROJECT',
|
||||
"/^(swimlane:)/" => 'T_SWIMLANE',
|
||||
"/^(ref:)/" => 'T_REFERENCE',
|
||||
"/^(reference:)/" => 'T_REFERENCE',
|
||||
"/^(\s+)/" => 'T_WHITESPACE',
|
||||
'/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE',
|
||||
'/^(yesterday|tomorrow|today)/' => 'T_DATE',
|
||||
'/^("(.*?)")/' => 'T_STRING',
|
||||
"/^(\w+)/" => 'T_STRING',
|
||||
"/^(#\d+)/" => 'T_STRING',
|
||||
);
|
||||
|
||||
/**
|
||||
* Tokenize input string
|
||||
*
|
||||
* @access public
|
||||
* @param string $input
|
||||
* @return array
|
||||
*/
|
||||
public function tokenize($input)
|
||||
{
|
||||
$tokens = array();
|
||||
$this->offset = 0;
|
||||
|
||||
while (isset($input[$this->offset])) {
|
||||
|
||||
$result = $this->match(substr($input, $this->offset));
|
||||
|
||||
if ($result === false) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$tokens[] = $result;
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a token that match and move the offset
|
||||
*
|
||||
* @access public
|
||||
* @param string $string
|
||||
* @return array|boolean
|
||||
*/
|
||||
public function match($string)
|
||||
{
|
||||
foreach ($this->tokenMap as $pattern => $name) {
|
||||
if (preg_match($pattern, $string, $matches)) {
|
||||
|
||||
$this->offset += strlen($matches[1]);
|
||||
|
||||
return array(
|
||||
'match' => trim($matches[1], '"'),
|
||||
'token' => $name,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the output of tokenizer to be easily parsed by the database filter
|
||||
*
|
||||
* Example: ['T_ASSIGNEE' => ['user1', 'user2'], 'T_TITLE' => 'task title']
|
||||
*
|
||||
* @access public
|
||||
* @param array $tokens
|
||||
* @return array
|
||||
*/
|
||||
public function map(array $tokens)
|
||||
{
|
||||
$map = array(
|
||||
'T_TITLE' => '',
|
||||
);
|
||||
|
||||
while (false !== ($token = current($tokens))) {
|
||||
|
||||
switch ($token['token']) {
|
||||
case 'T_ASSIGNEE':
|
||||
case 'T_COLOR':
|
||||
case 'T_CATEGORY':
|
||||
case 'T_COLUMN':
|
||||
case 'T_PROJECT':
|
||||
case 'T_SWIMLANE':
|
||||
$next = next($tokens);
|
||||
|
||||
if ($next !== false && $next['token'] === 'T_STRING') {
|
||||
$map[$token['token']][] = $next['match'];
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'T_STATUS':
|
||||
case 'T_DUE':
|
||||
case 'T_UPDATED':
|
||||
case 'T_CREATED':
|
||||
case 'T_DESCRIPTION':
|
||||
case 'T_REFERENCE':
|
||||
$next = next($tokens);
|
||||
|
||||
if ($next !== false && ($next['token'] === 'T_DATE' || $next['token'] === 'T_STRING')) {
|
||||
$map[$token['token']] = $next['match'];
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$map['T_TITLE'] .= $token['match'];
|
||||
break;
|
||||
}
|
||||
|
||||
next($tokens);
|
||||
}
|
||||
|
||||
$map['T_TITLE'] = trim($map['T_TITLE']);
|
||||
|
||||
if (empty($map['T_TITLE'])) {
|
||||
unset($map['T_TITLE']);
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
120
sources/app/Core/OAuth2.php
Normal file
120
sources/app/Core/OAuth2.php
Normal file
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
namespace Core;
|
||||
|
||||
/**
|
||||
* OAuth2 client
|
||||
*
|
||||
* @package core
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class OAuth2 extends Base
|
||||
{
|
||||
private $clientId;
|
||||
private $secret;
|
||||
private $callbackUrl;
|
||||
private $authUrl;
|
||||
private $tokenUrl;
|
||||
private $scopes;
|
||||
private $tokenType;
|
||||
private $accessToken;
|
||||
|
||||
/**
|
||||
* Create OAuth2 service
|
||||
*
|
||||
* @access public
|
||||
* @param string $clientId
|
||||
* @param string $secret
|
||||
* @param string $callbackUrl
|
||||
* @param string $authUrl
|
||||
* @param string $tokenUrl
|
||||
* @param array $scopes
|
||||
* @return OAuth2
|
||||
*/
|
||||
public function createService($clientId, $secret, $callbackUrl, $authUrl, $tokenUrl, array $scopes)
|
||||
{
|
||||
$this->clientId = $clientId;
|
||||
$this->secret = $secret;
|
||||
$this->callbackUrl = $callbackUrl;
|
||||
$this->authUrl = $authUrl;
|
||||
$this->tokenUrl = $tokenUrl;
|
||||
$this->scopes = $scopes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization url
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthorizationUrl()
|
||||
{
|
||||
$params = array(
|
||||
'response_type' => 'code',
|
||||
'client_id' => $this->clientId,
|
||||
'redirect_uri' => $this->callbackUrl,
|
||||
'scope' => implode(' ', $this->scopes),
|
||||
);
|
||||
|
||||
return $this->authUrl.'?'.http_build_query($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization header
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthorizationHeader()
|
||||
{
|
||||
if (strtolower($this->tokenType) === 'bearer') {
|
||||
return 'Authorization: Bearer '.$this->accessToken;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access token
|
||||
*
|
||||
* @access public
|
||||
* @param string $code
|
||||
* @return string
|
||||
*/
|
||||
public function getAccessToken($code)
|
||||
{
|
||||
if (empty($this->accessToken) && ! empty($code)) {
|
||||
|
||||
$params = array(
|
||||
'code' => $code,
|
||||
'client_id' => $this->clientId,
|
||||
'client_secret' => $this->secret,
|
||||
'redirect_uri' => $this->callbackUrl,
|
||||
'grant_type' => 'authorization_code',
|
||||
);
|
||||
|
||||
$response = json_decode($this->httpClient->postForm($this->tokenUrl, $params, array('Accept: application/json')), true);
|
||||
|
||||
$this->tokenType = isset($response['token_type']) ? $response['token_type'] : '';
|
||||
$this->accessToken = isset($response['access_token']) ? $response['access_token'] : '';
|
||||
}
|
||||
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set access token
|
||||
*
|
||||
* @access public
|
||||
* @param string $token
|
||||
* @param string $type
|
||||
* @return string
|
||||
*/
|
||||
public function setAccessToken($token, $type = 'bearer')
|
||||
{
|
||||
$this->accessToken = $token;
|
||||
$this->tokenType = $type;
|
||||
}
|
||||
}
|
7
sources/app/Event/TaskLinkEvent.php
Normal file
7
sources/app/Event/TaskLinkEvent.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Event;
|
||||
|
||||
class TaskLinkEvent extends GenericEvent
|
||||
{
|
||||
}
|
78
sources/app/Helper/App.php
Normal file
78
sources/app/Helper/App.php
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
namespace Helper;
|
||||
|
||||
/**
|
||||
* Application helpers
|
||||
*
|
||||
* @package helper
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class App extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Get router controller
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getRouterController()
|
||||
{
|
||||
return $this->router->getController();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get router action
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getRouterAction()
|
||||
{
|
||||
return $this->router->getAction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get javascript language code
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function jsLang()
|
||||
{
|
||||
return $this->config->getJsLanguageCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current timezone
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getTimezone()
|
||||
{
|
||||
return $this->config->getCurrentTimezone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session flash message
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function flashMessage()
|
||||
{
|
||||
$html = '';
|
||||
|
||||
if (isset($this->session['flash_message'])) {
|
||||
$html = '<div class="alert alert-success alert-fade-out">'.$this->helper->e($this->session['flash_message']).'</div>';
|
||||
unset($this->session['flash_message']);
|
||||
}
|
||||
else if (isset($this->session['flash_error_message'])) {
|
||||
$html = '<div class="alert alert-error">'.$this->helper->e($this->session['flash_error_message']).'</div>';
|
||||
unset($this->session['flash_error_message']);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
62
sources/app/Helper/Asset.php
Normal file
62
sources/app/Helper/Asset.php
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace Helper;
|
||||
|
||||
/**
|
||||
* Assets helpers
|
||||
*
|
||||
* @package helper
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Asset extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Add a Javascript asset
|
||||
*
|
||||
* @param string $filename Filename
|
||||
* @return string
|
||||
*/
|
||||
public function js($filename, $async = false)
|
||||
{
|
||||
return '<script '.($async ? 'async' : '').' type="text/javascript" src="'.$this->helper->url->dir().$filename.'?'.filemtime($filename).'"></script>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a stylesheet asset
|
||||
*
|
||||
* @param string $filename Filename
|
||||
* @param boolean $is_file Add file timestamp
|
||||
* @param string $media Media
|
||||
* @return string
|
||||
*/
|
||||
public function css($filename, $is_file = true, $media = 'screen')
|
||||
{
|
||||
return '<link rel="stylesheet" href="'.$this->helper->url->dir().$filename.($is_file ? '?'.filemtime($filename) : '').'" media="'.$media.'">';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom css
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function customCss()
|
||||
{
|
||||
if ($this->config->get('application_stylesheet')) {
|
||||
return '<style>'.$this->config->get('application_stylesheet').'</style>';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CSS for task colors
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function colorCss()
|
||||
{
|
||||
return '<style>'.$this->color->getCss().'</style>';
|
||||
}
|
||||
}
|
24
sources/app/Helper/Board.php
Normal file
24
sources/app/Helper/Board.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Helper;
|
||||
|
||||
/**
|
||||
* Board Helper
|
||||
*
|
||||
* @package helper
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Board extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Return true if tasks are collapsed
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @return boolean
|
||||
*/
|
||||
public function isCollapsed($project_id)
|
||||
{
|
||||
return $this->userSession->isBoardCollapsed($project_id);
|
||||
}
|
||||
}
|
114
sources/app/Helper/Dt.php
Normal file
114
sources/app/Helper/Dt.php
Normal file
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace Helper;
|
||||
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* DateTime helpers
|
||||
*
|
||||
* @package helper
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Dt extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Get duration in seconds into human format
|
||||
*
|
||||
* @access public
|
||||
* @param integer $seconds
|
||||
* @return string
|
||||
*/
|
||||
public function duration($seconds)
|
||||
{
|
||||
if ($seconds == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$dtF = new DateTime("@0");
|
||||
$dtT = new DateTime("@$seconds");
|
||||
return $dtF->diff($dtT)->format('%a days, %h hours, %i minutes and %s seconds');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the age of an item in quasi human readable format.
|
||||
* It's in this format: <1h , NNh, NNd
|
||||
*
|
||||
* @access public
|
||||
* @param integer $timestamp Unix timestamp of the artifact for which age will be calculated
|
||||
* @param integer $now Compare with this timestamp (Default value is the current unix timestamp)
|
||||
* @return string
|
||||
*/
|
||||
public function age($timestamp, $now = null)
|
||||
{
|
||||
if ($now === null) {
|
||||
$now = time();
|
||||
}
|
||||
|
||||
$diff = $now - $timestamp;
|
||||
|
||||
if ($diff < 900) {
|
||||
return t('<15m');
|
||||
}
|
||||
if ($diff < 1200) {
|
||||
return t('<30m');
|
||||
}
|
||||
else if ($diff < 3600) {
|
||||
return t('<1h');
|
||||
}
|
||||
else if ($diff < 86400) {
|
||||
return '~'.t('%dh', $diff / 3600);
|
||||
}
|
||||
|
||||
return t('%dd', ($now - $timestamp) / 86400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all hours for day
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getDayHours()
|
||||
{
|
||||
$values = array();
|
||||
|
||||
foreach (range(0, 23) as $hour) {
|
||||
foreach (array(0, 30) as $minute) {
|
||||
$time = sprintf('%02d:%02d', $hour, $minute);
|
||||
$values[$time] = $time;
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all days of a week
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getWeekDays()
|
||||
{
|
||||
$values = array();
|
||||
|
||||
foreach (range(1, 7) as $day) {
|
||||
$values[$day] = $this->getWeekDay($day);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the localized day name from the day number
|
||||
*
|
||||
* @access public
|
||||
* @param integer $day Day number
|
||||
* @return string
|
||||
*/
|
||||
public function getWeekDay($day)
|
||||
{
|
||||
return dt('%A', strtotime('next Monday +'.($day - 1).' days'));
|
||||
}
|
||||
}
|
56
sources/app/Helper/File.php
Normal file
56
sources/app/Helper/File.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace Helper;
|
||||
|
||||
/**
|
||||
* File helpers
|
||||
*
|
||||
* @package helper
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class File extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Get file icon
|
||||
*
|
||||
* @access public
|
||||
* @param string $filename Filename
|
||||
* @return string Font-Awesome-Icon-Name
|
||||
*/
|
||||
public function icon($filename){
|
||||
|
||||
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
|
||||
switch ($extension) {
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
case 'png':
|
||||
case 'gif':
|
||||
return 'fa-file-image-o';
|
||||
case 'xls':
|
||||
case 'xlsx':
|
||||
return 'fa-file-excel-o';
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return 'fa-file-word-o';
|
||||
case 'ppt':
|
||||
case 'pptx':
|
||||
return 'fa-file-powerpoint-o';
|
||||
case 'zip':
|
||||
case 'rar':
|
||||
return 'fa-file-archive-o';
|
||||
case 'mp3':
|
||||
return 'fa-audio-o';
|
||||
case 'avi':
|
||||
return 'fa-video-o';
|
||||
case 'php':
|
||||
case 'html':
|
||||
case 'css':
|
||||
return 'fa-code-o';
|
||||
case 'pdf':
|
||||
return 'fa-file-pdf-o';
|
||||
}
|
||||
|
||||
return 'fa-file-o';
|
||||
}
|
||||
}
|
323
sources/app/Helper/Form.php
Normal file
323
sources/app/Helper/Form.php
Normal file
|
@ -0,0 +1,323 @@
|
|||
<?php
|
||||
|
||||
namespace Helper;
|
||||
|
||||
use Core\Security;
|
||||
|
||||
/**
|
||||
* Form helpers
|
||||
*
|
||||
* @package helper
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Form extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Hidden CSRF token field
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function csrf()
|
||||
{
|
||||
return '<input type="hidden" name="csrf_token" value="'.Security::getCSRFToken().'"/>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a hidden form field
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Field name
|
||||
* @param array $values Form values
|
||||
* @return string
|
||||
*/
|
||||
public function hidden($name, array $values = array())
|
||||
{
|
||||
return '<input type="hidden" name="'.$name.'" id="form-'.$name.'" '.$this->formValue($values, $name).'/>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a select field
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Field name
|
||||
* @param array $options Options
|
||||
* @param array $values Form values
|
||||
* @param array $errors Form errors
|
||||
* @param string $class CSS class
|
||||
* @return string
|
||||
*/
|
||||
public function select($name, array $options, array $values = array(), array $errors = array(), array $attributes = array(), $class = '')
|
||||
{
|
||||
$html = '<select name="'.$name.'" id="form-'.$name.'" class="'.$class.'" '.implode(' ', $attributes).'>';
|
||||
|
||||
foreach ($options as $id => $value) {
|
||||
|
||||
$html .= '<option value="'.$this->helper->e($id).'"';
|
||||
|
||||
if (isset($values->$name) && $id == $values->$name) $html .= ' selected="selected"';
|
||||
if (isset($values[$name]) && $id == $values[$name]) $html .= ' selected="selected"';
|
||||
|
||||
$html .= '>'.$this->helper->e($value).'</option>';
|
||||
}
|
||||
|
||||
$html .= '</select>';
|
||||
$html .= $this->errorList($errors, $name);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a radio field group
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Field name
|
||||
* @param array $options Options
|
||||
* @param array $values Form values
|
||||
* @return string
|
||||
*/
|
||||
public function radios($name, array $options, array $values = array())
|
||||
{
|
||||
$html = '';
|
||||
|
||||
foreach ($options as $value => $label) {
|
||||
$html .= $this->radio($name, $label, $value, isset($values[$name]) && $values[$name] == $value);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a radio field
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Field name
|
||||
* @param string $label Form label
|
||||
* @param string $value Form value
|
||||
* @param boolean $selected Field selected or not
|
||||
* @param string $class CSS class
|
||||
* @return string
|
||||
*/
|
||||
public function radio($name, $label, $value, $selected = false, $class = '')
|
||||
{
|
||||
return '<label><input type="radio" name="'.$name.'" class="'.$class.'" value="'.$this->helper->e($value).'" '.($selected ? 'checked="checked"' : '').'> '.$this->helper->e($label).'</label>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a checkbox field
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Field name
|
||||
* @param string $label Form label
|
||||
* @param string $value Form value
|
||||
* @param boolean $checked Field selected or not
|
||||
* @param string $class CSS class
|
||||
* @return string
|
||||
*/
|
||||
public function checkbox($name, $label, $value, $checked = false, $class = '')
|
||||
{
|
||||
return '<label><input type="checkbox" name="'.$name.'" class="'.$class.'" value="'.$this->helper->e($value).'" '.($checked ? 'checked="checked"' : '').'> '.$this->helper->e($label).'</label>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a form label
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Field name
|
||||
* @param string $label Form label
|
||||
* @param array $attributes HTML attributes
|
||||
* @return string
|
||||
*/
|
||||
public function label($label, $name, array $attributes = array())
|
||||
{
|
||||
return '<label for="form-'.$name.'" '.implode(' ', $attributes).'>'.$this->helper->e($label).'</label>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a textarea
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Field name
|
||||
* @param array $values Form values
|
||||
* @param array $errors Form errors
|
||||
* @param array $attributes HTML attributes
|
||||
* @param string $class CSS class
|
||||
* @return string
|
||||
*/
|
||||
public function textarea($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
|
||||
{
|
||||
$class .= $this->errorClass($errors, $name);
|
||||
|
||||
$html = '<textarea name="'.$name.'" id="form-'.$name.'" class="'.$class.'" ';
|
||||
$html .= implode(' ', $attributes).'>';
|
||||
$html .= isset($values->$name) ? $this->helper->e($values->$name) : isset($values[$name]) ? $values[$name] : '';
|
||||
$html .= '</textarea>';
|
||||
$html .= $this->errorList($errors, $name);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a input field
|
||||
*
|
||||
* @access public
|
||||
* @param string $type HMTL input tag type
|
||||
* @param string $name Field name
|
||||
* @param array $values Form values
|
||||
* @param array $errors Form errors
|
||||
* @param array $attributes HTML attributes
|
||||
* @param string $class CSS class
|
||||
* @return string
|
||||
*/
|
||||
public function input($type, $name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
|
||||
{
|
||||
$class .= $this->errorClass($errors, $name);
|
||||
|
||||
$html = '<input type="'.$type.'" name="'.$name.'" id="form-'.$name.'" '.$this->formValue($values, $name).' class="'.$class.'" ';
|
||||
$html .= implode(' ', $attributes).'>';
|
||||
|
||||
if (in_array('required', $attributes)) {
|
||||
$html .= '<span class="form-required">*</span>';
|
||||
}
|
||||
|
||||
$html .= $this->errorList($errors, $name);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a text field
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Field name
|
||||
* @param array $values Form values
|
||||
* @param array $errors Form errors
|
||||
* @param array $attributes HTML attributes
|
||||
* @param string $class CSS class
|
||||
* @return string
|
||||
*/
|
||||
public function text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
|
||||
{
|
||||
return $this->input('text', $name, $values, $errors, $attributes, $class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a password field
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Field name
|
||||
* @param array $values Form values
|
||||
* @param array $errors Form errors
|
||||
* @param array $attributes HTML attributes
|
||||
* @param string $class CSS class
|
||||
* @return string
|
||||
*/
|
||||
public function password($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
|
||||
{
|
||||
return $this->input('password', $name, $values, $errors, $attributes, $class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an email field
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Field name
|
||||
* @param array $values Form values
|
||||
* @param array $errors Form errors
|
||||
* @param array $attributes HTML attributes
|
||||
* @param string $class CSS class
|
||||
* @return string
|
||||
*/
|
||||
public function email($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
|
||||
{
|
||||
return $this->input('email', $name, $values, $errors, $attributes, $class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a number field
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Field name
|
||||
* @param array $values Form values
|
||||
* @param array $errors Form errors
|
||||
* @param array $attributes HTML attributes
|
||||
* @param string $class CSS class
|
||||
* @return string
|
||||
*/
|
||||
public function number($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
|
||||
{
|
||||
return $this->input('number', $name, $values, $errors, $attributes, $class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a numeric field (allow decimal number)
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Field name
|
||||
* @param array $values Form values
|
||||
* @param array $errors Form errors
|
||||
* @param array $attributes HTML attributes
|
||||
* @param string $class CSS class
|
||||
* @return string
|
||||
*/
|
||||
public function numeric($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
|
||||
{
|
||||
return $this->input('text', $name, $values, $errors, $attributes, $class.' form-numeric');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the form error class
|
||||
*
|
||||
* @access private
|
||||
* @param array $errors Error list
|
||||
* @param string $name Field name
|
||||
* @return string
|
||||
*/
|
||||
private function errorClass(array $errors, $name)
|
||||
{
|
||||
return ! isset($errors[$name]) ? '' : ' form-error';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a list of form errors
|
||||
*
|
||||
* @access private
|
||||
* @param array $errors List of errors
|
||||
* @param string $name Field name
|
||||
* @return string
|
||||
*/
|
||||
private function errorList(array $errors, $name)
|
||||
{
|
||||
$html = '';
|
||||
|
||||
if (isset($errors[$name])) {
|
||||
|
||||
$html .= '<ul class="form-errors">';
|
||||
|
||||
foreach ($errors[$name] as $error) {
|
||||
$html .= '<li>'.$this->helper->e($error).'</li>';
|
||||
}
|
||||
|
||||
$html .= '</ul>';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an escaped form value
|
||||
*
|
||||
* @access private
|
||||
* @param mixed $values Values
|
||||
* @param string $name Field name
|
||||
* @return string
|
||||
*/
|
||||
private function formValue($values, $name)
|
||||
{
|
||||
if (isset($values->$name)) {
|
||||
return 'value="'.$this->helper->e($values->$name).'"';
|
||||
}
|
||||
|
||||
return isset($values[$name]) ? 'value="'.$this->helper->e($values[$name]).'"' : '';
|
||||
}
|
||||
}
|
42
sources/app/Helper/Subtask.php
Normal file
42
sources/app/Helper/Subtask.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace Helper;
|
||||
|
||||
/**
|
||||
* Subtask helpers
|
||||
*
|
||||
* @package helper
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Subtask extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Get the link to toggle subtask status
|
||||
*
|
||||
* @access public
|
||||
* @param array $subtask
|
||||
* @param string $redirect
|
||||
* @return string
|
||||
*/
|
||||
public function toggleStatus(array $subtask, $redirect)
|
||||
{
|
||||
if ($subtask['status'] == 0 && isset($this->session['has_subtask_inprogress']) && $this->session['has_subtask_inprogress'] === true) {
|
||||
|
||||
return $this->helper->url->link(
|
||||
trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']),
|
||||
'subtask',
|
||||
'subtaskRestriction',
|
||||
array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'redirect' => $redirect),
|
||||
false,
|
||||
'popover task-board-popover'
|
||||
);
|
||||
}
|
||||
|
||||
return $this->helper->url->link(
|
||||
trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']),
|
||||
'subtask',
|
||||
'toggleStatus',
|
||||
array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'redirect' => $redirect)
|
||||
);
|
||||
}
|
||||
}
|
37
sources/app/Helper/Task.php
Normal file
37
sources/app/Helper/Task.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace Helper;
|
||||
|
||||
/**
|
||||
* Task helpers
|
||||
*
|
||||
* @package helper
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Task extends \Core\Base
|
||||
{
|
||||
public function getColors()
|
||||
{
|
||||
return $this->color->getList();
|
||||
}
|
||||
|
||||
public function recurrenceTriggers()
|
||||
{
|
||||
return $this->task->getRecurrenceTriggerList();
|
||||
}
|
||||
|
||||
public function recurrenceTimeframes()
|
||||
{
|
||||
return $this->task->getRecurrenceTimeframeList();
|
||||
}
|
||||
|
||||
public function recurrenceBasedates()
|
||||
{
|
||||
return $this->task->getRecurrenceBasedateList();
|
||||
}
|
||||
|
||||
public function canRemove(array $task)
|
||||
{
|
||||
return $this->taskPermission->canRemoveTask($task);
|
||||
}
|
||||
}
|
72
sources/app/Helper/Text.php
Normal file
72
sources/app/Helper/Text.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace Helper;
|
||||
|
||||
use Core\Markdown;
|
||||
|
||||
/**
|
||||
* Text helpers
|
||||
*
|
||||
* @package helper
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Text extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Markdown transformation
|
||||
*
|
||||
* @param string $text Markdown content
|
||||
* @param array $link Link parameters for replacement
|
||||
* @return string
|
||||
*/
|
||||
public function markdown($text, array $link = array())
|
||||
{
|
||||
$parser = new Markdown($link, $this->helper->url);
|
||||
$parser->setMarkupEscaped(MARKDOWN_ESCAPE_HTML);
|
||||
return $parser->text($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a file size
|
||||
*
|
||||
* @param integer $size Size in bytes
|
||||
* @param integer $precision Precision
|
||||
* @return string
|
||||
*/
|
||||
public function bytes($size, $precision = 2)
|
||||
{
|
||||
$base = log($size) / log(1024);
|
||||
$suffixes = array('', 'k', 'M', 'G', 'T');
|
||||
|
||||
return round(pow(1024, $base - floor($base)), $precision).$suffixes[(int)floor($base)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if needle is contained in the haystack
|
||||
*
|
||||
* @param string $haystack Haystack
|
||||
* @param string $needle Needle
|
||||
* @return boolean
|
||||
*/
|
||||
public function contains($haystack, $needle)
|
||||
{
|
||||
return strpos($haystack, $needle) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value from a dictionary
|
||||
*
|
||||
* @param mixed $id Key
|
||||
* @param array $listing Dictionary
|
||||
* @param string $default_value Value displayed when the key doesn't exists
|
||||
* @return string
|
||||
*/
|
||||
public function in($id, array $listing, $default_value = '?')
|
||||
{
|
||||
if (isset($listing[$id])) {
|
||||
return $this->helper->e($listing[$id]);
|
||||
}
|
||||
|
||||
return $default_value;
|
||||
}
|
||||
}
|
170
sources/app/Helper/Url.php
Normal file
170
sources/app/Helper/Url.php
Normal file
|
@ -0,0 +1,170 @@
|
|||
<?php
|
||||
|
||||
namespace Helper;
|
||||
|
||||
use Core\Request;
|
||||
use Core\Security;
|
||||
|
||||
/**
|
||||
* Url helpers
|
||||
*
|
||||
* @package helper
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Url extends \Core\Base
|
||||
{
|
||||
private $base = '';
|
||||
private $directory = '';
|
||||
|
||||
/**
|
||||
* Helper to generate a link to the documentation
|
||||
*
|
||||
* @access public
|
||||
* @param string $label
|
||||
* @param string $file
|
||||
* @return string
|
||||
*/
|
||||
public function doc($label, $file)
|
||||
{
|
||||
return $this->link($label, 'doc', 'show', array('file' => $file), false, '', '', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML Link tag
|
||||
*
|
||||
* @access public
|
||||
* @param string $label Link label
|
||||
* @param string $controller Controller name
|
||||
* @param string $action Action name
|
||||
* @param array $params Url parameters
|
||||
* @param boolean $csrf Add a CSRF token
|
||||
* @param string $class CSS class attribute
|
||||
* @param boolean $new_tab Open the link in a new tab
|
||||
* @param string $anchor Link Anchor
|
||||
* @return string
|
||||
*/
|
||||
public function link($label, $controller, $action, array $params = array(), $csrf = false, $class = '', $title = '', $new_tab = false, $anchor = '')
|
||||
{
|
||||
return '<a href="'.$this->href($controller, $action, $params, $csrf, $anchor).'" class="'.$class.'" title="'.$title.'" '.($new_tab ? 'target="_blank"' : '').'>'.$label.'</a>';
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML Hyperlink
|
||||
*
|
||||
* @access public
|
||||
* @param string $controller Controller name
|
||||
* @param string $action Action name
|
||||
* @param array $params Url parameters
|
||||
* @param boolean $csrf Add a CSRF token
|
||||
* @param string $anchor Link Anchor
|
||||
* @param boolean $absolute Absolute or relative link
|
||||
* @return string
|
||||
*/
|
||||
public function href($controller, $action, array $params = array(), $csrf = false, $anchor = '', $absolute = false)
|
||||
{
|
||||
return $this->build('&', $controller, $action, $params, $csrf, $anchor, $absolute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate controller/action url
|
||||
*
|
||||
* @access public
|
||||
* @param string $controller Controller name
|
||||
* @param string $action Action name
|
||||
* @param array $params Url parameters
|
||||
* @param string $anchor Link Anchor
|
||||
* @param boolean $absolute Absolute or relative link
|
||||
* @return string
|
||||
*/
|
||||
public function to($controller, $action, array $params = array(), $anchor = '', $absolute = false)
|
||||
{
|
||||
return $this->build('&', $controller, $action, $params, false, $anchor, $absolute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get application base url
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function base()
|
||||
{
|
||||
if (empty($this->base)) {
|
||||
$this->base = $this->config->get('application_url') ?: $this->server();
|
||||
}
|
||||
|
||||
return $this->base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get application base directory
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function dir()
|
||||
{
|
||||
if (empty($this->directory) && isset($_SERVER['REQUEST_METHOD'])) {
|
||||
$this->directory = str_replace('\\', '/', dirname($_SERVER['PHP_SELF']));
|
||||
$this->directory = $this->directory !== '/' ? $this->directory.'/' : '/';
|
||||
$this->directory = str_replace('//', '/', $this->directory);
|
||||
}
|
||||
|
||||
return $this->directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current server base url
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function server()
|
||||
{
|
||||
if (empty($_SERVER['SERVER_NAME'])) {
|
||||
return 'http://localhost/';
|
||||
}
|
||||
|
||||
$url = Request::isHTTPS() ? 'https://' : 'http://';
|
||||
$url .= $_SERVER['SERVER_NAME'];
|
||||
$url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT'];
|
||||
$url .= $this->dir() ?: '/';
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build relative url
|
||||
*
|
||||
* @access private
|
||||
* @param string $separator Querystring argument separator
|
||||
* @param string $controller Controller name
|
||||
* @param string $action Action name
|
||||
* @param array $params Url parameters
|
||||
* @param boolean $csrf Add a CSRF token
|
||||
* @param string $anchor Link Anchor
|
||||
* @param boolean $absolute Absolute or relative link
|
||||
* @return string
|
||||
*/
|
||||
private function build($separator, $controller, $action, array $params = array(), $csrf = false, $anchor = '', $absolute = false)
|
||||
{
|
||||
$path = $this->router->findUrl($controller, $action, $params);
|
||||
$qs = array();
|
||||
|
||||
if (empty($path)) {
|
||||
$qs['controller'] = $controller;
|
||||
$qs['action'] = $action;
|
||||
$qs += $params;
|
||||
}
|
||||
|
||||
if ($csrf) {
|
||||
$qs['csrf_token'] = Security::getCSRFToken();
|
||||
}
|
||||
|
||||
if (! empty($qs)) {
|
||||
$path .= '?'.http_build_query($qs, '', $separator);
|
||||
}
|
||||
|
||||
return ($absolute ? $this->base() : $this->dir()).$path.(empty($anchor) ? '' : '#'.$anchor);
|
||||
}
|
||||
}
|
147
sources/app/Helper/User.php
Normal file
147
sources/app/Helper/User.php
Normal file
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
|
||||
namespace Helper;
|
||||
|
||||
/**
|
||||
* User helpers
|
||||
*
|
||||
* @package helper
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class User extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Get initials from a user
|
||||
*
|
||||
* @access public
|
||||
* @param string $name
|
||||
* @return string
|
||||
*/
|
||||
public function getInitials($name)
|
||||
{
|
||||
$initials = '';
|
||||
|
||||
foreach (explode(' ', $name) as $string) {
|
||||
$initials .= mb_substr($string, 0, 1);
|
||||
}
|
||||
|
||||
return mb_strtoupper($initials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user id
|
||||
*
|
||||
* @access public
|
||||
* @return integer
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->userSession->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user profile
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getProfileLink()
|
||||
{
|
||||
return $this->helper->url->link(
|
||||
$this->helper->e($this->getFullname()),
|
||||
'user',
|
||||
'show',
|
||||
array('user_id' => $this->userSession->getId())
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Check if the given user_id is the connected user
|
||||
*
|
||||
* @param integer $user_id User id
|
||||
* @return boolean
|
||||
*/
|
||||
public function isCurrentUser($user_id)
|
||||
{
|
||||
return $this->userSession->getId() == $user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if the logged user is admin
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function isAdmin()
|
||||
{
|
||||
return $this->userSession->isAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if the logged user is project admin
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function isProjectAdmin()
|
||||
{
|
||||
return $this->userSession->isProjectAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for project administration actions access (Project Admin group)
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function isProjectAdministrationAllowed($project_id)
|
||||
{
|
||||
if ($this->userSession->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->memoryCache->proxy('acl', 'handleProjectAdminPermissions', $project_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for project management actions access (Regular users who are Project Managers)
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function isProjectManagementAllowed($project_id)
|
||||
{
|
||||
if ($this->userSession->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->memoryCache->proxy('acl', 'handleProjectManagerPermissions', $project_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user full name
|
||||
*
|
||||
* @param array $user User properties
|
||||
* @return string
|
||||
*/
|
||||
public function getFullname(array $user = array())
|
||||
{
|
||||
return $this->user->getFullname(empty($user) ? $_SESSION['user'] : $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display gravatar image
|
||||
*
|
||||
* @access public
|
||||
* @param string $email
|
||||
* @param string $alt
|
||||
* @return string
|
||||
*/
|
||||
public function avatar($email, $alt = '')
|
||||
{
|
||||
if (! empty($email) && $this->config->get('integration_gravatar') == 1) {
|
||||
return '<img class="avatar" src="https://www.gravatar.com/avatar/'.md5(strtolower($email)).'?s=25" alt="'.$this->helper->e($alt).'" title="'.$this->helper->e($alt).'">';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
94
sources/app/Integration/HipchatWebhook.php
Normal file
94
sources/app/Integration/HipchatWebhook.php
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
namespace Integration;
|
||||
|
||||
/**
|
||||
* Hipchat webhook
|
||||
*
|
||||
* @package integration
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class HipchatWebhook extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Return true if Hipchat is enabled for this project or globally
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @return boolean
|
||||
*/
|
||||
public function isActivated($project_id)
|
||||
{
|
||||
return $this->config->get('integration_hipchat') == 1 || $this->projectIntegration->hasValue($project_id, 'hipchat', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API parameters
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @return array
|
||||
*/
|
||||
public function getParameters($project_id)
|
||||
{
|
||||
if ($this->config->get('integration_hipchat') == 1) {
|
||||
return array(
|
||||
'api_url' => $this->config->get('integration_hipchat_api_url'),
|
||||
'room_id' => $this->config->get('integration_hipchat_room_id'),
|
||||
'room_token' => $this->config->get('integration_hipchat_room_token'),
|
||||
);
|
||||
}
|
||||
|
||||
$options = $this->projectIntegration->getParameters($project_id);
|
||||
|
||||
return array(
|
||||
'api_url' => $options['hipchat_api_url'],
|
||||
'room_id' => $options['hipchat_room_id'],
|
||||
'room_token' => $options['hipchat_room_token'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the notification if activated
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @param integer $task_id Task id
|
||||
* @param string $event_name Event name
|
||||
* @param array $event Event data
|
||||
*/
|
||||
public function notify($project_id, $task_id, $event_name, array $event)
|
||||
{
|
||||
if ($this->isActivated($project_id)) {
|
||||
|
||||
$params = $this->getParameters($project_id);
|
||||
$project = $this->project->getbyId($project_id);
|
||||
|
||||
$event['event_name'] = $event_name;
|
||||
$event['author'] = $this->user->getFullname($this->session['user']);
|
||||
|
||||
$html = '<img src="http://kanboard.net/assets/img/favicon-32x32.png"/>';
|
||||
$html .= '<strong>'.$project['name'].'</strong>'.(isset($event['task']['title']) ? '<br/>'.$event['task']['title'] : '').'<br/>';
|
||||
$html .= $this->projectActivity->getTitle($event);
|
||||
|
||||
if ($this->config->get('application_url')) {
|
||||
$html .= '<br/><a href="'.$this->helper->url->href('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id), false, '', true).'">';
|
||||
$html .= t('view the task on Kanboard').'</a>';
|
||||
}
|
||||
|
||||
$payload = array(
|
||||
'message' => $html,
|
||||
'color' => 'yellow',
|
||||
);
|
||||
|
||||
$url = sprintf(
|
||||
'%s/v2/room/%s/notification?auth_token=%s',
|
||||
$params['api_url'],
|
||||
$params['room_id'],
|
||||
$params['room_token']
|
||||
);
|
||||
|
||||
$this->httpClient->postJson($url, $payload);
|
||||
}
|
||||
}
|
||||
}
|
129
sources/app/Integration/Jabber.php
Normal file
129
sources/app/Integration/Jabber.php
Normal file
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
namespace Integration;
|
||||
|
||||
use Exception;
|
||||
use Fabiang\Xmpp\Options;
|
||||
use Fabiang\Xmpp\Client;
|
||||
use Fabiang\Xmpp\Protocol\Message;
|
||||
use Fabiang\Xmpp\Protocol\Presence;
|
||||
|
||||
/**
|
||||
* Jabber
|
||||
*
|
||||
* @package integration
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Jabber extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Return true if Jabber is enabled for this project or globally
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @return boolean
|
||||
*/
|
||||
public function isActivated($project_id)
|
||||
{
|
||||
return $this->config->get('integration_jabber') == 1 || $this->projectIntegration->hasValue($project_id, 'jabber', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection parameters
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @return array
|
||||
*/
|
||||
public function getParameters($project_id)
|
||||
{
|
||||
if ($this->config->get('integration_jabber') == 1) {
|
||||
return array(
|
||||
'server' => $this->config->get('integration_jabber_server'),
|
||||
'domain' => $this->config->get('integration_jabber_domain'),
|
||||
'username' => $this->config->get('integration_jabber_username'),
|
||||
'password' => $this->config->get('integration_jabber_password'),
|
||||
'nickname' => $this->config->get('integration_jabber_nickname'),
|
||||
'room' => $this->config->get('integration_jabber_room'),
|
||||
);
|
||||
}
|
||||
|
||||
$options = $this->projectIntegration->getParameters($project_id);
|
||||
|
||||
return array(
|
||||
'server' => $options['jabber_server'],
|
||||
'domain' => $options['jabber_domain'],
|
||||
'username' => $options['jabber_username'],
|
||||
'password' => $options['jabber_password'],
|
||||
'nickname' => $options['jabber_nickname'],
|
||||
'room' => $options['jabber_room'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and send the message
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @param integer $task_id Task id
|
||||
* @param string $event_name Event name
|
||||
* @param array $event Event data
|
||||
*/
|
||||
public function notify($project_id, $task_id, $event_name, array $event)
|
||||
{
|
||||
if ($this->isActivated($project_id)) {
|
||||
|
||||
$project = $this->project->getbyId($project_id);
|
||||
|
||||
$event['event_name'] = $event_name;
|
||||
$event['author'] = $this->user->getFullname($this->session['user']);
|
||||
|
||||
$payload = '['.$project['name'].'] '.str_replace('"', '"', $this->projectActivity->getTitle($event)).(isset($event['task']['title']) ? ' ('.$event['task']['title'].')' : '');
|
||||
|
||||
if ($this->config->get('application_url')) {
|
||||
$payload .= ' '.$this->helper->url->to('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id), '', true);
|
||||
}
|
||||
|
||||
$this->sendMessage($project_id, $payload);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to the XMPP server
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @param string $payload
|
||||
*/
|
||||
public function sendMessage($project_id, $payload)
|
||||
{
|
||||
try {
|
||||
|
||||
$params = $this->getParameters($project_id);
|
||||
|
||||
$options = new Options($params['server']);
|
||||
$options->setUsername($params['username']);
|
||||
$options->setPassword($params['password']);
|
||||
$options->setTo($params['domain']);
|
||||
$options->setLogger($this->container['logger']);
|
||||
|
||||
$client = new Client($options);
|
||||
|
||||
$channel = new Presence;
|
||||
$channel->setTo($params['room'])->setNickName($params['nickname']);
|
||||
$client->send($channel);
|
||||
|
||||
$message = new Message;
|
||||
$message->setMessage($payload)
|
||||
->setTo($params['room'])
|
||||
->setType(Message::TYPE_GROUPCHAT);
|
||||
|
||||
$client->send($message);
|
||||
|
||||
$client->disconnect();
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->container['logger']->error('Jabber error: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
97
sources/app/Integration/Mailgun.php
Normal file
97
sources/app/Integration/Mailgun.php
Normal file
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace Integration;
|
||||
|
||||
use HTML_To_Markdown;
|
||||
use Core\Tool;
|
||||
|
||||
/**
|
||||
* Mailgun Integration
|
||||
*
|
||||
* @package integration
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Mailgun extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Send a HTML email
|
||||
*
|
||||
* @access public
|
||||
* @param string $email
|
||||
* @param string $name
|
||||
* @param string $subject
|
||||
* @param string $html
|
||||
* @param string $author
|
||||
*/
|
||||
public function sendEmail($email, $name, $subject, $html, $author)
|
||||
{
|
||||
$headers = array(
|
||||
'Authorization: Basic '.base64_encode('api:'.MAILGUN_API_TOKEN)
|
||||
);
|
||||
|
||||
$payload = array(
|
||||
'from' => sprintf('%s <%s>', $author, MAIL_FROM),
|
||||
'to' => sprintf('%s <%s>', $name, $email),
|
||||
'subject' => $subject,
|
||||
'html' => $html,
|
||||
);
|
||||
|
||||
$this->httpClient->postForm('https://api.mailgun.net/v3/'.MAILGUN_DOMAIN.'/messages', $payload, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse incoming email
|
||||
*
|
||||
* @access public
|
||||
* @param array $payload Incoming email
|
||||
* @return boolean
|
||||
*/
|
||||
public function receiveEmail(array $payload)
|
||||
{
|
||||
if (empty($payload['sender']) || empty($payload['subject']) || empty($payload['recipient'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The user must exists in Kanboard
|
||||
$user = $this->user->getByEmail($payload['sender']);
|
||||
|
||||
if (empty($user)) {
|
||||
$this->container['logger']->debug('Mailgun: ignored => user not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// The project must have a short name
|
||||
$project = $this->project->getByIdentifier(Tool::getMailboxHash($payload['recipient']));
|
||||
|
||||
if (empty($project)) {
|
||||
$this->container['logger']->debug('Mailgun: ignored => project not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// The user must be member of the project
|
||||
if (! $this->projectPermission->isMember($project['id'], $user['id'])) {
|
||||
$this->container['logger']->debug('Mailgun: ignored => user is not member of the project');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the Markdown contents
|
||||
if (! empty($payload['stripped-html'])) {
|
||||
$markdown = new HTML_To_Markdown($payload['stripped-html'], array('strip_tags' => true));
|
||||
$description = $markdown->output();
|
||||
}
|
||||
else if (! empty($payload['stripped-text'])) {
|
||||
$description = $payload['stripped-text'];
|
||||
}
|
||||
else {
|
||||
$description = '';
|
||||
}
|
||||
|
||||
// Finally, we create the task
|
||||
return (bool) $this->taskCreation->create(array(
|
||||
'project_id' => $project['id'],
|
||||
'title' => $payload['subject'],
|
||||
'description' => $description,
|
||||
'creator_id' => $user['id'],
|
||||
));
|
||||
}
|
||||
}
|
97
sources/app/Integration/Postmark.php
Normal file
97
sources/app/Integration/Postmark.php
Normal file
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace Integration;
|
||||
|
||||
use HTML_To_Markdown;
|
||||
|
||||
/**
|
||||
* Postmark integration
|
||||
*
|
||||
* @package integration
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Postmark extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Send a HTML email
|
||||
*
|
||||
* @access public
|
||||
* @param string $email
|
||||
* @param string $name
|
||||
* @param string $subject
|
||||
* @param string $html
|
||||
* @param string $author
|
||||
*/
|
||||
public function sendEmail($email, $name, $subject, $html, $author)
|
||||
{
|
||||
$headers = array(
|
||||
'Accept: application/json',
|
||||
'X-Postmark-Server-Token: '.POSTMARK_API_TOKEN,
|
||||
);
|
||||
|
||||
$payload = array(
|
||||
'From' => sprintf('%s <%s>', $author, MAIL_FROM),
|
||||
'To' => sprintf('%s <%s>', $name, $email),
|
||||
'Subject' => $subject,
|
||||
'HtmlBody' => $html,
|
||||
);
|
||||
|
||||
$this->httpClient->postJson('https://api.postmarkapp.com/email', $payload, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse incoming email
|
||||
*
|
||||
* @access public
|
||||
* @param array $payload Incoming email
|
||||
* @return boolean
|
||||
*/
|
||||
public function receiveEmail(array $payload)
|
||||
{
|
||||
if (empty($payload['From']) || empty($payload['Subject']) || empty($payload['MailboxHash'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The user must exists in Kanboard
|
||||
$user = $this->user->getByEmail($payload['From']);
|
||||
|
||||
if (empty($user)) {
|
||||
$this->container['logger']->debug('Postmark: ignored => user not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// The project must have a short name
|
||||
$project = $this->project->getByIdentifier($payload['MailboxHash']);
|
||||
|
||||
if (empty($project)) {
|
||||
$this->container['logger']->debug('Postmark: ignored => project not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// The user must be member of the project
|
||||
if (! $this->projectPermission->isMember($project['id'], $user['id'])) {
|
||||
$this->container['logger']->debug('Postmark: ignored => user is not member of the project');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the Markdown contents
|
||||
if (! empty($payload['HtmlBody'])) {
|
||||
$markdown = new HTML_To_Markdown($payload['HtmlBody'], array('strip_tags' => true));
|
||||
$description = $markdown->output();
|
||||
}
|
||||
else if (! empty($payload['TextBody'])) {
|
||||
$description = $payload['TextBody'];
|
||||
}
|
||||
else {
|
||||
$description = '';
|
||||
}
|
||||
|
||||
// Finally, we create the task
|
||||
return (bool) $this->taskCreation->create(array(
|
||||
'project_id' => $project['id'],
|
||||
'title' => $payload['Subject'],
|
||||
'description' => $description,
|
||||
'creator_id' => $user['id'],
|
||||
));
|
||||
}
|
||||
}
|
100
sources/app/Integration/Sendgrid.php
Normal file
100
sources/app/Integration/Sendgrid.php
Normal file
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
namespace Integration;
|
||||
|
||||
use HTML_To_Markdown;
|
||||
use Core\Tool;
|
||||
|
||||
/**
|
||||
* Sendgrid Integration
|
||||
*
|
||||
* @package integration
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Sendgrid extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Send a HTML email
|
||||
*
|
||||
* @access public
|
||||
* @param string $email
|
||||
* @param string $name
|
||||
* @param string $subject
|
||||
* @param string $html
|
||||
* @param string $author
|
||||
*/
|
||||
public function sendEmail($email, $name, $subject, $html, $author)
|
||||
{
|
||||
$payload = array(
|
||||
'api_user' => SENDGRID_API_USER,
|
||||
'api_key' => SENDGRID_API_KEY,
|
||||
'to' => $email,
|
||||
'toname' => $name,
|
||||
'from' => MAIL_FROM,
|
||||
'fromname' => $author,
|
||||
'html' => $html,
|
||||
'subject' => $subject,
|
||||
);
|
||||
|
||||
$this->httpClient->postForm('https://api.sendgrid.com/api/mail.send.json', $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse incoming email
|
||||
*
|
||||
* @access public
|
||||
* @param array $payload Incoming email
|
||||
* @return boolean
|
||||
*/
|
||||
public function receiveEmail(array $payload)
|
||||
{
|
||||
if (empty($payload['envelope']) || empty($payload['subject'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$envelope = json_decode($payload['envelope'], true);
|
||||
$sender = isset($envelope['to'][0]) ? $envelope['to'][0] : '';
|
||||
|
||||
// The user must exists in Kanboard
|
||||
$user = $this->user->getByEmail($envelope['from']);
|
||||
|
||||
if (empty($user)) {
|
||||
$this->container['logger']->debug('SendgridWebhook: ignored => user not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// The project must have a short name
|
||||
$project = $this->project->getByIdentifier(Tool::getMailboxHash($sender));
|
||||
|
||||
if (empty($project)) {
|
||||
$this->container['logger']->debug('SendgridWebhook: ignored => project not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// The user must be member of the project
|
||||
if (! $this->projectPermission->isMember($project['id'], $user['id'])) {
|
||||
$this->container['logger']->debug('SendgridWebhook: ignored => user is not member of the project');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the Markdown contents
|
||||
if (! empty($payload['html'])) {
|
||||
$markdown = new HTML_To_Markdown($payload['html'], array('strip_tags' => true));
|
||||
$description = $markdown->output();
|
||||
}
|
||||
else if (! empty($payload['text'])) {
|
||||
$description = $payload['text'];
|
||||
}
|
||||
else {
|
||||
$description = '';
|
||||
}
|
||||
|
||||
// Finally, we create the task
|
||||
return (bool) $this->taskCreation->create(array(
|
||||
'project_id' => $project['id'],
|
||||
'title' => $payload['subject'],
|
||||
'description' => $description,
|
||||
'creator_id' => $user['id'],
|
||||
));
|
||||
}
|
||||
}
|
71
sources/app/Integration/Smtp.php
Normal file
71
sources/app/Integration/Smtp.php
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace Integration;
|
||||
|
||||
use Swift_Message;
|
||||
use Swift_Mailer;
|
||||
use Swift_MailTransport;
|
||||
use Swift_SendmailTransport;
|
||||
use Swift_SmtpTransport;
|
||||
use Swift_TransportException;
|
||||
|
||||
/**
|
||||
* Smtp
|
||||
*
|
||||
* @package integration
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Smtp extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Send a HTML email
|
||||
*
|
||||
* @access public
|
||||
* @param string $email
|
||||
* @param string $name
|
||||
* @param string $subject
|
||||
* @param string $html
|
||||
* @param string $author
|
||||
*/
|
||||
public function sendEmail($email, $name, $subject, $html, $author)
|
||||
{
|
||||
try {
|
||||
|
||||
$message = Swift_Message::newInstance()
|
||||
->setSubject($subject)
|
||||
->setFrom(array(MAIL_FROM => $author))
|
||||
->setBody($html, 'text/html')
|
||||
->setTo(array($email => $name));
|
||||
|
||||
Swift_Mailer::newInstance($this->getTransport())->send($message);
|
||||
}
|
||||
catch (Swift_TransportException $e) {
|
||||
$this->container['logger']->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SwiftMailer transport
|
||||
*
|
||||
* @access private
|
||||
* @return \Swift_Transport
|
||||
*/
|
||||
private function getTransport()
|
||||
{
|
||||
switch (MAIL_TRANSPORT) {
|
||||
case 'smtp':
|
||||
$transport = Swift_SmtpTransport::newInstance(MAIL_SMTP_HOSTNAME, MAIL_SMTP_PORT);
|
||||
$transport->setUsername(MAIL_SMTP_USERNAME);
|
||||
$transport->setPassword(MAIL_SMTP_PASSWORD);
|
||||
$transport->setEncryption(MAIL_SMTP_ENCRYPTION);
|
||||
break;
|
||||
case 'sendmail':
|
||||
$transport = Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND);
|
||||
break;
|
||||
default:
|
||||
$transport = Swift_MailTransport::newInstance();
|
||||
}
|
||||
|
||||
return $transport;
|
||||
}
|
||||
}
|
227
sources/app/Library/password.php
Normal file
227
sources/app/Library/password.php
Normal file
|
@ -0,0 +1,227 @@
|
|||
<?php
|
||||
/**
|
||||
* A Compatibility library with PHP 5.5's simplified password hashing API.
|
||||
*
|
||||
* @author Anthony Ferrara <ircmaxell@php.net>
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @copyright 2012 The Authors
|
||||
*/
|
||||
|
||||
if (!defined('PASSWORD_BCRYPT')) {
|
||||
|
||||
define('PASSWORD_BCRYPT', 1);
|
||||
define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
|
||||
|
||||
if (version_compare(PHP_VERSION, '5.3.7', '<')) {
|
||||
|
||||
define('PASSWORD_PREFIX', '$2a$');
|
||||
}
|
||||
else {
|
||||
|
||||
define('PASSWORD_PREFIX', '$2y$');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash the password using the specified algorithm
|
||||
*
|
||||
* @param string $password The password to hash
|
||||
* @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
|
||||
* @param array $options The options for the algorithm to use
|
||||
*
|
||||
* @return string|false The hashed password, or false on error.
|
||||
*/
|
||||
function password_hash($password, $algo, array $options = array()) {
|
||||
if (!function_exists('crypt')) {
|
||||
trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
|
||||
return null;
|
||||
}
|
||||
if (!is_string($password)) {
|
||||
trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
|
||||
return null;
|
||||
}
|
||||
if (!is_int($algo)) {
|
||||
trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
|
||||
return null;
|
||||
}
|
||||
switch ($algo) {
|
||||
case PASSWORD_BCRYPT:
|
||||
// Note that this is a C constant, but not exposed to PHP, so we don't define it here.
|
||||
$cost = 10;
|
||||
if (isset($options['cost'])) {
|
||||
$cost = $options['cost'];
|
||||
if ($cost < 4 || $cost > 31) {
|
||||
trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
$required_salt_len = 22;
|
||||
$hash_format = sprintf("%s%02d$", PASSWORD_PREFIX, $cost);
|
||||
break;
|
||||
default:
|
||||
trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
|
||||
return null;
|
||||
}
|
||||
if (isset($options['salt'])) {
|
||||
switch (gettype($options['salt'])) {
|
||||
case 'NULL':
|
||||
case 'boolean':
|
||||
case 'integer':
|
||||
case 'double':
|
||||
case 'string':
|
||||
$salt = (string) $options['salt'];
|
||||
break;
|
||||
case 'object':
|
||||
if (method_exists($options['salt'], '__tostring')) {
|
||||
$salt = (string) $options['salt'];
|
||||
break;
|
||||
}
|
||||
case 'array':
|
||||
case 'resource':
|
||||
default:
|
||||
trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
|
||||
return null;
|
||||
}
|
||||
if (strlen($salt) < $required_salt_len) {
|
||||
trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
|
||||
return null;
|
||||
} elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
|
||||
$salt = str_replace('+', '.', base64_encode($salt));
|
||||
}
|
||||
} else {
|
||||
$buffer = '';
|
||||
$raw_length = (int) ($required_salt_len * 3 / 4 + 1);
|
||||
$buffer_valid = false;
|
||||
if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
|
||||
$buffer = mcrypt_create_iv($raw_length, MCRYPT_DEV_URANDOM);
|
||||
if ($buffer) {
|
||||
$buffer_valid = true;
|
||||
}
|
||||
}
|
||||
if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
|
||||
$buffer = openssl_random_pseudo_bytes($raw_length);
|
||||
if ($buffer) {
|
||||
$buffer_valid = true;
|
||||
}
|
||||
}
|
||||
if (!$buffer_valid && is_readable('/dev/urandom')) {
|
||||
$f = fopen('/dev/urandom', 'r');
|
||||
$read = strlen($buffer);
|
||||
while ($read < $raw_length) {
|
||||
$buffer .= fread($f, $raw_length - $read);
|
||||
$read = strlen($buffer);
|
||||
}
|
||||
fclose($f);
|
||||
if ($read >= $raw_length) {
|
||||
$buffer_valid = true;
|
||||
}
|
||||
}
|
||||
if (!$buffer_valid || strlen($buffer) < $raw_length) {
|
||||
$bl = strlen($buffer);
|
||||
for ($i = 0; $i < $raw_length; $i++) {
|
||||
if ($i < $bl) {
|
||||
$buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
|
||||
} else {
|
||||
$buffer .= chr(mt_rand(0, 255));
|
||||
}
|
||||
}
|
||||
}
|
||||
$salt = str_replace('+', '.', base64_encode($buffer));
|
||||
|
||||
}
|
||||
$salt = substr($salt, 0, $required_salt_len);
|
||||
|
||||
$hash = $hash_format . $salt;
|
||||
|
||||
$ret = crypt($password, $hash);
|
||||
|
||||
if (!is_string($ret) || strlen($ret) <= 13) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about the password hash. Returns an array of the information
|
||||
* that was used to generate the password hash.
|
||||
*
|
||||
* array(
|
||||
* 'algo' => 1,
|
||||
* 'algoName' => 'bcrypt',
|
||||
* 'options' => array(
|
||||
* 'cost' => 10,
|
||||
* ),
|
||||
* )
|
||||
*
|
||||
* @param string $hash The password hash to extract info from
|
||||
*
|
||||
* @return array The array of information about the hash.
|
||||
*/
|
||||
function password_get_info($hash) {
|
||||
$return = array(
|
||||
'algo' => 0,
|
||||
'algoName' => 'unknown',
|
||||
'options' => array(),
|
||||
);
|
||||
if (substr($hash, 0, 4) == PASSWORD_PREFIX && strlen($hash) == 60) {
|
||||
$return['algo'] = PASSWORD_BCRYPT;
|
||||
$return['algoName'] = 'bcrypt';
|
||||
list($cost) = sscanf($hash, PASSWORD_PREFIX."%d$");
|
||||
$return['options']['cost'] = $cost;
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the password hash needs to be rehashed according to the options provided
|
||||
*
|
||||
* If the answer is true, after validating the password using password_verify, rehash it.
|
||||
*
|
||||
* @param string $hash The hash to test
|
||||
* @param int $algo The algorithm used for new password hashes
|
||||
* @param array $options The options array passed to password_hash
|
||||
*
|
||||
* @return boolean True if the password needs to be rehashed.
|
||||
*/
|
||||
function password_needs_rehash($hash, $algo, array $options = array()) {
|
||||
$info = password_get_info($hash);
|
||||
if ($info['algo'] != $algo) {
|
||||
return true;
|
||||
}
|
||||
switch ($algo) {
|
||||
case PASSWORD_BCRYPT:
|
||||
$cost = isset($options['cost']) ? $options['cost'] : 10;
|
||||
if ($cost != $info['options']['cost']) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a password against a hash using a timing attack resistant approach
|
||||
*
|
||||
* @param string $password The password to verify
|
||||
* @param string $hash The hash to verify against
|
||||
*
|
||||
* @return boolean If the password matches the hash
|
||||
*/
|
||||
function password_verify($password, $hash) {
|
||||
if (!function_exists('crypt')) {
|
||||
trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
|
||||
return false;
|
||||
}
|
||||
$ret = crypt($password, $hash);
|
||||
if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$status = 0;
|
||||
for ($i = 0; $i < strlen($ret); $i++) {
|
||||
$status |= (ord($ret[$i]) ^ ord($hash[$i]));
|
||||
}
|
||||
|
||||
return $status === 0;
|
||||
}
|
||||
}
|
1070
sources/app/Locale/cs_CZ/translations.php
Normal file
1070
sources/app/Locale/cs_CZ/translations.php
Normal file
File diff suppressed because it is too large
Load diff
1070
sources/app/Locale/nb_NO/translations.php
Normal file
1070
sources/app/Locale/nb_NO/translations.php
Normal file
File diff suppressed because it is too large
Load diff
1070
sources/app/Locale/pt_PT/translations.php
Normal file
1070
sources/app/Locale/pt_PT/translations.php
Normal file
File diff suppressed because it is too large
Load diff
196
sources/app/Model/ProjectDailyColumnStats.php
Normal file
196
sources/app/Model/ProjectDailyColumnStats.php
Normal file
|
@ -0,0 +1,196 @@
|
|||
<?php
|
||||
|
||||
namespace Model;
|
||||
|
||||
/**
|
||||
* Project Daily Column Stats
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ProjectDailyColumnStats extends Base
|
||||
{
|
||||
/**
|
||||
* SQL table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TABLE = 'project_daily_column_stats';
|
||||
|
||||
/**
|
||||
* Update daily totals for the project and foreach column
|
||||
*
|
||||
* "total" is the number open of tasks in the column
|
||||
* "score" is the sum of tasks score in the column
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @param string $date Record date (YYYY-MM-DD)
|
||||
* @return boolean
|
||||
*/
|
||||
public function updateTotals($project_id, $date)
|
||||
{
|
||||
$status = $this->config->get('cfd_include_closed_tasks') == 1 ? array(Task::STATUS_OPEN, Task::STATUS_CLOSED) : array(Task::STATUS_OPEN);
|
||||
|
||||
return $this->db->transaction(function($db) use ($project_id, $date, $status) {
|
||||
|
||||
$column_ids = $db->table(Board::TABLE)->eq('project_id', $project_id)->findAllByColumn('id');
|
||||
|
||||
foreach ($column_ids as $column_id) {
|
||||
|
||||
// This call will fail if the record already exists
|
||||
// (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE)
|
||||
$db->table(ProjectDailyColumnStats::TABLE)->insert(array(
|
||||
'day' => $date,
|
||||
'project_id' => $project_id,
|
||||
'column_id' => $column_id,
|
||||
'total' => 0,
|
||||
'score' => 0,
|
||||
));
|
||||
|
||||
$db->table(ProjectDailyColumnStats::TABLE)
|
||||
->eq('project_id', $project_id)
|
||||
->eq('column_id', $column_id)
|
||||
->eq('day', $date)
|
||||
->update(array(
|
||||
'score' => $db->table(Task::TABLE)
|
||||
->eq('project_id', $project_id)
|
||||
->eq('column_id', $column_id)
|
||||
->eq('is_active', Task::STATUS_OPEN)
|
||||
->sum('score'),
|
||||
'total' => $db->table(Task::TABLE)
|
||||
->eq('project_id', $project_id)
|
||||
->eq('column_id', $column_id)
|
||||
->in('is_active', $status)
|
||||
->count()
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of recorded days for the data range
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @param string $from Start date (ISO format YYYY-MM-DD)
|
||||
* @param string $to End date
|
||||
* @return integer
|
||||
*/
|
||||
public function countDays($project_id, $from, $to)
|
||||
{
|
||||
$rq = $this->db->execute(
|
||||
'SELECT COUNT(DISTINCT day) FROM '.self::TABLE.' WHERE day >= ? AND day <= ? AND project_id=?',
|
||||
array($from, $to, $project_id)
|
||||
);
|
||||
|
||||
return $rq !== false ? $rq->fetchColumn(0) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw metrics for the project within a data range
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @param string $from Start date (ISO format YYYY-MM-DD)
|
||||
* @param string $to End date
|
||||
* @return array
|
||||
*/
|
||||
public function getRawMetrics($project_id, $from, $to)
|
||||
{
|
||||
return $this->db->table(ProjectDailyColumnStats::TABLE)
|
||||
->columns(
|
||||
ProjectDailyColumnStats::TABLE.'.column_id',
|
||||
ProjectDailyColumnStats::TABLE.'.day',
|
||||
ProjectDailyColumnStats::TABLE.'.total',
|
||||
ProjectDailyColumnStats::TABLE.'.score',
|
||||
Board::TABLE.'.title AS column_title'
|
||||
)
|
||||
->join(Board::TABLE, 'id', 'column_id')
|
||||
->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id)
|
||||
->gte('day', $from)
|
||||
->lte('day', $to)
|
||||
->asc(ProjectDailyColumnStats::TABLE.'.day')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw metrics for the project within a data range grouped by day
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @param string $from Start date (ISO format YYYY-MM-DD)
|
||||
* @param string $to End date
|
||||
* @return array
|
||||
*/
|
||||
public function getRawMetricsByDay($project_id, $from, $to)
|
||||
{
|
||||
return $this->db->table(ProjectDailyColumnStats::TABLE)
|
||||
->columns(
|
||||
ProjectDailyColumnStats::TABLE.'.day',
|
||||
'SUM('.ProjectDailyColumnStats::TABLE.'.total) AS total',
|
||||
'SUM('.ProjectDailyColumnStats::TABLE.'.score) AS score'
|
||||
)
|
||||
->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id)
|
||||
->gte('day', $from)
|
||||
->lte('day', $to)
|
||||
->asc(ProjectDailyColumnStats::TABLE.'.day')
|
||||
->groupBy(ProjectDailyColumnStats::TABLE.'.day')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregated metrics for the project within a data range
|
||||
*
|
||||
* [
|
||||
* ['Date', 'Column1', 'Column2'],
|
||||
* ['2014-11-16', 2, 5],
|
||||
* ['2014-11-17', 20, 15],
|
||||
* ]
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @param string $from Start date (ISO format YYYY-MM-DD)
|
||||
* @param string $to End date
|
||||
* @param string $column Column to aggregate
|
||||
* @return array
|
||||
*/
|
||||
public function getAggregatedMetrics($project_id, $from, $to, $column = 'total')
|
||||
{
|
||||
$columns = $this->board->getColumnsList($project_id);
|
||||
$column_ids = array_keys($columns);
|
||||
$metrics = array(array_merge(array(e('Date')), array_values($columns)));
|
||||
$aggregates = array();
|
||||
|
||||
// Fetch metrics for the project
|
||||
$records = $this->db->table(ProjectDailyColumnStats::TABLE)
|
||||
->eq('project_id', $project_id)
|
||||
->gte('day', $from)
|
||||
->lte('day', $to)
|
||||
->findAll();
|
||||
|
||||
// Aggregate by day
|
||||
foreach ($records as $record) {
|
||||
|
||||
if (! isset($aggregates[$record['day']])) {
|
||||
$aggregates[$record['day']] = array($record['day']);
|
||||
}
|
||||
|
||||
$aggregates[$record['day']][$record['column_id']] = $record[$column];
|
||||
}
|
||||
|
||||
// Aggregate by row
|
||||
foreach ($aggregates as $aggregate) {
|
||||
|
||||
$row = array($aggregate[0]);
|
||||
|
||||
foreach ($column_ids as $column_id) {
|
||||
$row[] = (int) $aggregate[$column_id];
|
||||
}
|
||||
|
||||
$metrics[] = $row;
|
||||
}
|
||||
|
||||
return $metrics;
|
||||
}
|
||||
}
|
72
sources/app/Model/ProjectDailyStats.php
Normal file
72
sources/app/Model/ProjectDailyStats.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace Model;
|
||||
|
||||
/**
|
||||
* Project Daily Stats
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ProjectDailyStats extends Base
|
||||
{
|
||||
/**
|
||||
* SQL table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TABLE = 'project_daily_stats';
|
||||
|
||||
/**
|
||||
* Update daily totals for the project
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @param string $date Record date (YYYY-MM-DD)
|
||||
* @return boolean
|
||||
*/
|
||||
public function updateTotals($project_id, $date)
|
||||
{
|
||||
$lead_cycle_time = $this->projectAnalytic->getAverageLeadAndCycleTime($project_id);
|
||||
|
||||
return $this->db->transaction(function($db) use ($project_id, $date, $lead_cycle_time) {
|
||||
|
||||
// This call will fail if the record already exists
|
||||
// (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE)
|
||||
$db->table(ProjectDailyStats::TABLE)->insert(array(
|
||||
'day' => $date,
|
||||
'project_id' => $project_id,
|
||||
'avg_lead_time' => 0,
|
||||
'avg_cycle_time' => 0,
|
||||
));
|
||||
|
||||
$db->table(ProjectDailyStats::TABLE)
|
||||
->eq('project_id', $project_id)
|
||||
->eq('day', $date)
|
||||
->update(array(
|
||||
'avg_lead_time' => $lead_cycle_time['avg_lead_time'],
|
||||
'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw metrics for the project within a data range
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @param string $from Start date (ISO format YYYY-MM-DD)
|
||||
* @param string $to End date
|
||||
* @return array
|
||||
*/
|
||||
public function getRawMetrics($project_id, $from, $to)
|
||||
{
|
||||
return $this->db->table(self::TABLE)
|
||||
->columns('day', 'avg_lead_time', 'avg_cycle_time')
|
||||
->eq(self::TABLE.'.project_id', $project_id)
|
||||
->gte('day', $from)
|
||||
->lte('day', $to)
|
||||
->asc(self::TABLE.'.day')
|
||||
->findAll();
|
||||
}
|
||||
}
|
66
sources/app/Model/ProjectIntegration.php
Normal file
66
sources/app/Model/ProjectIntegration.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace Model;
|
||||
|
||||
/**
|
||||
* Project integration
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ProjectIntegration extends Base
|
||||
{
|
||||
/**
|
||||
* SQL table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TABLE = 'project_integrations';
|
||||
|
||||
/**
|
||||
* Get all parameters for a project
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @return array
|
||||
*/
|
||||
public function getParameters($project_id)
|
||||
{
|
||||
return $this->db->table(self::TABLE)->eq('project_id', $project_id)->findOne() ?: array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save parameters for a project
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @param array $values
|
||||
* @return boolean
|
||||
*/
|
||||
public function saveParameters($project_id, array $values)
|
||||
{
|
||||
if ($this->db->table(self::TABLE)->eq('project_id', $project_id)->exists()) {
|
||||
return $this->db->table(self::TABLE)->eq('project_id', $project_id)->update($values);
|
||||
}
|
||||
|
||||
return $this->db->table(self::TABLE)->insert($values + array('project_id' => $project_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a project has the given parameter/value
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @param string $option
|
||||
* @param string $value
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasValue($project_id, $option, $value)
|
||||
{
|
||||
return $this->db
|
||||
->table(self::TABLE)
|
||||
->eq('project_id', $project_id)
|
||||
->eq($option, $value)
|
||||
->exists();
|
||||
}
|
||||
}
|
71
sources/app/Model/TaskAnalytic.php
Normal file
71
sources/app/Model/TaskAnalytic.php
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace Model;
|
||||
|
||||
/**
|
||||
* Task Analytic
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class TaskAnalytic extends Base
|
||||
{
|
||||
/**
|
||||
* Get the time between date_creation and date_completed or now if empty
|
||||
*
|
||||
* @access public
|
||||
* @param array $task
|
||||
* @return integer
|
||||
*/
|
||||
public function getLeadTime(array $task)
|
||||
{
|
||||
return ($task['date_completed'] ?: time()) - $task['date_creation'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time between date_started and date_completed or now if empty
|
||||
*
|
||||
* @access public
|
||||
* @param array $task
|
||||
* @return integer
|
||||
*/
|
||||
public function getCycleTime(array $task)
|
||||
{
|
||||
if (empty($task['date_started'])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ($task['date_completed'] ?: time()) - $task['date_started'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the average time spent in each column
|
||||
*
|
||||
* @access public
|
||||
* @param array $task
|
||||
* @return array
|
||||
*/
|
||||
public function getTimeSpentByColumn(array $task)
|
||||
{
|
||||
$result = array();
|
||||
$columns = $this->board->getColumnsList($task['project_id']);
|
||||
$sums = $this->transition->getTimeSpentByTask($task['id']);
|
||||
|
||||
foreach ($columns as $column_id => $column_title) {
|
||||
|
||||
$time_spent = isset($sums[$column_id]) ? $sums[$column_id] : 0;
|
||||
|
||||
if ($task['column_id'] == $column_id) {
|
||||
$time_spent += ($task['date_completed'] ?: time()) - $task['date_moved'];
|
||||
}
|
||||
|
||||
$result[] = array(
|
||||
'id' => $column_id,
|
||||
'title' => $column_title,
|
||||
'time_spent' => $time_spent,
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
38
sources/app/Subscriber/RecurringTaskSubscriber.php
Normal file
38
sources/app/Subscriber/RecurringTaskSubscriber.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace Subscriber;
|
||||
|
||||
use Event\TaskEvent;
|
||||
use Model\Task;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class RecurringTaskSubscriber extends \Core\Base implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return array(
|
||||
Task::EVENT_MOVE_COLUMN => array('onMove', 0),
|
||||
Task::EVENT_CLOSE => array('onClose', 0),
|
||||
);
|
||||
}
|
||||
|
||||
public function onMove(TaskEvent $event)
|
||||
{
|
||||
if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING) {
|
||||
|
||||
if ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_FIRST_COLUMN && $this->board->getFirstColumn($event['project_id']) == $event['src_column_id']) {
|
||||
$this->taskDuplication->duplicateRecurringTask($event['task_id']);
|
||||
}
|
||||
else if ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_LAST_COLUMN && $this->board->getLastColumn($event['project_id']) == $event['dst_column_id']) {
|
||||
$this->taskDuplication->duplicateRecurringTask($event['task_id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onClose(TaskEvent $event)
|
||||
{
|
||||
if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING && $event['recurrence_trigger'] == Task::RECURRING_TRIGGER_CLOSE) {
|
||||
$this->taskDuplication->duplicateRecurringTask($event['task_id']);
|
||||
}
|
||||
}
|
||||
}
|
47
sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php
Normal file
47
sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace Subscriber;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Model\Subtask;
|
||||
use Event\SubtaskEvent;
|
||||
|
||||
class SubtaskTimeTrackingSubscriber extends \Core\Base implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return array(
|
||||
Subtask::EVENT_CREATE => array('updateTaskTime', 0),
|
||||
Subtask::EVENT_UPDATE => array(
|
||||
array('logStartEnd', 10),
|
||||
array('updateTaskTime', 0),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function updateTaskTime(SubtaskEvent $event)
|
||||
{
|
||||
if (isset($event['task_id'])) {
|
||||
$this->subtaskTimeTracking->updateTaskTimeTracking($event['task_id']);
|
||||
}
|
||||
}
|
||||
|
||||
public function logStartEnd(SubtaskEvent $event)
|
||||
{
|
||||
if (isset($event['status']) && $this->config->get('subtask_time_tracking') == 1) {
|
||||
|
||||
$subtask = $this->subtask->getById($event['id']);
|
||||
|
||||
if (empty($subtask['user_id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($subtask['status'] == Subtask::STATUS_INPROGRESS) {
|
||||
return $this->subtaskTimeTracking->logStartTime($subtask['id'], $subtask['user_id']);
|
||||
}
|
||||
else {
|
||||
return $this->subtaskTimeTracking->logEndTime($subtask['id'], $subtask['user_id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
40
sources/app/Template/activity/project.php
Normal file
40
sources/app/Template/activity/project.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<section id="main">
|
||||
<div class="page-header">
|
||||
<ul>
|
||||
<li>
|
||||
<span class="dropdown">
|
||||
<span>
|
||||
<i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
|
||||
<ul>
|
||||
<?= $this->render('project/dropdown', array('project' => $project)) ?>
|
||||
</ul>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-th fa-fw"></i>
|
||||
<?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-calendar fa-fw"></i>
|
||||
<?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?>
|
||||
</li>
|
||||
<?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
|
||||
<li>
|
||||
<i class="fa fa-cog fa-fw"></i>
|
||||
<?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $project['id'])) ?>
|
||||
</li>
|
||||
<?php endif ?>
|
||||
<li>
|
||||
<i class="fa fa-folder fa-fw"></i>
|
||||
<?= $this->url->link(t('All projects'), 'project', 'index') ?>
|
||||
</li>
|
||||
<?php if ($project['is_public']): ?>
|
||||
<li><i class="fa fa-rss-square fa-fw"></i><?= $this->url->link(t('RSS feed'), 'feed', 'project', array('token' => $project['token']), false, '', '', true) ?></li>
|
||||
<li><i class="fa fa-calendar fa-fw"></i><?= $this->url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token'])) ?></li>
|
||||
<?php endif ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<?= $this->render('event/events', array('events' => $events)) ?>
|
||||
</section>
|
5
sources/app/Template/activity/task.php
Normal file
5
sources/app/Template/activity/task.php
Normal file
|
@ -0,0 +1,5 @@
|
|||
<div class="page-header">
|
||||
<h2><?= t('Activity stream') ?></h2>
|
||||
</div>
|
||||
|
||||
<?= $this->render('event/events', array('events' => $events)) ?>
|
29
sources/app/Template/analytic/avg_time_columns.php
Normal file
29
sources/app/Template/analytic/avg_time_columns.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<div class="page-header">
|
||||
<h2><?= t('Average time spent into each column') ?></h2>
|
||||
</div>
|
||||
|
||||
<?php if (empty($metrics)): ?>
|
||||
<p class="alert"><?= t('Not enough data to show the graph.') ?></p>
|
||||
<?php else: ?>
|
||||
<section id="analytic-avg-time-column">
|
||||
|
||||
<div id="chart" data-metrics='<?= json_encode($metrics, JSON_HEX_APOS) ?>' data-label="<?= t('Average time spent') ?>"></div>
|
||||
|
||||
<table class="table-stripped">
|
||||
<tr>
|
||||
<th><?= t('Column') ?></th>
|
||||
<th><?= t('Average time spent') ?></th>
|
||||
</tr>
|
||||
<?php foreach ($metrics as $column): ?>
|
||||
<tr>
|
||||
<td><?= $this->e($column['title']) ?></td>
|
||||
<td><?= $this->dt->duration($column['average']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
|
||||
<p class="alert alert-info">
|
||||
<?= t('This chart show the average time spent into each column for the last %d tasks.', 1000) ?>
|
||||
</p>
|
||||
</section>
|
||||
<?php endif ?>
|
34
sources/app/Template/analytic/burndown.php
Normal file
34
sources/app/Template/analytic/burndown.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<div class="page-header">
|
||||
<h2><?= t('Burndown chart') ?></h2>
|
||||
</div>
|
||||
|
||||
<?php if (! $display_graph): ?>
|
||||
<p class="alert"><?= t('You need at least 2 days of data to show the chart.') ?></p>
|
||||
<?php else: ?>
|
||||
<section id="analytic-burndown">
|
||||
<div id="chart" data-metrics='<?= json_encode($metrics, JSON_HEX_APOS) ?>' data-date-format="<?= e('%%Y-%%m-%%d') ?>" data-label-total="<?= t('Total for all columns') ?>"></div>
|
||||
</section>
|
||||
<?php endif ?>
|
||||
|
||||
<hr/>
|
||||
|
||||
<form method="post" class="form-inline" action="<?= $this->url->href('analytic', 'burndown', array('project_id' => $project['id'])) ?>" autocomplete="off">
|
||||
|
||||
<?= $this->form->csrf() ?>
|
||||
|
||||
<div class="form-inline-group">
|
||||
<?= $this->form->label(t('Start Date'), 'from') ?>
|
||||
<?= $this->form->text('from', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
|
||||
</div>
|
||||
|
||||
<div class="form-inline-group">
|
||||
<?= $this->form->label(t('End Date'), 'to') ?>
|
||||
<?= $this->form->text('to', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
|
||||
</div>
|
||||
|
||||
<div class="form-inline-group">
|
||||
<input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="alert alert-info"><?= t('This chart show the task complexity over the time (Work Remaining).') ?></p>
|
42
sources/app/Template/analytic/lead_cycle_time.php
Normal file
42
sources/app/Template/analytic/lead_cycle_time.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<div class="page-header">
|
||||
<h2><?= t('Average Lead and Cycle time') ?></h2>
|
||||
</div>
|
||||
|
||||
<div class="listing">
|
||||
<ul>
|
||||
<li><?= t('Average lead time: ').'<strong>'.$this->dt->duration($average['avg_lead_time']) ?></strong></li>
|
||||
<li><?= t('Average cycle time: ').'<strong>'.$this->dt->duration($average['avg_cycle_time']) ?></strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<?php if (empty($metrics)): ?>
|
||||
<p class="alert"><?= t('Not enough data to show the graph.') ?></p>
|
||||
<?php else: ?>
|
||||
<section id="analytic-lead-cycle-time">
|
||||
|
||||
<div id="chart" data-metrics='<?= json_encode($metrics, JSON_HEX_APOS) ?>' data-label-cycle="<?= t('Cycle Time') ?>" data-label-lead="<?= t('Lead Time') ?>"></div>
|
||||
|
||||
<form method="post" class="form-inline" action="<?= $this->url->href('analytic', 'leadAndCycleTime', array('project_id' => $project['id'])) ?>" autocomplete="off">
|
||||
|
||||
<?= $this->form->csrf() ?>
|
||||
|
||||
<div class="form-inline-group">
|
||||
<?= $this->form->label(t('Start Date'), 'from') ?>
|
||||
<?= $this->form->text('from', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
|
||||
</div>
|
||||
|
||||
<div class="form-inline-group">
|
||||
<?= $this->form->label(t('End Date'), 'to') ?>
|
||||
<?= $this->form->text('to', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
|
||||
</div>
|
||||
|
||||
<div class="form-inline-group">
|
||||
<input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="alert alert-info">
|
||||
<?= t('This chart show the average lead and cycle time for the last %d tasks over the time.', 1000) ?>
|
||||
</p>
|
||||
</section>
|
||||
<?php endif ?>
|
4
sources/app/Template/app/activity.php
Normal file
4
sources/app/Template/app/activity.php
Normal file
|
@ -0,0 +1,4 @@
|
|||
<div class="page-header">
|
||||
<h2><?= t('My activity stream') ?></h2>
|
||||
</div>
|
||||
<?= $this->render('event/events', array('events' => $events)) ?>
|
5
sources/app/Template/app/calendar.php
Normal file
5
sources/app/Template/app/calendar.php
Normal file
|
@ -0,0 +1,5 @@
|
|||
<div id="calendar"
|
||||
data-check-url="<?= $this->url->href('calendar', 'user', array('user_id' => $user['id'])) ?>"
|
||||
data-save-url="<?= $this->url->href('calendar', 'save') ?>"
|
||||
>
|
||||
</div>
|
18
sources/app/Template/app/filters_helper.php
Normal file
18
sources/app/Template/app/filters_helper.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<div class="dropdown filters">
|
||||
<i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Filters') ?></a>
|
||||
<ul>
|
||||
<li><a href="#" class="filter-helper" data-filter="<?= isset($reset) ? $reset : '' ?>" title="<?= t('Keyboard shortcut: "%s"', 'r') ?>"><?= t('Reset filters') ?></a></li>
|
||||
<li><a href="#" class="filter-helper" data-filter="status:open assignee:me"><?= t('My tasks') ?></a></li>
|
||||
<li><a href="#" class="filter-helper" data-filter="status:open assignee:me due:tomorrow"><?= t('My tasks due tomorrow') ?></a></li>
|
||||
<li><a href="#" class="filter-helper" data-filter="status:open due:today"><?= t('Tasks due today') ?></a></li>
|
||||
<li><a href="#" class="filter-helper" data-filter="status:open due:tomorrow"><?= t('Tasks due tomorrow') ?></a></li>
|
||||
<li><a href="#" class="filter-helper" data-filter="status:open due:yesterday"><?= t('Tasks due yesterday') ?></a></li>
|
||||
<li><a href="#" class="filter-helper" data-filter="status:closed"><?= t('Closed tasks') ?></a></li>
|
||||
<li><a href="#" class="filter-helper" data-filter="status:open"><?= t('Open tasks') ?></a></li>
|
||||
<li><a href="#" class="filter-helper" data-filter="status:open assignee:nobody"><?= t('Not assigned') ?></a></li>
|
||||
<li><a href="#" class="filter-helper" data-filter="status:open category:none"><?= t('No category') ?></a></li>
|
||||
<li>
|
||||
<?= $this->url->doc(t('View advanced search syntax'), 'search') ?>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
40
sources/app/Template/app/layout.php
Normal file
40
sources/app/Template/app/layout.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<section id="main">
|
||||
<div class="page-header page-header-mobile">
|
||||
<ul>
|
||||
<?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
|
||||
<li>
|
||||
<i class="fa fa-plus fa-fw"></i>
|
||||
<?= $this->url->link(t('New project'), 'project', 'create') ?>
|
||||
</li>
|
||||
<?php endif ?>
|
||||
<li>
|
||||
<i class="fa fa-lock fa-fw"></i>
|
||||
<?= $this->url->link(t('New private project'), 'project', 'create', array('private' => 1)) ?>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-search fa-fw"></i>
|
||||
<?= $this->url->link(t('Search'), 'search', 'index') ?>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-folder fa-fw"></i>
|
||||
<?= $this->url->link(t('Project management'), 'project', 'index') ?>
|
||||
</li>
|
||||
<?php if ($this->user->isAdmin()): ?>
|
||||
<li>
|
||||
<i class="fa fa-user fa-fw"></i>
|
||||
<?= $this->url->link(t('User management'), 'user', 'index') ?>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-cog fa-fw"></i>
|
||||
<?= $this->url->link(t('Settings'), 'config', 'index') ?>
|
||||
</li>
|
||||
<?php endif ?>
|
||||
</ul>
|
||||
</div>
|
||||
<section class="sidebar-container" id="dashboard">
|
||||
<?= $this->render('app/sidebar', array('user' => $user)) ?>
|
||||
<div class="sidebar-content">
|
||||
<?= $content_for_sublayout ?>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
13
sources/app/Template/app/overview.php
Normal file
13
sources/app/Template/app/overview.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<div class="search">
|
||||
<form method="get" action="<?= $this->url->dir() ?>" class="search">
|
||||
<?= $this->form->hidden('controller', array('controller' => 'search')) ?>
|
||||
<?= $this->form->hidden('action', array('action' => 'index')) ?>
|
||||
<?= $this->form->text('search', array(), array(), array('placeholder="'.t('Search').'"'), 'form-input-large') ?>
|
||||
</form>
|
||||
|
||||
<?= $this->render('app/filters_helper') ?>
|
||||
</div>
|
||||
|
||||
<?= $this->render('app/projects', array('paginator' => $project_paginator)) ?>
|
||||
<?= $this->render('app/tasks', array('paginator' => $task_paginator)) ?>
|
||||
<?= $this->render('app/subtasks', array('paginator' => $subtask_paginator)) ?>
|
25
sources/app/Template/app/sidebar.php
Normal file
25
sources/app/Template/app/sidebar.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<div class="sidebar">
|
||||
<h2><?= $this->e($user['name'] ?: $user['username']) ?></h2>
|
||||
<ul>
|
||||
<li <?= $this->app->getRouterAction() === 'index' ? 'class="active"' : '' ?>>
|
||||
<?= $this->url->link(t('Overview'), 'app', 'index', array('user_id' => $user['id'])) ?>
|
||||
</li>
|
||||
<li <?= $this->app->getRouterAction() === 'projects' ? 'class="active"' : '' ?>>
|
||||
<?= $this->url->link(t('My projects'), 'app', 'projects', array('user_id' => $user['id'])) ?>
|
||||
</li>
|
||||
<li <?= $this->app->getRouterAction() === 'tasks' ? 'class="active"' : '' ?>>
|
||||
<?= $this->url->link(t('My tasks'), 'app', 'tasks', array('user_id' => $user['id'])) ?>
|
||||
</li>
|
||||
<li <?= $this->app->getRouterAction() === 'subtasks' ? 'class="active"' : '' ?>>
|
||||
<?= $this->url->link(t('My subtasks'), 'app', 'subtasks', array('user_id' => $user['id'])) ?>
|
||||
</li>
|
||||
<li <?= $this->app->getRouterAction() === 'calendar' ? 'class="active"' : '' ?>>
|
||||
<?= $this->url->link(t('My calendar'), 'app', 'calendar', array('user_id' => $user['id'])) ?>
|
||||
</li>
|
||||
<li <?= $this->app->getRouterAction() === 'activity' ? 'class="active"' : '' ?>>
|
||||
<?= $this->url->link(t('My activity stream'), 'app', 'activity', array('user_id' => $user['id'])) ?>
|
||||
</li>
|
||||
</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>
|
||||
</div>
|
50
sources/app/Template/auth/index.php
Normal file
50
sources/app/Template/auth/index.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<div class="form-login">
|
||||
|
||||
<?php if (isset($errors['login'])): ?>
|
||||
<p class="alert alert-error"><?= $this->e($errors['login']) ?></p>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (! HIDE_LOGIN_FORM): ?>
|
||||
<form method="post" action="<?= $this->url->href('auth', 'check') ?>">
|
||||
|
||||
<?= $this->form->csrf() ?>
|
||||
|
||||
<?= $this->form->label(t('Username'), 'username') ?>
|
||||
<?= $this->form->text('username', $values, $errors, array('autofocus', 'required')) ?>
|
||||
|
||||
<?= $this->form->label(t('Password'), 'password') ?>
|
||||
<?= $this->form->password('password', $values, $errors, array('required')) ?>
|
||||
|
||||
<?php if (isset($captcha) && $captcha): ?>
|
||||
<?= $this->form->label(t('Enter the text below'), 'captcha') ?>
|
||||
<img src="<?= $this->url->href('auth', 'captcha') ?>"/>
|
||||
<?= $this->form->text('captcha', $values, $errors, array('required')) ?>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (REMEMBER_ME_AUTH): ?>
|
||||
<?= $this->form->checkbox('remember_me', t('Remember Me'), 1, true) ?><br/>
|
||||
<?php endif ?>
|
||||
|
||||
<div class="form-actions">
|
||||
<input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (GOOGLE_AUTH || GITHUB_AUTH || GITLAB_AUTH): ?>
|
||||
<ul class="no-bullet">
|
||||
<?php if (GOOGLE_AUTH): ?>
|
||||
<li><?= $this->url->link(t('Login with my Google Account'), 'oauth', 'google') ?></li>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (GITHUB_AUTH): ?>
|
||||
<li><?= $this->url->link(t('Login with my Github Account'), 'oauth', 'github') ?></li>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (GITLAB_AUTH): ?>
|
||||
<li><?= $this->url->link(t('Login with my Gitlab Account'), 'oauth', 'gitlab') ?></li>
|
||||
<?php endif ?>
|
||||
</ul>
|
||||
<?php endif ?>
|
||||
|
||||
</div>
|
21
sources/app/Template/board/popover_assignee.php
Normal file
21
sources/app/Template/board/popover_assignee.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<section id="main">
|
||||
<section>
|
||||
<h3><?= t('Change assignee for the task "%s"', $values['title']) ?></h3>
|
||||
<form method="post" action="<?= $this->url->href('board', 'updateAssignee', array('task_id' => $values['id'], 'project_id' => $values['project_id'])) ?>">
|
||||
|
||||
<?= $this->form->csrf() ?>
|
||||
|
||||
<?= $this->form->hidden('id', $values) ?>
|
||||
<?= $this->form->hidden('project_id', $values) ?>
|
||||
|
||||
<?= $this->form->label(t('Assignee'), 'owner_id') ?>
|
||||
<?= $this->form->select('owner_id', $users_list, $values, array(), array('autofocus')) ?><br/>
|
||||
|
||||
<div class="form-actions">
|
||||
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
|
||||
<?= t('or') ?>
|
||||
<?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
21
sources/app/Template/board/popover_category.php
Normal file
21
sources/app/Template/board/popover_category.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<section id="main">
|
||||
<section>
|
||||
<h3><?= t('Change category for the task "%s"', $values['title']) ?></h3>
|
||||
<form method="post" action="<?= $this->url->href('board', 'updateCategory', array('task_id' => $values['id'], 'project_id' => $values['project_id'])) ?>">
|
||||
|
||||
<?= $this->form->csrf() ?>
|
||||
|
||||
<?= $this->form->hidden('id', $values) ?>
|
||||
<?= $this->form->hidden('project_id', $values) ?>
|
||||
|
||||
<?= $this->form->label(t('Category'), 'category_id') ?>
|
||||
<?= $this->form->select('category_id', $categories_list, $values, array(), array('autofocus')) ?><br/>
|
||||
|
||||
<div class="form-actions">
|
||||
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
|
||||
<?= t('or') ?>
|
||||
<?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
18
sources/app/Template/board/private_view.php
Normal file
18
sources/app/Template/board/private_view.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<section id="main">
|
||||
|
||||
<?= $this->render('project/filters', array(
|
||||
'project' => $project,
|
||||
'filters' => $filters,
|
||||
'categories_list' => $categories_list,
|
||||
'users_list' => $users_list,
|
||||
'is_board' => true,
|
||||
)) ?>
|
||||
|
||||
<?= $this->render('board/table_container', array(
|
||||
'project' => $project,
|
||||
'swimlanes' => $swimlanes,
|
||||
'board_private_refresh_interval' => $board_private_refresh_interval,
|
||||
'board_highlight_period' => $board_highlight_period,
|
||||
)) ?>
|
||||
|
||||
</section>
|
11
sources/app/Template/board/public_view.php
Normal file
11
sources/app/Template/board/public_view.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<section id="main" class="public-board">
|
||||
|
||||
<?= $this->render('board/table_container', array(
|
||||
'project' => $project,
|
||||
'swimlanes' => $swimlanes,
|
||||
'board_private_refresh_interval' => $board_private_refresh_interval,
|
||||
'board_highlight_period' => $board_highlight_period,
|
||||
'not_editable' => true,
|
||||
)) ?>
|
||||
|
||||
</section>
|
31
sources/app/Template/board/table_container.php
Normal file
31
sources/app/Template/board/table_container.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<div id="board-container">
|
||||
<?php if (isset($not_editable)): ?>
|
||||
<table id="board" class="board-project-<?= $project['id'] ?>">
|
||||
<?php else: ?>
|
||||
<table id="board"
|
||||
class="board-project-<?= $project['id'] ?>"
|
||||
data-project-id="<?= $project['id'] ?>"
|
||||
data-check-interval="<?= $board_private_refresh_interval ?>"
|
||||
data-save-url="<?= $this->url->href('board', 'save', array('project_id' => $project['id'])) ?>"
|
||||
data-reload-url="<?= $this->url->href('board', 'reload', array('project_id' => $project['id'])) ?>"
|
||||
data-check-url="<?= $this->url->href('board', 'check', array('project_id' => $project['id'], 'timestamp' => time())) ?>"
|
||||
data-task-creation-url="<?= $this->url->href('taskcreation', 'create', array('project_id' => $project['id'])) ?>"
|
||||
>
|
||||
<?php endif ?>
|
||||
|
||||
<?php foreach ($swimlanes as $swimlane): ?>
|
||||
<?php if (empty($swimlane['columns'])): ?>
|
||||
<p class="alert alert-error"><?= t('There is no column in your project!') ?></p>
|
||||
<?php break ?>
|
||||
<?php else: ?>
|
||||
<?= $this->render('board/table_swimlane', array(
|
||||
'project' => $project,
|
||||
'swimlane' => $swimlane,
|
||||
'board_highlight_period' => $board_highlight_period,
|
||||
'hide_swimlane' => count($swimlanes) === 1,
|
||||
'not_editable' => isset($not_editable),
|
||||
)) ?>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
</div>
|
94
sources/app/Template/board/table_swimlane.php
Normal file
94
sources/app/Template/board/table_swimlane.php
Normal file
|
@ -0,0 +1,94 @@
|
|||
<tr id="swimlane-<?= $swimlane['id'] ?>">
|
||||
<!-- swimlane toggle -->
|
||||
<?php if (! $hide_swimlane): ?>
|
||||
<th class="board-swimlane-header">
|
||||
<?php if (! $not_editable): ?>
|
||||
<a href="#" class="board-swimlane-toggle" data-swimlane-id="<?= $swimlane['id'] ?>">
|
||||
<i class="fa fa-minus-circle hide-icon-swimlane-<?= $swimlane['id'] ?>"></i>
|
||||
<i class="fa fa-plus-circle show-icon-swimlane-<?= $swimlane['id'] ?>" style="display: none"></i>
|
||||
</a>
|
||||
<span class="board-swimlane-toggle-title show-icon-swimlane-<?= $swimlane['id'] ?>"><?= $this->e($swimlane['name']) ?></span>
|
||||
<?php endif ?>
|
||||
</th>
|
||||
<?php endif ?>
|
||||
|
||||
<!-- column header title -->
|
||||
<?php foreach ($swimlane['columns'] as $column): ?>
|
||||
<th class="board-column-header board-column-header-<?= $column['id'] ?>" data-column-id="<?= $column['id'] ?>">
|
||||
<div class="board-column-collapsed">
|
||||
<span title="<?= t('Task count') ?>" class="board-column-header-task-count" title="<?= t('Show this column') ?>">
|
||||
<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="board-column-expanded">
|
||||
<?php if (! $not_editable): ?>
|
||||
<div class="board-add-icon">
|
||||
<?= $this->url->link('+', 'taskcreation', 'create', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'popover', t('Add a new task')) ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<span class="board-column-title" data-column-id="<?= $column['id'] ?>" title="<?= t('Hide this column') ?>">
|
||||
<?= $this->e($column['title']) ?>
|
||||
</span>
|
||||
|
||||
<?php if (! $not_editable && ! empty($column['description'])): ?>
|
||||
<span class="tooltip pull-right" title='<?= $this->e($this->text->markdown($column['description'])) ?>'>
|
||||
<i class="fa fa-info-circle"></i>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (! empty($column['score'])): ?>
|
||||
<span class="pull-right" title="<?= t('Score') ?>">
|
||||
<?= $column['score'] ?>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ($column['task_limit']): ?>
|
||||
<span title="<?= t('Task limit') ?>">
|
||||
(<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>/<?= $this->e($column['task_limit']) ?>)
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span title="<?= t('Task count') ?>" class="board-column-header-task-count">
|
||||
(<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>)
|
||||
</span>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</th>
|
||||
<?php endforeach ?>
|
||||
</tr>
|
||||
<tr class="board-swimlane swimlane-row-<?= $swimlane['id'] ?>">
|
||||
|
||||
<!-- swimlane title -->
|
||||
<?php if (! $hide_swimlane): ?>
|
||||
<th class="board-swimlane-title">
|
||||
<?= $this->e($swimlane['name']) ?>
|
||||
|
||||
<div title="<?= t('Task count') ?>" class="board-column-header-task-count">
|
||||
(<span><?= $swimlane['nb_tasks'] ?></span>)
|
||||
</div>
|
||||
</th>
|
||||
<?php endif ?>
|
||||
|
||||
<!-- task list -->
|
||||
<?php foreach ($swimlane['columns'] as $column): ?>
|
||||
<td class="board-column-<?= $column['id'] ?> <?= $column['task_limit'] && $column['nb_tasks'] > $column['task_limit'] ? 'board-task-list-limit' : '' ?>">
|
||||
<div class="board-task-list board-column-expanded" data-column-id="<?= $column['id'] ?>" data-swimlane-id="<?= $swimlane['id'] ?>" data-task-limit="<?= $column['task_limit'] ?>">
|
||||
<?php foreach ($column['tasks'] as $task): ?>
|
||||
<?= $this->render($not_editable ? 'board/task_public' : 'board/task_private', array(
|
||||
'project' => $project,
|
||||
'task' => $task,
|
||||
'board_highlight_period' => $board_highlight_period,
|
||||
'not_editable' => $not_editable,
|
||||
)) ?>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<div class="board-column-collapsed">
|
||||
<div class="board-rotation-wrapper">
|
||||
<div class="board-column-title board-rotation" data-column-id="<?= $column['id'] ?>" title="<?= t('Show this column') ?>">
|
||||
<?= $this->e($column['title']) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<?php endforeach ?>
|
||||
</tr>
|
16
sources/app/Template/board/tooltip_comments.php
Normal file
16
sources/app/Template/board/tooltip_comments.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<section>
|
||||
<?php foreach ($comments as $comment): ?>
|
||||
<p class="comment-title">
|
||||
<?php if (! empty($comment['username'])): ?>
|
||||
<span class="comment-username"><?= $this->e($comment['name'] ?: $comment['username']) ?></span> @
|
||||
<?php endif ?>
|
||||
<span class="comment-date"><?= dt('%b %e, %Y, %k:%M %p', $comment['date_creation']) ?></span>
|
||||
</p>
|
||||
|
||||
<div class="comment-inner">
|
||||
<div class="markdown">
|
||||
<?= $this->text->markdown($comment['comment']) ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
</section>
|
5
sources/app/Template/board/tooltip_description.php
Normal file
5
sources/app/Template/board/tooltip_description.php
Normal file
|
@ -0,0 +1,5 @@
|
|||
<section class="tooltip-large">
|
||||
<div class="markdown">
|
||||
<?= $this->text->markdown($task['description']) ?>
|
||||
</div>
|
||||
</section>
|
18
sources/app/Template/board/tooltip_files.php
Normal file
18
sources/app/Template/board/tooltip_files.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<table class="table-small">
|
||||
<?php foreach($files as $file): ?>
|
||||
<tr>
|
||||
<th>
|
||||
<i class="fa <?= $this->file->icon($file['name']) ?> fa-fw"></i>
|
||||
<?= $this->e($file['name']) ?>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<i class="fa fa-download fa-fw"></i><?= $this->url->link(t('download'), 'file', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?>
|
||||
<?php if ($file['is_image'] == 1): ?>
|
||||
<i class="fa fa-eye"></i> <?= $this->url->link(t('open file'), 'file', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
7
sources/app/Template/board/tooltip_subtasks.php
Normal file
7
sources/app/Template/board/tooltip_subtasks.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<section id="tooltip-subtasks">
|
||||
<?php foreach ($subtasks as $subtask): ?>
|
||||
<?= $this->subtask->toggleStatus($subtask, 'board') ?>
|
||||
<?= $this->e(empty($subtask['username']) ? '' : ' ['.$this->user->getFullname($subtask).']') ?>
|
||||
<br/>
|
||||
<?php endforeach ?>
|
||||
</section>
|
18
sources/app/Template/board/tooltip_tasklinks.php
Normal file
18
sources/app/Template/board/tooltip_tasklinks.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<div class="tooltip-tasklinks">
|
||||
<ul>
|
||||
<?php foreach($links as $link): ?>
|
||||
<li>
|
||||
<strong><?= t($link['label']) ?></strong>
|
||||
<?= $this->url->link(
|
||||
$this->e('#'.$link['task_id'].' - '.$link['title']),
|
||||
'task', 'show', array('task_id' => $link['task_id'], 'project_id' => $link['project_id']),
|
||||
false,
|
||||
$link['is_active'] ? '' : 'task-link-closed'
|
||||
) ?>
|
||||
<?php if (! empty($link['task_assignee_username'])): ?>
|
||||
[<?= $this->e($link['task_assignee_name'] ?: $link['task_assignee_username']) ?>]
|
||||
<?php endif ?>
|
||||
</li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
</div>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue