diff --git a/sources/app/Action/TaskClose.php b/sources/app/Action/TaskClose.php
index 760dfd8..b7cd4db 100644
--- a/sources/app/Action/TaskClose.php
+++ b/sources/app/Action/TaskClose.php
@@ -4,6 +4,7 @@ namespace Action;
use Integration\GitlabWebhook;
use Integration\GithubWebhook;
+use Integration\BitbucketWebhook;
use Model\Task;
/**
@@ -28,6 +29,7 @@ class TaskClose extends Base
GithubWebhook::EVENT_ISSUE_CLOSED,
GitlabWebhook::EVENT_COMMIT,
GitlabWebhook::EVENT_ISSUE_CLOSED,
+ BitbucketWebhook::EVENT_COMMIT,
);
}
@@ -44,6 +46,7 @@ class TaskClose extends Base
case GithubWebhook::EVENT_ISSUE_CLOSED:
case GitlabWebhook::EVENT_COMMIT:
case GitlabWebhook::EVENT_ISSUE_CLOSED:
+ case BitbucketWebhook::EVENT_COMMIT:
return array();
default:
return array('column_id' => t('Column'));
@@ -63,6 +66,7 @@ class TaskClose extends Base
case GithubWebhook::EVENT_ISSUE_CLOSED:
case GitlabWebhook::EVENT_COMMIT:
case GitlabWebhook::EVENT_ISSUE_CLOSED:
+ case BitbucketWebhook::EVENT_COMMIT:
return array('task_id');
default:
return array('task_id', 'column_id');
@@ -95,6 +99,7 @@ class TaskClose extends Base
case GithubWebhook::EVENT_ISSUE_CLOSED:
case GitlabWebhook::EVENT_COMMIT:
case GitlabWebhook::EVENT_ISSUE_CLOSED:
+ case BitbucketWebhook::EVENT_COMMIT:
return true;
default:
return $data['column_id'] == $this->getParam('column_id');
diff --git a/sources/app/Action/TaskLogMoveAnotherColumn.php b/sources/app/Action/TaskLogMoveAnotherColumn.php
new file mode 100644
index 0000000..621e8e6
--- /dev/null
+++ b/sources/app/Action/TaskLogMoveAnotherColumn.php
@@ -0,0 +1,84 @@
+ 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 (append to the task description).
+ *
+ * @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)
+ {
+ if (! $this->userSession->isLogged()) {
+ return false;
+ }
+
+ $column = $this->board->getColumn($data['column_id']);
+
+ return (bool) $this->comment->create(array(
+ 'comment' => t('Moved to column %s', $column['title']),
+ 'task_id' => $data['task_id'],
+ 'user_id' => $this->userSession->getId(),
+ ));
+ }
+
+ /**
+ * 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');
+ }
+}
diff --git a/sources/app/Auth/Database.php b/sources/app/Auth/Database.php
index 2804b9a..e69f18a 100644
--- a/sources/app/Auth/Database.php
+++ b/sources/app/Auth/Database.php
@@ -30,9 +30,14 @@ class Database extends Base
*/
public function authenticate($username, $password)
{
- $user = $this->db->table(User::TABLE)->eq('username', $username)->eq('is_ldap_user', 0)->findOne();
+ $user = $this->db
+ ->table(User::TABLE)
+ ->eq('username', $username)
+ ->eq('disable_login_form', 0)
+ ->eq('is_ldap_user', 0)
+ ->findOne();
- if ($user && password_verify($password, $user['password'])) {
+ if (is_array($user) && password_verify($password, $user['password'])) {
$this->userSession->refresh($user);
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
return true;
diff --git a/sources/app/Auth/Ldap.php b/sources/app/Auth/Ldap.php
index b344061..376d16f 100644
--- a/sources/app/Auth/Ldap.php
+++ b/sources/app/Auth/Ldap.php
@@ -29,6 +29,7 @@ class Ldap extends Base
*/
public function authenticate($username, $password)
{
+ $username = LDAP_USERNAME_CASE_SENSITIVE ? $username : strtolower($username);
$result = $this->findUser($username, $password);
if (is_array($result)) {
@@ -199,11 +200,90 @@ class Ldap extends Base
return array(
'username' => $username,
- 'name' => isset($info[0][LDAP_ACCOUNT_FULLNAME][0]) ? $info[0][LDAP_ACCOUNT_FULLNAME][0] : '',
- 'email' => isset($info[0][LDAP_ACCOUNT_EMAIL][0]) ? $info[0][LDAP_ACCOUNT_EMAIL][0] : '',
+ 'name' => $this->getFromInfo($info, LDAP_ACCOUNT_FULLNAME),
+ 'email' => $this->getFromInfo($info, LDAP_ACCOUNT_EMAIL),
);
}
return false;
}
+
+ /**
+ * Retrieve info on LDAP user
+ *
+ * @param string $username Username
+ * @param string $email Email address
+ */
+ public function lookup($username = null, $email = null)
+ {
+ $query = $this->getQuery($username, $email);
+ if ($query === false) {
+ return false;
+ }
+
+ // Connect and attempt anonymous bind
+ $ldap = $this->connect();
+ if (! is_resource($ldap) || ! $this->bind($ldap, null, null)) {
+ return false;
+ }
+
+ // Try to find user
+ $sr = @ldap_search($ldap, LDAP_ACCOUNT_BASE, $query, array(LDAP_ACCOUNT_FULLNAME, LDAP_ACCOUNT_EMAIL, LDAP_ACCOUNT_ID));
+ if ($sr === false) {
+ return false;
+ }
+
+ $info = ldap_get_entries($ldap, $sr);
+
+ // User not found
+ if (count($info) == 0 || $info['count'] == 0) {
+ return false;
+ }
+
+ // User id not retrieved: LDAP_ACCOUNT_ID not properly configured
+ if (! $username && ! isset($info[0][LDAP_ACCOUNT_ID][0])) {
+ return false;
+ }
+
+ return array(
+ 'username' => $this->getFromInfo($info, LDAP_ACCOUNT_ID, $username),
+ 'name' => $this->getFromInfo($info, LDAP_ACCOUNT_FULLNAME),
+ 'email' => $this->getFromInfo($info, LDAP_ACCOUNT_EMAIL, $email),
+ );
+ }
+
+ /**
+ * Get the LDAP query to find a user
+ *
+ * @param string $username Username
+ * @param string $email Email address
+ */
+ private function getQuery($username, $email)
+ {
+ if ($username && $email) {
+ return '(&('.sprintf(LDAP_USER_PATTERN, $username).')('.sprintf(LDAP_ACCOUNT_EMAIL, $email).')';
+ }
+ else if ($username) {
+ return sprintf(LDAP_USER_PATTERN, $username);
+ }
+ else if ($email) {
+ return '('.LDAP_ACCOUNT_EMAIL.'='.$email.')';
+ }
+ else {
+ return false;
+ }
+ }
+
+ /**
+ * Return a value from the LDAP info
+ *
+ * @param array $info LDAP info
+ * @param string $key Key
+ * @param string $default Default value if key not set in entry
+ * @return string
+ */
+ private function getFromInfo($info, $key, $default = '')
+ {
+ return isset($info[0][$key][0]) ? $info[0][$key][0] : $default;
+ }
}
diff --git a/sources/app/Auth/ReverseProxy.php b/sources/app/Auth/ReverseProxy.php
index b84550c..6cd01b2 100644
--- a/sources/app/Auth/ReverseProxy.php
+++ b/sources/app/Auth/ReverseProxy.php
@@ -66,6 +66,7 @@ class ReverseProxy extends Base
'username' => $login,
'is_admin' => REVERSE_PROXY_DEFAULT_ADMIN === $login,
'is_ldap_user' => 1,
+ 'disable_login_form' => 1,
));
}
}
diff --git a/sources/app/Controller/App.php b/sources/app/Controller/App.php
index aa2673a..46731e7 100644
--- a/sources/app/Controller/App.php
+++ b/sources/app/Controller/App.php
@@ -2,7 +2,8 @@
namespace Controller;
-use Model\SubTask as SubTaskModel;
+use Model\Subtask as SubtaskModel;
+use Model\Task as TaskModel;
/**
* Application controller
@@ -22,164 +23,62 @@ class App extends Base
$this->response->text('OK');
}
+ /**
+ * User dashboard view for admins
+ *
+ * @access public
+ */
+ public function dashboard()
+ {
+ $this->index($this->request->getIntegerParam('user_id'), 'dashboard');
+ }
+
/**
* Dashboard for the current user
*
* @access public
*/
- public function index()
+ public function index($user_id = 0, $action = 'index')
{
- $paginate = $this->request->getStringParam('paginate', 'userTasks');
- $offset = $this->request->getIntegerParam('offset', 0);
- $direction = $this->request->getStringParam('direction');
- $order = $this->request->getStringParam('order');
-
- $user_id = $this->userSession->getId();
- $projects = $this->projectPermission->getMemberProjects($user_id);
+ $status = array(SubTaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS);
+ $user_id = $user_id ?: $this->userSession->getId();
+ $projects = $this->projectPermission->getActiveMemberProjects($user_id);
$project_ids = array_keys($projects);
- $params = array(
+ $task_paginator = $this->paginator
+ ->setUrl('app', $action, array('pagination' => 'tasks'))
+ ->setMax(10)
+ ->setOrder('tasks.id')
+ ->setQuery($this->taskFinder->getUserQuery($user_id))
+ ->calculateOnlyIf($this->request->getStringParam('pagination') === 'tasks');
+
+ $subtask_paginator = $this->paginator
+ ->setUrl('app', $action, array('pagination' => 'subtasks'))
+ ->setMax(10)
+ ->setOrder('tasks.id')
+ ->setQuery($this->subtask->getUserQuery($user_id, $status))
+ ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks');
+
+ $project_paginator = $this->paginator
+ ->setUrl('app', $action, array('pagination' => 'projects'))
+ ->setMax(10)
+ ->setOrder('name')
+ ->setQuery($this->project->getQueryColumnStats($project_ids))
+ ->calculateOnlyIf($this->request->getStringParam('pagination') === 'projects');
+
+ $this->response->html($this->template->layout('app/dashboard', array(
'title' => t('Dashboard'),
'board_selector' => $this->projectPermission->getAllowedProjects($user_id),
- 'events' => $this->projectActivity->getProjects($project_ids, 10),
- );
-
- $params += $this->getTaskPagination($user_id, $paginate, $offset, $order, $direction);
- $params += $this->getSubtaskPagination($user_id, $paginate, $offset, $order, $direction);
- $params += $this->getProjectPagination($project_ids, $paginate, $offset, $order, $direction);
-
- $this->response->html($this->template->layout('app/dashboard', $params));
+ 'events' => $this->projectActivity->getProjects($project_ids, 5),
+ 'task_paginator' => $task_paginator,
+ 'subtask_paginator' => $subtask_paginator,
+ 'project_paginator' => $project_paginator,
+ 'user_id' => $user_id,
+ )));
}
/**
- * Get tasks pagination
- *
- * @access public
- * @param integer $user_id
- * @param string $paginate
- * @param integer $offset
- * @param string $order
- * @param string $direction
- */
- private function getTaskPagination($user_id, $paginate, $offset, $order, $direction)
- {
- $limit = 10;
-
- if (! in_array($order, array('tasks.id', 'project_name', 'title', 'date_due'))) {
- $order = 'tasks.id';
- $direction = 'ASC';
- }
-
- if ($paginate === 'userTasks') {
- $tasks = $this->taskPaginator->userTasks($user_id, $offset, $limit, $order, $direction);
- }
- else {
- $offset = 0;
- $tasks = $this->taskPaginator->userTasks($user_id, $offset, $limit);
- }
-
- return array(
- 'tasks' => $tasks,
- 'task_pagination' => array(
- 'controller' => 'app',
- 'action' => 'index',
- 'params' => array('paginate' => 'userTasks'),
- 'direction' => $direction,
- 'order' => $order,
- 'total' => $this->taskPaginator->countUserTasks($user_id),
- 'offset' => $offset,
- 'limit' => $limit,
- )
- );
- }
-
- /**
- * Get subtasks pagination
- *
- * @access public
- * @param integer $user_id
- * @param string $paginate
- * @param integer $offset
- * @param string $order
- * @param string $direction
- */
- private function getSubtaskPagination($user_id, $paginate, $offset, $order, $direction)
- {
- $status = array(SubTaskModel::STATUS_TODO, SubTaskModel::STATUS_INPROGRESS);
- $limit = 10;
-
- if (! in_array($order, array('tasks.id', 'project_name', 'status', 'title'))) {
- $order = 'tasks.id';
- $direction = 'ASC';
- }
-
- if ($paginate === 'userSubtasks') {
- $subtasks = $this->subtaskPaginator->userSubtasks($user_id, $status, $offset, $limit, $order, $direction);
- }
- else {
- $offset = 0;
- $subtasks = $this->subtaskPaginator->userSubtasks($user_id, $status, $offset, $limit);
- }
-
- return array(
- 'subtasks' => $subtasks,
- 'subtask_pagination' => array(
- 'controller' => 'app',
- 'action' => 'index',
- 'params' => array('paginate' => 'userSubtasks'),
- 'direction' => $direction,
- 'order' => $order,
- 'total' => $this->subtaskPaginator->countUserSubtasks($user_id, $status),
- 'offset' => $offset,
- 'limit' => $limit,
- )
- );
- }
-
- /**
- * Get projects pagination
- *
- * @access public
- * @param array $project_ids
- * @param string $paginate
- * @param integer $offset
- * @param string $order
- * @param string $direction
- */
- private function getProjectPagination(array $project_ids, $paginate, $offset, $order, $direction)
- {
- $limit = 10;
-
- if (! in_array($order, array('id', 'name'))) {
- $order = 'name';
- $direction = 'ASC';
- }
-
- if ($paginate === 'projectSummaries') {
- $projects = $this->projectPaginator->projectSummaries($project_ids, $offset, $limit, $order, $direction);
- }
- else {
- $offset = 0;
- $projects = $this->projectPaginator->projectSummaries($project_ids, $offset, $limit);
- }
-
- return array(
- 'projects' => $projects,
- 'project_pagination' => array(
- 'controller' => 'app',
- 'action' => 'index',
- 'params' => array('paginate' => 'projectSummaries'),
- 'direction' => $direction,
- 'order' => $order,
- 'total' => count($project_ids),
- 'offset' => $offset,
- 'limit' => $limit,
- )
- );
- }
-
- /**
- * Render Markdown Text and reply with the HTML Code
+ * Render Markdown text and reply with the HTML Code
*
* @access public
*/
@@ -190,10 +89,34 @@ class App extends Base
if (empty($payload['text'])) {
$this->response->html('
'.t('Nothing to preview...').'
');
}
- else {
- $this->response->html(
- $this->template->markdown($payload['text'])
- );
- }
+
+ $this->response->html($this->template->markdown($payload['text']));
+ }
+
+ /**
+ * Colors stylesheet
+ *
+ * @access public
+ */
+ public function colors()
+ {
+ $this->response->css($this->color->getCss());
+ }
+
+ /**
+ * Task autocompletion (Ajax)
+ *
+ * @access public
+ */
+ public function autocomplete()
+ {
+ $this->response->json(
+ $this->taskFilter
+ ->create()
+ ->filterByProjects($this->projectPermission->getActiveMemberProjectIds($this->userSession->getId()))
+ ->excludeTasks(array($this->request->getIntegerParam('exclude_task_id')))
+ ->filterByTitle($this->request->getStringParam('term'))
+ ->toAutoCompletion()
+ );
}
}
diff --git a/sources/app/Controller/Base.php b/sources/app/Controller/Base.php
index 8a5354a..d949048 100644
--- a/sources/app/Controller/Base.php
+++ b/sources/app/Controller/Base.php
@@ -17,8 +17,13 @@ use Symfony\Component\EventDispatcher\Event;
* @package controller
* @author Frederic Guillot
*
+ * @property \Core\Helper $helper
* @property \Core\Session $session
* @property \Core\Template $template
+ * @property \Core\Paginator $paginator
+ * @property \Integration\GithubWebhook $githubWebhook
+ * @property \Integration\GitlabWebhook $gitlabWebhook
+ * @property \Integration\BitbucketWebhook $bitbucketWebhook
* @property \Model\Acl $acl
* @property \Model\Authentication $authentication
* @property \Model\Action $action
@@ -33,22 +38,29 @@ use Symfony\Component\EventDispatcher\Event;
* @property \Model\Notification $notification
* @property \Model\Project $project
* @property \Model\ProjectPermission $projectPermission
+ * @property \Model\ProjectDuplication $projectDuplication
* @property \Model\ProjectAnalytic $projectAnalytic
+ * @property \Model\ProjectActivity $projectActivity
* @property \Model\ProjectDailySummary $projectDailySummary
- * @property \Model\SubTask $subTask
+ * @property \Model\Subtask $subtask
+ * @property \Model\Swimlane $swimlane
* @property \Model\Task $task
+ * @property \Model\Link $link
* @property \Model\TaskCreation $taskCreation
* @property \Model\TaskModification $taskModification
* @property \Model\TaskDuplication $taskDuplication
* @property \Model\TaskHistory $taskHistory
* @property \Model\TaskExport $taskExport
* @property \Model\TaskFinder $taskFinder
+ * @property \Model\TaskFilter $taskFilter
* @property \Model\TaskPosition $taskPosition
* @property \Model\TaskPermission $taskPermission
* @property \Model\TaskStatus $taskStatus
* @property \Model\TaskValidator $taskValidator
+ * @property \Model\TaskLink $taskLink
* @property \Model\CommentHistory $commentHistory
* @property \Model\SubtaskHistory $subtaskHistory
+ * @property \Model\SubtaskTimeTracking $subtaskTimeTracking
* @property \Model\TimeTracking $timeTracking
* @property \Model\User $user
* @property \Model\UserSession $userSession
@@ -107,7 +119,7 @@ abstract class Base
}
$this->container['logger']->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nb_queries));
- $this->container['logger']->debug('RENDERING={time}', array('time' => microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']));
+ $this->container['logger']->debug('RENDERING={time}', array('time' => microtime(true) - @$_SERVER['REQUEST_TIME_FLOAT']));
}
}
@@ -131,7 +143,7 @@ abstract class Base
private function sendHeaders($action)
{
// HTTP secure headers
- $this->response->csp(array('style-src' => "'self' 'unsafe-inline'"));
+ $this->response->csp(array('style-src' => "'self' 'unsafe-inline'", 'img-src' => '*'));
$this->response->nosniff();
$this->response->xss();
@@ -158,16 +170,19 @@ abstract class Base
$this->container['dispatcher']->dispatch('session.bootstrap', new Event);
if (! $this->acl->isPublicAction($controller, $action)) {
- $this->handleAuthenticatedUser($controller, $action);
+ $this->handleAuthentication();
+ $this->handleAuthorization($controller, $action);
+
+ $this->session['has_subtask_inprogress'] = $this->subtask->hasSubtaskInProgress($this->userSession->getId());
}
}
/**
- * Check page access and authentication
+ * Check authentication
*
* @access public
*/
- public function handleAuthenticatedUser($controller, $action)
+ public function handleAuthentication()
{
if (! $this->authentication->isAuthenticated()) {
@@ -177,8 +192,24 @@ abstract class Base
$this->response->redirect('?controller=user&action=login&redirect_query='.urlencode($this->request->getQueryString()));
}
+ }
- if (! $this->acl->isAllowed($controller, $action, $this->request->getIntegerParam('project_id', 0))) {
+ /**
+ * Check page access and authorization
+ *
+ * @access public
+ */
+ public function handleAuthorization($controller, $action)
+ {
+ $project_id = $this->request->getIntegerParam('project_id');
+ $task_id = $this->request->getIntegerParam('task_id');
+
+ // Allow urls without "project_id"
+ if ($task_id > 0 && $project_id === 0) {
+ $project_id = $this->taskFinder->getProjectId($task_id);
+ }
+
+ if (! $this->acl->isAllowed($controller, $action, $project_id)) {
$this->forbidden();
}
}
@@ -280,7 +311,7 @@ abstract class Base
{
$task = $this->taskFinder->getDetails($this->request->getIntegerParam('task_id'));
- if (! $task || $task['project_id'] != $this->request->getIntegerParam('project_id')) {
+ if (! $task) {
$this->notfound();
}
diff --git a/sources/app/Controller/Board.php b/sources/app/Controller/Board.php
index 48f2b51..90b7f35 100644
--- a/sources/app/Controller/Board.php
+++ b/sources/app/Controller/Board.php
@@ -205,6 +205,7 @@ class Board extends Base
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;
}
@@ -218,28 +219,39 @@ class Board extends Base
}
/**
- * Validate and update a board
+ * Display a form to edit a board
*
* @access public
*/
- public function update()
+ public function editColumn(array $values = array(), array $errors = array())
{
$project = $this->getProject();
- $columns = $this->board->getColumns($project['id']);
- $data = $this->request->getValues();
- $values = $columns_list = array();
+ $column = $this->board->getColumn($this->request->getIntegerParam('column_id'));
- foreach ($columns as $column) {
- $columns_list[$column['id']] = $column['title'];
- $values['title['.$column['id'].']'] = isset($data['title'][$column['id']]) ? $data['title'][$column['id']] : '';
- $values['task_limit['.$column['id'].']'] = isset($data['task_limit'][$column['id']]) ? $data['task_limit'][$column['id']] : 0;
- }
+ $this->response->html($this->projectLayout('board/edit_column', array(
+ 'errors' => $errors,
+ 'values' => $values ?: $column,
+ 'project' => $project,
+ 'column' => $column,
+ 'title' => t('Edit column "%s"', $column['title'])
+ )));
+ }
- list($valid, $errors) = $this->board->validateModification($columns_list, $values);
+ /**
+ * Validate and update a column
+ *
+ * @access public
+ */
+ public function updateColumn()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getValues();
+
+ list($valid, $errors) = $this->board->validateModification($values);
if ($valid) {
- if ($this->board->update($data)) {
+ if ($this->board->updateColumn($values['id'], $values['title'], $values['task_limit'], $values['description'])) {
$this->session->flash(t('Board updated successfully.'));
$this->response->redirect('?controller=board&action=edit&project_id='.$project['id']);
}
@@ -248,7 +260,7 @@ class Board extends Base
}
}
- $this->edit($values, $errors);
+ $this->editcolumn($values, $errors);
}
/**
@@ -271,7 +283,7 @@ class Board extends Base
if ($valid) {
- if ($this->board->addColumn($project['id'], $data['title'])) {
+ if ($this->board->addColumn($project['id'], $data['title'], $data['task_limit'], $data['description'])) {
$this->session->flash(t('Board updated successfully.'));
$this->response->redirect('?controller=board&action=edit&project_id='.$project['id']);
}
@@ -389,6 +401,20 @@ class Board extends Base
);
}
+ /**
+ * Get links on mouseover
+ *
+ * @access public
+ */
+ public function tasklinks()
+ {
+ $task = $this->getTask();
+ $this->response->html($this->template->render('board/tasklinks', array(
+ 'links' => $this->taskLink->getLinks($task['id']),
+ 'task' => $task,
+ )));
+ }
+
/**
* Get subtasks on mouseover
*
@@ -398,23 +424,7 @@ class Board extends Base
{
$task = $this->getTask();
$this->response->html($this->template->render('board/subtasks', array(
- 'subtasks' => $this->subTask->getAll($task['id']),
- 'task' => $task,
- )));
- }
-
- /**
- * Change the status of a subtask from the mouseover
- *
- * @access public
- */
- public function toggleSubtask()
- {
- $task = $this->getTask();
- $this->subTask->toggleStatus($this->request->getIntegerParam('subtask_id'));
-
- $this->response->html($this->template->render('board/subtasks', array(
- 'subtasks' => $this->subTask->getAll($task['id']),
+ 'subtasks' => $this->subtask->getAll($task['id']),
'task' => $task,
)));
}
@@ -449,7 +459,7 @@ class Board extends Base
}
/**
- * Display the description
+ * Display task description
*
* @access public
*/
diff --git a/sources/app/Controller/Calendar.php b/sources/app/Controller/Calendar.php
new file mode 100644
index 0000000..1c7ac7c
--- /dev/null
+++ b/sources/app/Controller/Calendar.php
@@ -0,0 +1,107 @@
+getProject();
+
+ $this->response->html($this->template->layout('calendar/show', array(
+ 'check_interval' => $this->config->get('board_private_refresh_interval'),
+ 'users_list' => $this->projectPermission->getMemberList($project['id'], true, true),
+ 'categories_list' => $this->category->getList($project['id'], true, true),
+ 'columns_list' => $this->board->getColumnsList($project['id'], true),
+ 'swimlanes_list' => $this->swimlane->getList($project['id'], true),
+ 'colors_list' => $this->color->getList(true),
+ 'status_list' => $this->taskStatus->getList(true),
+ 'project' => $project,
+ 'title' => t('Calendar for "%s"', $project['name']),
+ 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ )));
+ }
+
+ /**
+ * Get tasks to display on the calendar (project view)
+ *
+ * @access public
+ */
+ public function project()
+ {
+ $project_id = $this->request->getIntegerParam('project_id');
+ $start = $this->request->getStringParam('start');
+ $end = $this->request->getStringParam('end');
+
+ $due_tasks = $this->taskFilter
+ ->create()
+ ->filterByProject($project_id)
+ ->filterByCategory($this->request->getIntegerParam('category_id', -1))
+ ->filterByOwner($this->request->getIntegerParam('owner_id', -1))
+ ->filterByColumn($this->request->getIntegerParam('column_id', -1))
+ ->filterBySwimlane($this->request->getIntegerParam('swimlane_id', -1))
+ ->filterByColor($this->request->getStringParam('color_id'))
+ ->filterByStatus($this->request->getIntegerParam('is_active', -1))
+ ->filterByDueDateRange($start, $end)
+ ->toCalendarEvents();
+
+ $subtask_timeslots = $this->subtaskTimeTracking->getProjectCalendarEvents($project_id, $start, $end);
+
+ $this->response->json(array_merge($due_tasks, $subtask_timeslots));
+ }
+
+ /**
+ * Get tasks to display on the calendar (user view)
+ *
+ * @access public
+ */
+ public function user()
+ {
+ $user_id = $this->request->getIntegerParam('user_id');
+ $start = $this->request->getStringParam('start');
+ $end = $this->request->getStringParam('end');
+
+ $due_tasks = $this->taskFilter
+ ->create()
+ ->filterByOwner($user_id)
+ ->filterByStatus(TaskModel::STATUS_OPEN)
+ ->filterByDueDateRange($start, $end)
+ ->toCalendarEvents();
+
+ $subtask_timeslots = $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end);
+
+ $this->response->json(array_merge($due_tasks, $subtask_timeslots));
+ }
+
+ /**
+ * Update task due date
+ *
+ * @access public
+ */
+ public function save()
+ {
+ if ($this->request->isAjax() && $this->request->isPost()) {
+
+ $values = $this->request->getJson();
+
+ $this->taskModification->update(array(
+ 'id' => $values['task_id'],
+ 'date_due' => $values['date_due'],
+ ));
+ }
+ }
+}
diff --git a/sources/app/Controller/Comment.php b/sources/app/Controller/Comment.php
index 9796ea3..5003200 100644
--- a/sources/app/Controller/Comment.php
+++ b/sources/app/Controller/Comment.php
@@ -41,6 +41,7 @@ class Comment extends Base
public function create(array $values = array(), array $errors = array())
{
$task = $this->getTask();
+ $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
if (empty($values)) {
$values = array(
@@ -49,11 +50,20 @@ class Comment extends Base
);
}
+ if ($ajax) {
+ $this->response->html($this->template->render('comment/create', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'task' => $task,
+ 'ajax' => $ajax,
+ )));
+ }
+
$this->response->html($this->taskLayout('comment/create', array(
'values' => $values,
'errors' => $errors,
'task' => $task,
- 'title' => t('Add a comment')
+ 'title' => t('Add a comment'),
)));
}
@@ -66,6 +76,7 @@ class Comment extends Base
{
$task = $this->getTask();
$values = $this->request->getValues();
+ $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
list($valid, $errors) = $this->comment->validateCreation($values);
@@ -78,6 +89,10 @@ class Comment extends Base
$this->session->flashError(t('Unable to create your comment.'));
}
+ if ($ajax) {
+ $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
+ }
+
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#comments');
}
diff --git a/sources/app/Controller/Config.php b/sources/app/Controller/Config.php
index 9005c30..01c7ad5 100644
--- a/sources/app/Controller/Config.php
+++ b/sources/app/Controller/Config.php
@@ -38,7 +38,7 @@ class Config extends Base
{
if ($this->request->isPost()) {
- $values = $this->request->getValues();
+ $values = $this->request->getValues() + array('subtask_restriction' => 0, 'subtask_time_tracking' => 0);
if ($this->config->save($values)) {
$this->config->reload();
diff --git a/sources/app/Controller/File.php b/sources/app/Controller/File.php
index 6305261..3255fe8 100644
--- a/sources/app/Controller/File.php
+++ b/sources/app/Controller/File.php
@@ -2,8 +2,6 @@
namespace Controller;
-use Model\File as FileModel;
-
/**
* File controller
*
@@ -54,7 +52,7 @@ class File extends Base
{
$task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
- $filename = FileModel::BASE_PATH.$file['path'];
+ $filename = FILES_DIR.$file['path'];
if ($file['task_id'] == $task['id'] && file_exists($filename)) {
$this->response->forceDownload($file['name']);
@@ -91,7 +89,7 @@ class File extends Base
{
$task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
- $filename = FileModel::BASE_PATH.$file['path'];
+ $filename = FILES_DIR.$file['path'];
if ($file['task_id'] == $task['id'] && file_exists($filename)) {
$metadata = getimagesize($filename);
diff --git a/sources/app/Controller/Link.php b/sources/app/Controller/Link.php
new file mode 100644
index 0000000..ec9c619
--- /dev/null
+++ b/sources/app/Controller/Link.php
@@ -0,0 +1,162 @@
+projectPermission->getAllowedProjects($this->userSession->getId());
+ $params['config_content_for_layout'] = $this->template->render($template, $params);
+
+ return $this->template->layout('config/layout', $params);
+ }
+
+ /**
+ * Get the current link
+ *
+ * @access private
+ * @return array
+ */
+ private function getLink()
+ {
+ $link = $this->link->getById($this->request->getIntegerParam('link_id'));
+
+ if (! $link) {
+ $this->notfound();
+ }
+
+ return $link;
+ }
+
+ /**
+ * List of links
+ *
+ * @access public
+ */
+ public function index(array $values = array(), array $errors = array())
+ {
+ $this->response->html($this->layout('link/index', array(
+ 'links' => $this->link->getMergedList(),
+ 'values' => $values,
+ 'errors' => $errors,
+ 'title' => t('Settings').' > '.t('Task\'s links'),
+ )));
+ }
+
+ /**
+ * Validate and save a new link
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->link->validateCreation($values);
+
+ if ($valid) {
+
+ if ($this->link->create($values['label'], $values['opposite_label'])) {
+ $this->session->flash(t('Link added successfully.'));
+ $this->response->redirect($this->helper->url('link', 'index'));
+ }
+ else {
+ $this->session->flashError(t('Unable to create your link.'));
+ }
+ }
+
+ $this->index($values, $errors);
+ }
+
+ /**
+ * Edit form
+ *
+ * @access public
+ */
+ public function edit(array $values = array(), array $errors = array())
+ {
+ $link = $this->getLink();
+ $link['label'] = t($link['label']);
+
+ $this->response->html($this->layout('link/edit', array(
+ 'values' => $values ?: $link,
+ 'errors' => $errors,
+ 'labels' => $this->link->getList($link['id']),
+ 'link' => $link,
+ 'title' => t('Link modification')
+ )));
+ }
+
+ /**
+ * Edit a link (validate the form and update the database)
+ *
+ * @access public
+ */
+ public function update()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->link->validateModification($values);
+
+ if ($valid) {
+ if ($this->link->update($values)) {
+ $this->session->flash(t('Link updated successfully.'));
+ $this->response->redirect($this->helper->url('link', 'index'));
+ }
+ else {
+ $this->session->flashError(t('Unable to update your link.'));
+ }
+ }
+
+ $this->edit($values, $errors);
+ }
+
+ /**
+ * Confirmation dialog before removing a link
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $link = $this->getLink();
+
+ $this->response->html($this->layout('link/remove', array(
+ 'link' => $link,
+ 'title' => t('Remove a link')
+ )));
+ }
+
+ /**
+ * Remove a link
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $link = $this->getLink();
+
+ if ($this->link->remove($link['id'])) {
+ $this->session->flash(t('Link removed successfully.'));
+ }
+ else {
+ $this->session->flashError(t('Unable to remove this link.'));
+ }
+
+ $this->response->redirect($this->helper->url('link', 'index'));
+ }
+}
diff --git a/sources/app/Controller/Project.php b/sources/app/Controller/Project.php
index d0da53d..fb0a8d0 100644
--- a/sources/app/Controller/Project.php
+++ b/sources/app/Controller/Project.php
@@ -17,24 +17,25 @@ class Project extends Base
*/
public function index()
{
- $projects = $this->project->getAll(! $this->userSession->isAdmin());
- $nb_projects = count($projects);
- $active_projects = array();
- $inactive_projects = array();
-
- foreach ($projects as $project) {
- if ($project['is_active'] == 1) {
- $active_projects[] = $project;
- }
- else {
- $inactive_projects[] = $project;
- }
+ if ($this->userSession->isAdmin()) {
+ $project_ids = $this->project->getAllIds();
}
+ else {
+ $project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId());
+ }
+
+ $nb_projects = count($project_ids);
+
+ $paginator = $this->paginator
+ ->setUrl('project', 'index')
+ ->setMax(20)
+ ->setOrder('name')
+ ->setQuery($this->project->getQueryColumnStats($project_ids))
+ ->calculate();
$this->response->html($this->template->layout('project/index', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
- 'active_projects' => $active_projects,
- 'inactive_projects' => $inactive_projects,
+ 'paginator' => $paginator,
'nb_projects' => $nb_projects,
'title' => t('Projects').' ('.$nb_projects.')'
)));
@@ -51,7 +52,7 @@ class Project extends Base
$this->response->html($this->projectLayout('project/show', array(
'project' => $project,
- 'stats' => $this->project->getStats($project['id']),
+ 'stats' => $this->project->getTaskStats($project['id']),
'title' => $project['name'],
)));
}
@@ -297,6 +298,7 @@ class Project extends Base
* Duplicate a project
*
* @author Antonio Rabelo
+ * @author Michael Lüpkes
* @access public
*/
public function duplicate()
@@ -304,10 +306,8 @@ class Project extends Base
$project = $this->getProject();
if ($this->request->getStringParam('duplicate') === 'yes') {
-
- $this->checkCSRFParam();
-
- if ($this->project->duplicate($project['id'])) {
+ $values = array_keys($this->request->getValues());
+ if ($this->projectDuplication->duplicate($project['id'], $values)) {
$this->session->flash(t('Project cloned successfully.'));
} else {
$this->session->flashError(t('Unable to clone this project.'));
@@ -425,38 +425,32 @@ class Project extends Base
{
$project = $this->getProject();
$search = $this->request->getStringParam('search');
- $direction = $this->request->getStringParam('direction', 'DESC');
- $order = $this->request->getStringParam('order', 'tasks.id');
- $offset = $this->request->getIntegerParam('offset', 0);
- $tasks = array();
$nb_tasks = 0;
- $limit = 25;
+
+ $paginator = $this->paginator
+ ->setUrl('project', 'search', array('search' => $search, 'project_id' => $project['id']))
+ ->setMax(30)
+ ->setOrder('tasks.id')
+ ->setDirection('DESC');
if ($search !== '') {
- $tasks = $this->taskPaginator->searchTasks($project['id'], $search, $offset, $limit, $order, $direction);
- $nb_tasks = $this->taskPaginator->countSearchTasks($project['id'], $search);
+
+ $paginator
+ ->setQuery($this->taskFinder->getSearchQuery($project['id'], $search))
+ ->calculate();
+
+ $nb_tasks = $paginator->getTotal();
}
$this->response->html($this->template->layout('project/search', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
- 'tasks' => $tasks,
- 'nb_tasks' => $nb_tasks,
- 'pagination' => array(
- 'controller' => 'project',
- 'action' => 'search',
- 'params' => array('search' => $search, 'project_id' => $project['id']),
- 'direction' => $direction,
- 'order' => $order,
- 'total' => $nb_tasks,
- 'offset' => $offset,
- 'limit' => $limit,
- ),
'values' => array(
'search' => $search,
'controller' => 'project',
'action' => 'search',
'project_id' => $project['id'],
),
+ 'paginator' => $paginator,
'project' => $project,
'columns' => $this->board->getColumnsList($project['id']),
'categories' => $this->category->getList($project['id'], false),
@@ -472,32 +466,21 @@ class Project extends Base
public function tasks()
{
$project = $this->getProject();
- $direction = $this->request->getStringParam('direction', 'DESC');
- $order = $this->request->getStringParam('order', 'tasks.date_completed');
- $offset = $this->request->getIntegerParam('offset', 0);
- $limit = 25;
-
- $tasks = $this->taskPaginator->closedTasks($project['id'], $offset, $limit, $order, $direction);
- $nb_tasks = $this->taskPaginator->countClosedTasks($project['id']);
+ $paginator = $this->paginator
+ ->setUrl('project', 'tasks', array('project_id' => $project['id']))
+ ->setMax(30)
+ ->setOrder('tasks.id')
+ ->setDirection('DESC')
+ ->setQuery($this->taskFinder->getClosedTaskQuery($project['id']))
+ ->calculate();
$this->response->html($this->template->layout('project/tasks', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
- 'pagination' => array(
- 'controller' => 'project',
- 'action' => 'tasks',
- 'params' => array('project_id' => $project['id']),
- 'direction' => $direction,
- 'order' => $order,
- 'total' => $nb_tasks,
- 'offset' => $offset,
- 'limit' => $limit,
- ),
'project' => $project,
'columns' => $this->board->getColumnsList($project['id']),
'categories' => $this->category->getList($project['id'], false),
- 'tasks' => $tasks,
- 'nb_tasks' => $nb_tasks,
- 'title' => t('Completed tasks for "%s"', $project['name']).' ('.$nb_tasks.')'
+ 'paginator' => $paginator,
+ 'title' => t('Completed tasks for "%s"', $project['name']).' ('.$paginator->getTotal().')'
)));
}
diff --git a/sources/app/Controller/Subtask.php b/sources/app/Controller/Subtask.php
index 0521b89..c7ec00d 100644
--- a/sources/app/Controller/Subtask.php
+++ b/sources/app/Controller/Subtask.php
@@ -2,8 +2,10 @@
namespace Controller;
+use Model\Subtask as SubtaskModel;
+
/**
- * SubTask controller
+ * Subtask controller
*
* @package controller
* @author Frederic Guillot
@@ -18,7 +20,7 @@ class Subtask extends Base
*/
private function getSubtask()
{
- $subtask = $this->subTask->getById($this->request->getIntegerParam('subtask_id'));
+ $subtask = $this->subtask->getById($this->request->getIntegerParam('subtask_id'));
if (! $subtask) {
$this->notfound();
@@ -61,11 +63,11 @@ class Subtask extends Base
$task = $this->getTask();
$values = $this->request->getValues();
- list($valid, $errors) = $this->subTask->validateCreation($values);
+ list($valid, $errors) = $this->subtask->validateCreation($values);
if ($valid) {
- if ($this->subTask->create($values)) {
+ if ($this->subtask->create($values)) {
$this->session->flash(t('Sub-task added successfully.'));
}
else {
@@ -96,7 +98,7 @@ class Subtask extends Base
'values' => empty($values) ? $subtask : $values,
'errors' => $errors,
'users_list' => $this->projectPermission->getMemberList($task['project_id']),
- 'status_list' => $this->subTask->getStatusList(),
+ 'status_list' => $this->subtask->getStatusList(),
'subtask' => $subtask,
'task' => $task,
)));
@@ -113,11 +115,11 @@ class Subtask extends Base
$this->getSubtask();
$values = $this->request->getValues();
- list($valid, $errors) = $this->subTask->validateModification($values);
+ list($valid, $errors) = $this->subtask->validateModification($values);
if ($valid) {
- if ($this->subTask->update($values)) {
+ if ($this->subtask->update($values)) {
$this->session->flash(t('Sub-task updated successfully.'));
}
else {
@@ -157,7 +159,7 @@ class Subtask extends Base
$task = $this->getTask();
$subtask = $this->getSubtask();
- if ($this->subTask->remove($subtask['id'])) {
+ if ($this->subtask->remove($subtask['id'])) {
$this->session->flash(t('Sub-task removed successfully.'));
}
else {
@@ -175,12 +177,86 @@ class Subtask extends Base
public function toggleStatus()
{
$task = $this->getTask();
- $subtask_id = $this->request->getIntegerParam('subtask_id');
+ $subtask = $this->getSubtask();
+ $redirect = $this->request->getStringParam('redirect', 'task');
- if (! $this->subTask->toggleStatus($subtask_id)) {
- $this->session->flashError(t('Unable to update your sub-task.'));
+ $this->subtask->toggleStatus($subtask['id']);
+
+ if ($redirect === 'board') {
+
+ $this->session['has_subtask_inprogress'] = $this->subtask->hasSubtaskInProgress($this->userSession->getId());
+
+ $this->response->html($this->template->render('board/subtasks', array(
+ 'subtasks' => $this->subtask->getAll($task['id']),
+ 'task' => $task,
+ )));
}
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#subtasks');
+ $this->toggleRedirect($task, $redirect);
+ }
+
+ /**
+ * Handle subtask restriction (popover)
+ *
+ * @access public
+ */
+ public function subtaskRestriction()
+ {
+ $task = $this->getTask();
+ $subtask = $this->getSubtask();
+
+ $this->response->html($this->template->render('subtask/restriction_change_status', array(
+ 'status_list' => array(
+ SubtaskModel::STATUS_TODO => t('Todo'),
+ SubtaskModel::STATUS_DONE => t('Done'),
+ ),
+ 'subtask_inprogress' => $this->subtask->getSubtaskInProgress($this->userSession->getId()),
+ 'subtask' => $subtask,
+ 'task' => $task,
+ 'redirect' => $this->request->getStringParam('redirect'),
+ )));
+ }
+
+ /**
+ * Change status of the in progress subtask and the other subtask
+ *
+ * @access public
+ */
+ public function changeRestrictionStatus()
+ {
+ $task = $this->getTask();
+ $subtask = $this->getSubtask();
+ $values = $this->request->getValues();
+
+ // Change status of the previous in progress subtask
+ $this->subtask->update(array(
+ 'id' => $values['id'],
+ 'status' => $values['status'],
+ ));
+
+ // Set the current subtask to in pogress
+ $this->subtask->update(array(
+ 'id' => $subtask['id'],
+ 'status' => SubtaskModel::STATUS_INPROGRESS,
+ ));
+
+ $this->toggleRedirect($task, $values['redirect']);
+ }
+
+ /**
+ * Redirect to the right page
+ *
+ * @access private
+ */
+ private function toggleRedirect(array $task, $redirect)
+ {
+ switch ($redirect) {
+ case 'board':
+ $this->response->redirect($this->helper->url('board', 'show', array('project_id' => $task['project_id'])));
+ case 'dashboard':
+ $this->response->redirect($this->helper->url('app', 'index'));
+ default:
+ $this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
+ }
}
}
diff --git a/sources/app/Controller/Task.php b/sources/app/Controller/Task.php
index 7f85f36..741db61 100644
--- a/sources/app/Controller/Task.php
+++ b/sources/app/Controller/Task.php
@@ -35,7 +35,8 @@ class Task extends Base
$this->response->html($this->template->layout('task/public', array(
'project' => $project,
'comments' => $this->comment->getAll($task['id']),
- 'subtasks' => $this->subTask->getAll($task['id']),
+ 'subtasks' => $this->subtask->getAll($task['id']),
+ 'links' => $this->taskLink->getLinks($task['id']),
'task' => $task,
'columns_list' => $this->board->getColumnsList($task['project_id']),
'colors_list' => $this->color->getList(),
@@ -54,7 +55,7 @@ class Task extends Base
public function show()
{
$task = $this->getTask();
- $subtasks = $this->subTask->getAll($task['id']);
+ $subtasks = $this->subtask->getAll($task['id']);
$values = array(
'id' => $task['id'],
@@ -70,9 +71,9 @@ class Task extends Base
'files' => $this->file->getAll($task['id']),
'comments' => $this->comment->getAll($task['id']),
'subtasks' => $subtasks,
+ 'links' => $this->taskLink->getLinks($task['id']),
'task' => $task,
'values' => $values,
- 'timesheet' => $this->timeTracking->getTaskTimesheet($task, $subtasks),
'columns_list' => $this->board->getColumnsList($task['project_id']),
'colors_list' => $this->color->getList(),
'date_format' => $this->config->get('application_date_format'),
@@ -250,6 +251,7 @@ class Task extends Base
public function close()
{
$task = $this->getTask();
+ $redirect = $this->request->getStringParam('redirect');
if ($this->request->getStringParam('confirmation') === 'yes') {
@@ -261,11 +263,23 @@ class Task extends Base
$this->session->flashError(t('Unable to close this task.'));
}
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']);
+ if ($redirect === 'board') {
+ $this->response->redirect($this->helper->url('board', 'show', array('project_id' => $task['project_id'])));
+ }
+
+ $this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
+ }
+
+ if ($this->request->isAjax()) {
+ $this->response->html($this->template->render('task/close', array(
+ 'task' => $task,
+ 'redirect' => $redirect,
+ )));
}
$this->response->html($this->taskLayout('task/close', array(
'task' => $task,
+ 'redirect' => $redirect,
)));
}
@@ -418,7 +432,7 @@ class Task extends Base
$task = $this->getTask();
$values = $task;
$errors = array();
- $projects_list = $this->projectPermission->getMemberProjects($this->userSession->getId());
+ $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId());
unset($projects_list[$task['project_id']]);
@@ -457,7 +471,7 @@ class Task extends Base
$task = $this->getTask();
$values = $task;
$errors = array();
- $projects_list = $this->projectPermission->getMemberProjects($this->userSession->getId());
+ $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId());
unset($projects_list[$task['project_id']]);
@@ -485,4 +499,27 @@ class Task extends Base
'projects_list' => $projects_list,
)));
}
+
+ /**
+ * Display the time tracking details
+ *
+ * @access public
+ */
+ public function timesheet()
+ {
+ $task = $this->getTask();
+
+ $subtask_paginator = $this->paginator
+ ->setUrl('task', 'timesheet', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'pagination' => 'subtasks'))
+ ->setMax(15)
+ ->setOrder('start')
+ ->setDirection('DESC')
+ ->setQuery($this->subtaskTimeTracking->getTaskQuery($task['id']))
+ ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks');
+
+ $this->response->html($this->taskLayout('task/time_tracking', array(
+ 'task' => $task,
+ 'subtask_paginator' => $subtask_paginator,
+ )));
+ }
}
diff --git a/sources/app/Controller/Tasklink.php b/sources/app/Controller/Tasklink.php
new file mode 100644
index 0000000..61b7fab
--- /dev/null
+++ b/sources/app/Controller/Tasklink.php
@@ -0,0 +1,116 @@
+taskLink->getById($this->request->getIntegerParam('link_id'));
+
+ if (! $link) {
+ $this->notfound();
+ }
+
+ return $link;
+ }
+
+ /**
+ * Creation form
+ *
+ * @access public
+ */
+ public function create(array $values = array(), array $errors = array())
+ {
+ $task = $this->getTask();
+
+ if (empty($values)) {
+ $values = array(
+ 'task_id' => $task['id'],
+ );
+ }
+
+ $this->response->html($this->taskLayout('tasklink/create', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'task' => $task,
+ 'labels' => $this->link->getList(0, false),
+ 'title' => t('Add a new link')
+ )));
+ }
+
+ /**
+ * Validation and creation
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $task = $this->getTask();
+ $values = $this->request->getValues();
+
+ list($valid, $errors) = $this->taskLink->validateCreation($values);
+
+ if ($valid) {
+
+ if ($this->taskLink->create($values['task_id'], $values['opposite_task_id'], $values['link_id'])) {
+ $this->session->flash(t('Link added successfully.'));
+ $this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links');
+ }
+ else {
+ $this->session->flashError(t('Unable to create your link.'));
+ }
+ }
+
+ $this->create($values, $errors);
+ }
+
+ /**
+ * Confirmation dialog before removing a link
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $task = $this->getTask();
+ $link = $this->getTaskLink();
+
+ $this->response->html($this->taskLayout('tasklink/remove', array(
+ 'link' => $link,
+ 'task' => $task,
+ )));
+ }
+
+ /**
+ * Remove a link
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $task = $this->getTask();
+
+ if ($this->taskLink->remove($this->request->getIntegerParam('link_id'))) {
+ $this->session->flash(t('Link removed successfully.'));
+ }
+ else {
+ $this->session->flashError(t('Unable to remove this link.'));
+ }
+
+ $this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
+ }
+}
diff --git a/sources/app/Controller/User.php b/sources/app/Controller/User.php
index 7fddf70..decdb64 100644
--- a/sources/app/Controller/User.php
+++ b/sources/app/Controller/User.php
@@ -56,7 +56,7 @@ class User extends Base
if ($valid) {
if ($redirect_query !== '') {
- $this->response->redirect('?'.$redirect_query);
+ $this->response->redirect('?'.urldecode($redirect_query));
}
else {
$this->response->redirect('?controller=app');
@@ -115,31 +115,19 @@ class User extends Base
*/
public function index()
{
- $direction = $this->request->getStringParam('direction', 'ASC');
- $order = $this->request->getStringParam('order', 'username');
- $offset = $this->request->getIntegerParam('offset', 0);
- $limit = 25;
-
- $users = $this->user->paginate($offset, $limit, $order, $direction);
- $nb_users = $this->user->count();
+ $paginator = $this->paginator
+ ->setUrl('user', 'index')
+ ->setMax(30)
+ ->setOrder('username')
+ ->setQuery($this->user->getQuery())
+ ->calculate();
$this->response->html(
$this->template->layout('user/index', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
'projects' => $this->project->getList(),
- 'nb_users' => $nb_users,
- 'users' => $users,
- 'title' => t('Users').' ('.$nb_users.')',
- 'pagination' => array(
- 'controller' => 'user',
- 'action' => 'index',
- 'direction' => $direction,
- 'order' => $order,
- 'total' => $nb_users,
- 'offset' => $offset,
- 'limit' => $limit,
- 'params' => array(),
- ),
+ 'title' => t('Users').' ('.$paginator->getTotal().')',
+ 'paginator' => $paginator,
)));
}
@@ -201,6 +189,43 @@ class User extends Base
)));
}
+ /**
+ * Display user calendar
+ *
+ * @access public
+ */
+ public function calendar()
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->layout('user/calendar', array(
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Display timesheet
+ *
+ * @access public
+ */
+ public function timesheet()
+ {
+ $user = $this->getUser();
+
+ $subtask_paginator = $this->paginator
+ ->setUrl('user', 'timesheet', array('user_id' => $user['id'], 'pagination' => 'subtasks'))
+ ->setMax(20)
+ ->setOrder('start')
+ ->setDirection('DESC')
+ ->setQuery($this->subtaskTimeTracking->getUserQuery($user['id']))
+ ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks');
+
+ $this->response->html($this->layout('user/timesheet', array(
+ 'subtask_paginator' => $subtask_paginator,
+ 'user' => $user,
+ )));
+ }
+
/**
* Display last connections
*
@@ -330,7 +355,7 @@ class User extends Base
if ($this->request->isPost()) {
- $values = $this->request->getValues();
+ $values = $this->request->getValues() + array('disable_login_form' => 0);
if ($this->userSession->isAdmin()) {
$values += array('is_admin' => 0);
@@ -462,7 +487,7 @@ class User extends Base
*
* @access public
*/
- public function gitHub()
+ public function github()
{
$code = $this->request->getStringParam('code');
@@ -506,7 +531,7 @@ class User extends Base
*
* @access public
*/
- public function unlinkGitHub()
+ public function unlinkGithub()
{
$this->checkCSRFParam();
diff --git a/sources/app/Controller/Webhook.php b/sources/app/Controller/Webhook.php
index 1ae3b0a..ef79379 100644
--- a/sources/app/Controller/Webhook.php
+++ b/sources/app/Controller/Webhook.php
@@ -82,4 +82,22 @@ class Webhook extends Base
echo $result ? 'PARSED' : 'IGNORED';
}
+
+ /**
+ * Handle Bitbucket webhooks
+ *
+ * @access public
+ */
+ public function bitbucket()
+ {
+ if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) {
+ $this->response->text('Not Authorized', 401);
+ }
+
+ $this->bitbucketWebhook->setProjectId($this->request->getIntegerParam('project_id'));
+
+ $result = $this->bitbucketWebhook->parsePayload(json_decode(@$_POST['payload'], true));
+
+ echo $result ? 'PARSED' : 'IGNORED';
+ }
}
diff --git a/sources/app/Core/Helper.php b/sources/app/Core/Helper.php
index e9fa186..01ebb08 100644
--- a/sources/app/Core/Helper.php
+++ b/sources/app/Core/Helper.php
@@ -3,7 +3,6 @@
namespace Core;
use Pimple\Container;
-use Parsedown;
/**
* Template helpers
@@ -13,6 +12,7 @@ use Parsedown;
*
* @property \Core\Session $session
* @property \Model\Acl $acl
+ * @property \Model\Config $config
* @property \Model\User $user
* @property \Model\UserSession $userSession
*/
@@ -49,6 +49,33 @@ class Helper
return $this->container[$name];
}
+ /**
+ * 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 getTaskAge($timestamp, $now = null)
+ {
+ if ($now === null) {
+ $now = time();
+ }
+
+ $diff = $now - $timestamp;
+
+ if ($diff < 3600) {
+ return t('<1h');
+ }
+ else if ($diff < 86400) {
+ return t('%dh', $diff / 3600);
+ }
+
+ return t('%dd', ($now - $timestamp) / 86400);
+ }
+
/**
* Proxy cache helper for acl::isManagerActionAllowed()
*
@@ -104,9 +131,9 @@ class Helper
* @param string $filename Filename
* @return string
*/
- public function css($filename)
+ public function css($filename, $is_file = true)
{
- return '';
+ return '';
}
/**
@@ -194,9 +221,9 @@ class Helper
* @param string $class CSS class
* @return string
*/
- public function formSelect($name, array $options, array $values = array(), array $errors = array(), $class = '')
+ public function formSelect($name, array $options, array $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
- $html = '