diff options
Diffstat (limited to 'app/Controller')
32 files changed, 3570 insertions, 1062 deletions
diff --git a/app/Controller/Action.php b/app/Controller/Action.php index 714c87f3..cd24453a 100644 --- a/app/Controller/Action.php +++ b/app/Controller/Action.php @@ -17,9 +17,9 @@ class Action extends Base */ public function index() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); - $this->response->html($this->projectLayout('action_index', array( + $this->response->html($this->projectLayout('action/index', array( 'values' => array('project_id' => $project['id']), 'project' => $project, 'actions' => $this->action->getAllByProject($project['id']), @@ -27,11 +27,10 @@ class Action extends Base 'available_events' => $this->action->getAvailableEvents(), 'available_params' => $this->action->getAllActionParameters(), 'columns_list' => $this->board->getColumnsList($project['id']), - 'users_list' => $this->projectPermission->getUsersList($project['id']), + 'users_list' => $this->projectPermission->getMemberList($project['id']), 'projects_list' => $this->project->getList(false), 'colors_list' => $this->color->getList(), 'categories_list' => $this->category->getList($project['id']), - 'menu' => 'projects', 'title' => t('Automatic actions') ))); } @@ -43,18 +42,17 @@ class Action extends Base */ public function event() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues(); if (empty($values['action_name']) || empty($values['project_id'])) { $this->response->redirect('?controller=action&action=index&project_id='.$project['id']); } - $this->response->html($this->projectLayout('action_event', array( + $this->response->html($this->projectLayout('action/event', array( 'values' => $values, 'project' => $project, 'events' => $this->action->getCompatibleEvents($values['action_name']), - 'menu' => 'projects', 'title' => t('Automatic actions') ))); } @@ -66,7 +64,7 @@ class Action extends Base */ public function params() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues(); if (empty($values['action_name']) || empty($values['project_id']) || empty($values['event_name'])) { @@ -83,16 +81,15 @@ class Action extends Base $projects_list = $this->project->getList(false); unset($projects_list[$project['id']]); - $this->response->html($this->projectLayout('action_params', array( + $this->response->html($this->projectLayout('action/params', array( 'values' => $values, 'action_params' => $action_params, 'columns_list' => $this->board->getColumnsList($project['id']), - 'users_list' => $this->projectPermission->getUsersList($project['id']), + 'users_list' => $this->projectPermission->getMemberList($project['id']), 'projects_list' => $projects_list, 'colors_list' => $this->color->getList(), 'categories_list' => $this->category->getList($project['id']), 'project' => $project, - 'menu' => 'projects', 'title' => t('Automatic actions') ))); } @@ -104,7 +101,7 @@ class Action extends Base */ public function create() { - $this->doCreation($this->getProjectManagement(), $this->request->getValues()); + $this->doCreation($this->getProject(), $this->request->getValues()); } /** @@ -138,14 +135,13 @@ class Action extends Base */ public function confirm() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); - $this->response->html($this->projectLayout('action_remove', array( + $this->response->html($this->projectLayout('action/remove', array( 'action' => $this->action->getById($this->request->getIntegerParam('action_id')), 'available_events' => $this->action->getAvailableEvents(), 'available_actions' => $this->action->getAvailableActions(), 'project' => $project, - 'menu' => 'projects', 'title' => t('Remove an action') ))); } @@ -158,10 +154,10 @@ class Action extends Base public function remove() { $this->checkCSRFParam(); - $project = $this->getProjectManagement(); + $project = $this->getProject(); $action = $this->action->getById($this->request->getIntegerParam('action_id')); - if ($action && $this->action->remove($action['id'])) { + if (! empty($action) && $this->action->remove($action['id'])) { $this->session->flash(t('Action removed successfully.')); } else { $this->session->flashError(t('Unable to remove this action.')); diff --git a/app/Controller/Analytic.php b/app/Controller/Analytic.php new file mode 100644 index 00000000..f31870e0 --- /dev/null +++ b/app/Controller/Analytic.php @@ -0,0 +1,170 @@ +<?php + +namespace Controller; + +/** + * Project Anaytic controller + * + * @package controller + * @author Frederic Guillot + */ +class Analytic extends Base +{ + /** + * Common layout for analytic 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); + + return $this->template->layout('analytic/layout', $params); + } + + /** + * Show tasks distribution graph + * + * @access public + */ + public function tasks() + { + $project = $this->getProject(); + $metrics = $this->projectAnalytic->getTaskRepartition($project['id']); + + if ($this->request->isAjax()) { + $this->response->json(array( + 'metrics' => $metrics, + 'labels' => array( + 'column_title' => t('Column'), + 'nb_tasks' => t('Number of tasks'), + ) + )); + } + else { + $this->response->html($this->layout('analytic/tasks', array( + 'project' => $project, + 'metrics' => $metrics, + 'title' => t('Task repartition for "%s"', $project['name']), + ))); + } + } + + /** + * Show users repartition + * + * @access public + */ + public function users() + { + $project = $this->getProject(); + $metrics = $this->projectAnalytic->getUserRepartition($project['id']); + + if ($this->request->isAjax()) { + $this->response->json(array( + 'metrics' => $metrics, + 'labels' => array( + 'user' => t('User'), + 'nb_tasks' => t('Number of tasks'), + ) + )); + } + else { + $this->response->html($this->layout('analytic/users', array( + 'project' => $project, + 'metrics' => $metrics, + 'title' => t('User repartition for "%s"', $project['name']), + ))); + } + } + + /** + * Show cumulative flow diagram + * + * @access public + */ + public function cfd() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week'))); + $to = $this->request->getStringParam('to', date('Y-m-d')); + + if (! empty($values)) { + $from = $values['from']; + $to = $values['to']; + } + + if ($this->request->isAjax()) { + $this->response->json(array( + 'columns' => array_values($this->board->getColumnsList($project['id'])), + 'metrics' => $this->projectDailySummary->getRawMetrics($project['id'], $from, $to), + 'labels' => array( + 'column' => t('Column'), + 'day' => t('Date'), + 'total' => t('Tasks'), + ) + )); + } + else { + $this->response->html($this->layout('analytic/cfd', array( + 'values' => array( + 'from' => $from, + 'to' => $to, + ), + 'display_graph' => $this->projectDailySummary->countDays($project['id'], $from, $to) >= 2, + 'project' => $project, + 'date_format' => $this->config->get('application_date_format'), + 'date_formats' => $this->dateParser->getAvailableFormats(), + 'title' => t('Cumulative flow diagram for "%s"', $project['name']), + ))); + } + } + + /** + * Show burndown chart + * + * @access public + */ + public function burndown() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week'))); + $to = $this->request->getStringParam('to', date('Y-m-d')); + + if (! empty($values)) { + $from = $values['from']; + $to = $values['to']; + } + + if ($this->request->isAjax()) { + $this->response->json(array( + 'metrics' => $this->projectDailySummary->getRawMetricsByDay($project['id'], $from, $to), + 'labels' => array( + 'day' => t('Date'), + 'score' => t('Complexity'), + ) + )); + } + else { + $this->response->html($this->layout('analytic/burndown', array( + 'values' => array( + 'from' => $from, + 'to' => $to, + ), + 'display_graph' => $this->projectDailySummary->countDays($project['id'], $from, $to) >= 2, + 'project' => $project, + 'date_format' => $this->config->get('application_date_format'), + 'date_formats' => $this->dateParser->getAvailableFormats(), + 'title' => t('Burndown chart for "%s"', $project['name']), + ))); + } + } +} diff --git a/app/Controller/App.php b/app/Controller/App.php index feec4221..8a97e8c7 100644 --- a/app/Controller/App.php +++ b/app/Controller/App.php @@ -2,7 +2,8 @@ namespace Controller; -use Model\Project as ProjectModel; +use Model\Subtask as SubtaskModel; +use Model\Task as TaskModel; /** * Application controller @@ -13,21 +14,117 @@ use Model\Project as ProjectModel; class App extends Base { /** + * Check if the user is connected + * + * @access public + */ + public function status() + { + $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') { - $user_id = $this->acl->getUserId(); - $projects = $this->projectPermission->getAllowedProjects($user_id); - - $this->response->html($this->template->layout('app_index', array( - 'board_selector' => $projects, - 'events' => $this->projectActivity->getProjects(array_keys($projects), 10), - 'tasks' => $this->taskFinder->getAllTasksByUser($user_id), - 'menu' => 'dashboard', + $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); + + $task_paginator = $this->paginator + ->setUrl('app', $action, array('pagination' => 'tasks', 'user_id' => $user_id)) + ->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', 'user_id' => $user_id)) + ->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', 'user_id' => $user_id)) + ->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, 5), + 'task_paginator' => $task_paginator, + 'subtask_paginator' => $subtask_paginator, + 'project_paginator' => $project_paginator, + 'user_id' => $user_id, ))); } + + /** + * Render Markdown text and reply with the HTML Code + * + * @access public + */ + public function preview() + { + $payload = $this->request->getJson(); + + if (empty($payload['text'])) { + $this->response->html('<p>'.t('Nothing to preview...').'</p>'); + } + + $this->response->html($this->helper->text->markdown($payload['text'])); + } + + /** + * Colors stylesheet + * + * @access public + */ + public function colors() + { + $this->response->css($this->color->getCss()); + } + + /** + * Task autocompletion (Ajax) + * + * @access public + */ + public function autocomplete() + { + $search = $this->request->getStringParam('term'); + + $filter = $this->taskFilter + ->create() + ->filterByProjects($this->projectPermission->getActiveMemberProjectIds($this->userSession->getId())) + ->excludeTasks(array($this->request->getIntegerParam('exclude_task_id'))); + + // Search by task id or by title + if (ctype_digit($search)) { + $filter->filterById($search); + } + else { + $filter->filterByTitle($search); + } + + $this->response->json($filter->toAutoCompletion()); + } } diff --git a/app/Controller/Auth.php b/app/Controller/Auth.php new file mode 100644 index 00000000..24e6e242 --- /dev/null +++ b/app/Controller/Auth.php @@ -0,0 +1,67 @@ +<?php + +namespace Controller; + +/** + * 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( + 'errors' => $errors, + 'values' => $values, + 'no_layout' => true, + 'redirect_query' => $this->request->getStringParam('redirect_query'), + 'title' => t('Login') + ))); + } + + /** + * Check credentials + * + * @access public + */ + public function check() + { + $redirect_query = $this->request->getStringParam('redirect_query'); + $values = $this->request->getValues(); + list($valid, $errors) = $this->authentication->validateForm($values); + + if ($valid) { + + if ($redirect_query !== '') { + $this->response->redirect('?'.urldecode($redirect_query)); + } + + $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')); + } +} diff --git a/app/Controller/Base.php b/app/Controller/Base.php index a8e22fd8..fcd07b99 100644 --- a/app/Controller/Base.php +++ b/app/Controller/Base.php @@ -2,166 +2,171 @@ namespace Controller; -use Core\Tool; -use Core\Registry; +use Pimple\Container; use Core\Security; +use Core\Request; +use Core\Response; +use Core\Template; +use Core\Session; use Model\LastLogin; +use Symfony\Component\EventDispatcher\Event; /** * Base controller * * @package controller * @author Frederic Guillot - * - * @property \Model\Acl $acl - * @property \Model\Authentication $authentication - * @property \Model\Action $action - * @property \Model\Board $board - * @property \Model\Category $category - * @property \Model\Color $color - * @property \Model\Comment $comment - * @property \Model\Config $config - * @property \Model\File $file - * @property \Model\LastLogin $lastLogin - * @property \Model\Notification $notification - * @property \Model\Project $project - * @property \Model\ProjectPermission $projectPermission - * @property \Model\SubTask $subTask - * @property \Model\Task $task - * @property \Model\TaskHistory $taskHistory - * @property \Model\TaskExport $taskExport - * @property \Model\TaskFinder $taskFinder - * @property \Model\TaskPermission $taskPermission - * @property \Model\TaskValidator $taskValidator - * @property \Model\CommentHistory $commentHistory - * @property \Model\SubtaskHistory $subtaskHistory - * @property \Model\TimeTracking $timeTracking - * @property \Model\User $user - * @property \Model\Webhook $webhook */ -abstract class Base +abstract class Base extends \Core\Base { /** * Request instance * - * @accesss public + * @accesss protected * @var \Core\Request */ - public $request; + protected $request; /** * Response instance * - * @accesss public + * @accesss protected * @var \Core\Response */ - public $response; - - /** - * Template instance - * - * @accesss public - * @var \Core\Template - */ - public $template; - - /** - * Session instance - * - * @accesss public - * @var \Core\Session - */ - public $session; - - /** - * Registry instance - * - * @access private - * @var \Core\Registry - */ - private $registry; + protected $response; /** * Constructor * * @access public - * @param \Core\Registry $registry Registry instance + * @param \Pimple\Container $container */ - public function __construct(Registry $registry) + public function __construct(Container $container) { - $this->registry = $registry; + $this->container = $container; + $this->request = new Request; + $this->response = new Response; + + if (DEBUG) { + $this->container['logger']->debug('START_REQUEST='.$_SERVER['REQUEST_URI']); + } } /** - * Load automatically models + * Destructor * * @access public - * @param string $name Model name - * @return mixed */ - public function __get($name) + public function __destruct() { - return Tool::loadModel($this->registry, $name); + if (DEBUG) { + + foreach ($this->container['db']->getLogMessages() as $message) { + $this->container['logger']->debug($message); + } + + $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('END_REQUEST='.$_SERVER['REQUEST_URI']); + } } /** - * Method executed before each action + * Send HTTP headers * - * @access public + * @access private */ - public function beforeAction($controller, $action) + private function sendHeaders($action) { - // Start the session - $this->session->open(BASE_URL_DIRECTORY, SESSION_SAVE_PATH); - // 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(); // Allow the public board iframe inclusion - if ($action !== 'readonly') { + if (ENABLE_XFRAME && $action !== 'readonly') { $this->response->xframe(); } if (ENABLE_HSTS) { $this->response->hsts(); } + } + + /** + * Method executed before each action + * + * @access public + */ + public function beforeAction($controller, $action) + { + // Start the session + $this->session->open(BASE_URL_DIRECTORY); + $this->sendHeaders($action); + $this->container['dispatcher']->dispatch('session.bootstrap', new Event); - $this->config->setupTranslations(); - $this->config->setupTimezone(); + if (! $this->acl->isPublicAction($controller, $action)) { + $this->handleAuthentication(); + $this->handle2FA($controller, $action); + $this->handleAuthorization($controller, $action); - // Authentication - if (! $this->authentication->isAuthenticated($controller, $action)) { - $this->response->redirect('?controller=user&action=login&redirect_query='.urlencode($this->request->getQueryString())); + $this->session['has_subtask_inprogress'] = $this->subtask->hasSubtaskInProgress($this->userSession->getId()); } + } - // Check if the user is allowed to see this page - if (! $this->acl->isPageAccessAllowed($controller, $action)) { - $this->response->redirect('?controller=user&action=forbidden'); + /** + * Check authentication + * + * @access public + */ + public function handleAuthentication() + { + if (! $this->authentication->isAuthenticated()) { + + if ($this->request->isAjax()) { + $this->response->text('Not Authorized', 401); + } + + $this->response->redirect($this->helper->url->to('auth', 'login', array('redirect_query' => urlencode($this->request->getQueryString())))); } + } - // Attach events - $this->attachEvents(); + /** + * Check 2FA + * + * @access public + */ + public function handle2FA($controller, $action) + { + $ignore = ($controller === 'twofactor' && in_array($action, array('code', 'check'))) || ($controller === 'auth' && $action === 'logout'); + + if ($ignore === false && $this->userSession->has2FA() && ! $this->userSession->check2FA()) { + + if ($this->request->isAjax()) { + $this->response->text('Not Authorized', 401); + } + + $this->response->redirect($this->helper->url->to('twofactor', 'code')); + } } /** - * Attach events + * Check page access and authorization * - * @access private + * @access public */ - private function attachEvents() + public function handleAuthorization($controller, $action) { - $models = array( - 'projectActivity', // Order is important - 'action', - 'project', - 'webhook', - 'notification', - ); - - foreach ($models as $model) { - $this->$model->attachEvents(); + $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(); } } @@ -173,7 +178,7 @@ abstract class Base */ public function notfound($no_layout = false) { - $this->response->html($this->template->layout('app_notfound', array( + $this->response->html($this->template->layout('app/notfound', array( 'title' => t('Page not found'), 'no_layout' => $no_layout, ))); @@ -187,7 +192,7 @@ abstract class Base */ public function forbidden($no_layout = false) { - $this->response->html($this->template->layout('app_forbidden', array( + $this->response->html($this->template->layout('app/forbidden', array( 'title' => t('Access Forbidden'), 'no_layout' => $no_layout, ))); @@ -206,19 +211,6 @@ abstract class Base } /** - * Check if the current user have access to the given project - * - * @access protected - * @param integer $project_id Project id - */ - protected function checkProjectPermissions($project_id) - { - if ($this->acl->isRegularUser() && ! $this->projectPermission->isUserAllowed($project_id, $this->acl->getUserId())) { - $this->forbidden(); - } - } - - /** * Redirection when there is no project in the database * * @access protected @@ -239,14 +231,12 @@ abstract class Base */ protected function taskLayout($template, array $params) { - if (isset($params['task']) && $this->taskPermission->canRemoveTask($params['task']) === false) { - $params['hide_remove_menu'] = true; - } - - $content = $this->template->load($template, $params); + $content = $this->template->render($template, $params); $params['task_content_for_layout'] = $content; + $params['title'] = $params['task']['project_name'].' > '.$params['task']['title']; + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); - return $this->template->layout('task_layout', $params); + return $this->template->layout('task/layout', $params); } /** @@ -257,13 +247,15 @@ abstract class Base * @param array $params Template parameters * @return string */ - protected function projectLayout($template, array $params) + protected function projectLayout($template, array $params, $sidebar_template = 'project/sidebar') { - $content = $this->template->load($template, $params); + $content = $this->template->render($template, $params); $params['project_content_for_layout'] = $content; - $params['menu'] = 'projects'; + $params['title'] = $params['project']['name'] === $params['title'] ? $params['title'] : $params['project']['name'].' > '.$params['title']; + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); + $params['sidebar_template'] = $sidebar_template; - return $this->template->layout('project_layout', $params); + return $this->template->layout('project/layout', $params); } /** @@ -276,12 +268,10 @@ abstract class Base { $task = $this->taskFinder->getDetails($this->request->getIntegerParam('task_id')); - if (! $task) { + if (empty($task)) { $this->notfound(); } - $this->checkProjectPermissions($task['project_id']); - return $task; } @@ -297,34 +287,11 @@ abstract class Base $project_id = $this->request->getIntegerParam('project_id', $project_id); $project = $this->project->getById($project_id); - if (! $project) { + if (empty($project)) { $this->session->flashError(t('Project not found.')); $this->response->redirect('?controller=project'); } - $this->checkProjectPermissions($project['id']); - - return $project; - } - - /** - * Common method to get a project with administration rights - * - * @access protected - * @return array - */ - protected function getProjectManagement() - { - $project = $this->project->getById($this->request->getIntegerParam('project_id')); - - if (! $project) { - $this->notfound(); - } - - if ($this->acl->isRegularUser() && ! $this->projectPermission->adminAllowed($project['id'], $this->acl->getUserId())) { - $this->forbidden(); - } - return $project; } } diff --git a/app/Controller/Board.php b/app/Controller/Board.php index d49ad021..2b633d82 100644 --- a/app/Controller/Board.php +++ b/app/Controller/Board.php @@ -2,10 +2,6 @@ namespace Controller; -use Model\Project as ProjectModel; -use Model\User as UserModel; -use Core\Security; - /** * Board controller * @@ -15,133 +11,6 @@ use Core\Security; class Board extends Base { /** - * Move a column down or up - * - * @access public - */ - public function moveColumn() - { - $this->checkCSRFParam(); - $project = $this->getProjectManagement(); - $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('?controller=board&action=edit&project_id='.$project['id']); - } - - /** - * Change a task assignee directly from the board - * - * @access public - */ - public function changeAssignee() - { - $task = $this->getTask(); - $project = $this->project->getById($task['project_id']); - $projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); - $params = array( - 'errors' => array(), - 'values' => $task, - 'users_list' => $this->projectPermission->getUsersList($project['id']), - 'projects' => $projects, - 'current_project_id' => $project['id'], - 'current_project_name' => $project['name'], - ); - - if ($this->request->isAjax()) { - - $this->response->html($this->template->load('board_assignee', $params)); - } - else { - - $this->response->html($this->template->layout('board_assignee', $params + array( - 'menu' => 'boards', - 'title' => t('Change assignee').' - '.$task['title'], - ))); - } - } - - /** - * Validate an assignee modification - * - * @access public - */ - public function updateAssignee() - { - $values = $this->request->getValues(); - $this->checkProjectPermissions($values['project_id']); - - list($valid,) = $this->taskValidator->validateAssigneeModification($values); - - if ($valid && $this->task->update($values)) { - $this->session->flash(t('Task updated successfully.')); - } - else { - $this->session->flashError(t('Unable to update your task.')); - } - - $this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']); - } - - /** - * Change a task category directly from the board - * - * @access public - */ - public function changeCategory() - { - $task = $this->getTask(); - $project = $this->project->getById($task['project_id']); - $projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); - $params = array( - 'errors' => array(), - 'values' => $task, - 'categories_list' => $this->category->getList($project['id']), - 'projects' => $projects, - 'current_project_id' => $project['id'], - 'current_project_name' => $project['name'], - ); - - if ($this->request->isAjax()) { - - $this->response->html($this->template->load('board_category', $params)); - } - else { - - $this->response->html($this->template->layout('board_category', $params + array( - 'menu' => 'boards', - 'title' => t('Change category').' - '.$task['title'], - ))); - } - } - - /** - * Validate a category modification - * - * @access public - */ - public function updateCategory() - { - $values = $this->request->getValues(); - $this->checkProjectPermissions($values['project_id']); - - list($valid,) = $this->taskValidator->validateCategoryModification($values); - - if ($valid && $this->task->update($values)) { - $this->session->flash(t('Task updated successfully.')); - } - else { - $this->session->flashError(t('Unable to update your task.')); - } - - $this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']); - } - - /** * Display the public version of a board * Access checked by a simple token, no user login, read only, auto-refresh * @@ -153,19 +22,25 @@ class Board extends Base $project = $this->project->getByToken($token); // Token verification - if (! $project) { + if (empty($project)) { $this->forbidden(true); } + list($categories_listing, $categories_description) = $this->category->getBoardCategories($project['id']); + // Display the board with a specific layout - $this->response->html($this->template->layout('board_public', array( + $this->response->html($this->template->layout('board/public', array( 'project' => $project, - 'columns' => $this->board->get($project['id']), - 'categories' => $this->category->getList($project['id'], false), + 'swimlanes' => $this->board->getBoard($project['id']), + 'categories_listing' => $categories_listing, + 'categories_description' => $categories_description, 'title' => $project['name'], + 'description' => $project['description'], 'no_layout' => true, 'not_editable' => true, 'board_public_refresh_interval' => $this->config->get('board_public_refresh_interval'), + 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), + 'board_highlight_period' => $this->config->get('board_highlight_period'), ))); } @@ -176,16 +51,16 @@ class Board extends Base */ public function index() { - $last_seen_project_id = $this->user->getLastSeenProjectId(); - $favorite_project_id = $this->user->getFavoriteProjectId(); + $last_seen_project_id = $this->userSession->getLastSeenProjectId(); + $favorite_project_id = $this->userSession->getFavoriteProjectId(); $project_id = $last_seen_project_id ?: $favorite_project_id; if (! $project_id) { - $projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); + $projects = $this->projectPermission->getAllowedProjects($this->userSession->getId()); if (empty($projects)) { - if ($this->acl->isAdminUser()) { + if ($this->userSession->isAdmin()) { $this->redirectNoProject(); } @@ -207,23 +82,24 @@ class Board extends Base public function show($project_id = 0) { $project = $this->getProject($project_id); - $projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); + $projects = $this->projectPermission->getAllowedProjects($this->userSession->getId()); $board_selector = $projects; unset($board_selector[$project['id']]); - $this->user->storeLastSeenProjectId($project['id']); + $this->userSession->storeLastSeenProjectId($project['id']); + + list($categories_listing, $categories_description) = $this->category->getBoardCategories($project['id']); - $this->response->html($this->template->layout('board_index', array( - 'users' => $this->projectPermission->getUsersList($project['id'], true, true), - 'filters' => array('user_id' => UserModel::EVERYBODY_ID), + $this->response->html($this->template->layout('board/index', array( + 'users' => $this->projectPermission->getMemberList($project['id'], true, true), 'projects' => $projects, - 'current_project_id' => $project['id'], - 'current_project_name' => $project['name'], - 'board' => $this->board->get($project['id']), - 'categories' => $this->category->getList($project['id'], true, true), - 'menu' => 'boards', + 'project' => $project, + 'swimlanes' => $this->board->getBoard($project['id']), + 'categories_listing' => $categories_listing, + 'categories_description' => $categories_description, 'title' => $project['name'], + 'description' => $project['description'], 'board_selector' => $board_selector, 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), 'board_highlight_period' => $this->config->get('board_highlight_period'), @@ -231,215 +107,264 @@ class Board extends Base } /** - * Display a form to edit a board + * Save the board (Ajax request made by the drag and drop) * * @access public */ - public function edit() + public function save() { - $project = $this->getProjectManagement(); - $columns = $this->board->getColumns($project['id']); - $values = array(); + $project_id = $this->request->getIntegerParam('project_id'); - foreach ($columns as $column) { - $values['title['.$column['id'].']'] = $column['title']; - $values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null; + if (! $project_id || ! $this->request->isAjax()) { + return $this->response->status(403); } - $this->response->html($this->projectLayout('board_edit', array( - 'errors' => array(), - 'values' => $values + array('project_id' => $project['id']), - 'columns' => $columns, - 'project' => $project, - 'menu' => 'projects', - 'title' => t('Edit board') - ))); + if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) { + $this->response->text('Forbidden', 403); + } + + $values = $this->request->getJson(); + + $result =$this->taskPosition->movePosition( + $project_id, + $values['task_id'], + $values['column_id'], + $values['position'], + $values['swimlane_id'] + ); + + if (! $result) { + return $this->response->status(400); + } + + list($categories_listing, $categories_description) = $this->category->getBoardCategories($project_id); + + $this->response->html( + $this->template->render('board/show', array( + 'project' => $this->project->getById($project_id), + 'swimlanes' => $this->board->getBoard($project_id), + 'categories_listing' => $categories_listing, + 'categories_description' => $categories_description, + 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), + 'board_highlight_period' => $this->config->get('board_highlight_period'), + )), + 201 + ); } /** - * Validate and update a board + * Check if the board have been changed * * @access public */ - public function update() + public function check() { - $project = $this->getProjectManagement(); - $columns = $this->board->getColumns($project['id']); - $data = $this->request->getValues(); - $values = $columns_list = array(); - - 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; + if (! $this->request->isAjax()) { + return $this->response->status(403); } - list($valid, $errors) = $this->board->validateModification($columns_list, $values); + $project_id = $this->request->getIntegerParam('project_id'); + $timestamp = $this->request->getIntegerParam('timestamp'); - if ($valid) { + if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) { + $this->response->text('Forbidden', 403); + } - if ($this->board->update($data)) { - $this->session->flash(t('Board updated successfully.')); - $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']); - } - else { - $this->session->flashError(t('Unable to update this board.')); - } + if (! $this->project->isModifiedSince($project_id, $timestamp)) { + return $this->response->status(304); } - $this->response->html($this->projectLayout('board_edit', array( - 'errors' => $errors, - 'values' => $values + array('project_id' => $project['id']), - 'columns' => $columns, - 'project' => $project, - 'menu' => 'projects', - 'title' => t('Edit board') - ))); + list($categories_listing, $categories_description) = $this->category->getBoardCategories($project_id); + + $this->response->html( + $this->template->render('board/show', array( + 'project' => $this->project->getById($project_id), + 'swimlanes' => $this->board->getBoard($project_id), + 'categories_listing' => $categories_listing, + 'categories_description' => $categories_description, + 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), + 'board_highlight_period' => $this->config->get('board_highlight_period'), + )) + ); } /** - * Validate and add a new column + * Get links on mouseover * * @access public */ - public function add() + public function tasklinks() { - $project = $this->getProjectManagement(); - $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); + $task = $this->getTask(); + $this->response->html($this->template->render('board/tasklinks', array( + 'links' => $this->taskLink->getAll($task['id']), + 'task' => $task, + ))); + } - if ($valid) { + /** + * Get subtasks on mouseover + * + * @access public + */ + public function subtasks() + { + $task = $this->getTask(); + $this->response->html($this->template->render('board/subtasks', array( + 'subtasks' => $this->subtask->getAll($task['id']), + 'task' => $task, + ))); + } - if ($this->board->addColumn($project['id'], $data['title'])) { - $this->session->flash(t('Board updated successfully.')); - $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']); - } - else { - $this->session->flashError(t('Unable to update this board.')); - } - } + /** + * Display all attachments during the task mouseover + * + * @access public + */ + public function attachments() + { + $task = $this->getTask(); - $this->response->html($this->projectLayout('board_edit', array( - 'errors' => $errors, - 'values' => $values + $data, - 'columns' => $columns, - 'project' => $project, - 'menu' => 'projects', - 'title' => t('Edit board') + $this->response->html($this->template->render('board/files', array( + 'files' => $this->file->getAllDocuments($task['id']), + 'images' => $this->file->getAllImages($task['id']), + 'task' => $task, ))); } /** - * Remove a column + * Display comments during a task mouseover * * @access public */ - public function remove() + public function comments() { - $project = $this->getProjectManagement(); + $task = $this->getTask(); - if ($this->request->getStringParam('remove') === 'yes') { + $this->response->html($this->template->render('board/comments', array( + 'comments' => $this->comment->getAll($task['id']) + ))); + } - $this->checkCSRFParam(); - $column = $this->board->getColumn($this->request->getIntegerParam('column_id')); + /** + * Display task description + * + * @access public + */ + public function description() + { + $task = $this->getTask(); - if ($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->html($this->template->render('board/description', array( + 'task' => $task + ))); + } - $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']); - } + /** + * Change a task assignee directly from the board + * + * @access public + */ + public function changeAssignee() + { + $task = $this->getTask(); + $project = $this->project->getById($task['project_id']); - $this->response->html($this->projectLayout('board_remove', array( - 'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')), + $this->response->html($this->template->render('board/assignee', array( + 'values' => $task, + 'users_list' => $this->projectPermission->getMemberList($project['id']), 'project' => $project, - 'menu' => 'projects', - 'title' => t('Remove a column from a board') ))); } /** - * Save the board (Ajax request made by the drag and drop) + * Validate an assignee modification * * @access public */ - public function save() + public function updateAssignee() { - $project_id = $this->request->getIntegerParam('project_id'); + $values = $this->request->getValues(); - if ($project_id > 0 && $this->request->isAjax()) { + list($valid,) = $this->taskValidator->validateAssigneeModification($values); - if (! $this->projectPermission->isUserAllowed($project_id, $this->acl->getUserId())) { - $this->response->status(401); - } + if ($valid && $this->taskModification->update($values)) { + $this->session->flash(t('Task updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update your task.')); + } - $values = $this->request->getValues(); + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id']))); + } - if ($this->task->movePosition($project_id, $values['task_id'], $values['column_id'], $values['position'])) { + /** + * Change a task category directly from the board + * + * @access public + */ + public function changeCategory() + { + $task = $this->getTask(); + $project = $this->project->getById($task['project_id']); - $this->response->html( - $this->template->load('board_show', array( - 'current_project_id' => $project_id, - 'board' => $this->board->get($project_id), - 'categories' => $this->category->getList($project_id, false), - 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), - 'board_highlight_period' => $this->config->get('board_highlight_period'), - )), - 201 - ); - } - else { + $this->response->html($this->template->render('board/category', array( + 'values' => $task, + 'categories_list' => $this->category->getList($project['id']), + 'project' => $project, + ))); + } - $this->response->status(400); - } + /** + * Validate a category modification + * + * @access public + */ + public function updateCategory() + { + $values = $this->request->getValues(); + + list($valid,) = $this->taskValidator->validateCategoryModification($values); + + if ($valid && $this->taskModification->update($values)) { + $this->session->flash(t('Task updated successfully.')); } else { - $this->response->status(401); + $this->session->flashError(t('Unable to update your task.')); } + + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id']))); } /** - * Check if the board have been changed + * Screenshot popover * * @access public */ - public function check() + public function screenshot() { - if ($this->request->isAjax()) { + $task = $this->getTask(); - $project_id = $this->request->getIntegerParam('project_id'); - $timestamp = $this->request->getIntegerParam('timestamp'); + $this->response->html($this->template->render('file/screenshot', array( + 'task' => $task, + 'redirect' => 'board', + ))); + } - if ($project_id > 0 && ! $this->projectPermission->isUserAllowed($project_id, $this->acl->getUserId())) { - $this->response->text('Not Authorized', 401); - } + /** + * Get recurrence information on mouseover + * + * @access public + */ + public function recurrence() + { + $task = $this->getTask(); - if ($this->project->isModifiedSince($project_id, $timestamp)) { - $this->response->html( - $this->template->load('board_show', array( - 'current_project_id' => $project_id, - 'board' => $this->board->get($project_id), - 'categories' => $this->category->getList($project_id, false), - 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), - 'board_highlight_period' => $this->config->get('board_highlight_period'), - )) - ); - } - else { - $this->response->status(304); - } - } - else { - $this->response->status(401); - } + $this->response->html($this->template->render('task/recurring_info', array( + 'task' => $task, + 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), + 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), + 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), + ))); } } diff --git a/app/Controller/Budget.php b/app/Controller/Budget.php new file mode 100644 index 00000000..a2f7e0db --- /dev/null +++ b/app/Controller/Budget.php @@ -0,0 +1,135 @@ +<?php + +namespace Controller; + +/** + * Budget + * + * @package controller + * @author Frederic Guillot + */ +class Budget extends Base +{ + /** + * Budget index page + * + * @access public + */ + public function index() + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('budget/index', array( + 'daily_budget' => $this->budget->getDailyBudgetBreakdown($project['id']), + 'project' => $project, + 'title' => t('Budget') + ), 'budget/sidebar')); + } + + /** + * Cost breakdown by users/subtasks/tasks + * + * @access public + */ + public function breakdown() + { + $project = $this->getProject(); + + $paginator = $this->paginator + ->setUrl('budget', 'breakdown', array('project_id' => $project['id'])) + ->setMax(30) + ->setOrder('start') + ->setDirection('DESC') + ->setQuery($this->budget->getSubtaskBreakdown($project['id'])) + ->calculate(); + + $this->response->html($this->projectLayout('budget/breakdown', array( + 'paginator' => $paginator, + 'project' => $project, + 'title' => t('Budget') + ), 'budget/sidebar')); + } + + /** + * Create budget lines + * + * @access public + */ + public function create(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + + if (empty($values)) { + $values['date'] = date('Y-m-d'); + } + + $this->response->html($this->projectLayout('budget/create', array( + 'lines' => $this->budget->getAll($project['id']), + 'values' => $values + array('project_id' => $project['id']), + 'errors' => $errors, + 'project' => $project, + 'title' => t('Budget lines') + ), 'budget/sidebar')); + } + + /** + * Validate and save a new budget + * + * @access public + */ + public function save() + { + $project = $this->getProject(); + + $values = $this->request->getValues(); + list($valid, $errors) = $this->budget->validateCreation($values); + + if ($valid) { + + if ($this->budget->create($values['project_id'], $values['amount'], $values['comment'], $values['date'])) { + $this->session->flash(t('The budget line have been created successfully.')); + $this->response->redirect($this->helper->url->to('budget', 'create', array('project_id' => $project['id']))); + } + else { + $this->session->flashError(t('Unable to create the budget line.')); + } + } + + $this->create($values, $errors); + } + + /** + * Confirmation dialog before removing a budget + * + * @access public + */ + public function confirm() + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('budget/remove', array( + 'project' => $project, + 'budget_id' => $this->request->getIntegerParam('budget_id'), + 'title' => t('Remove a budget line'), + ), 'budget/sidebar')); + } + + /** + * Remove a budget + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + + if ($this->budget->remove($this->request->getIntegerParam('budget_id'))) { + $this->session->flash(t('Budget line removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this budget line.')); + } + + $this->response->redirect($this->helper->url->to('budget', 'create', array('project_id' => $project['id']))); + } +} diff --git a/app/Controller/Calendar.php b/app/Controller/Calendar.php new file mode 100644 index 00000000..41642a59 --- /dev/null +++ b/app/Controller/Calendar.php @@ -0,0 +1,128 @@ +<?php + +namespace Controller; + +use Model\Task as TaskModel; + +/** + * Project Calendar controller + * + * @package controller + * @author Frederic Guillot + * @author Timo Litzbarski + */ +class Calendar extends Base +{ + /** + * Show calendar view for projects + * + * @access public + */ + public function show() + { + $project = $this->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'); + + // Common filter + $filter = $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)); + + // Tasks + if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') { + $events = $filter->copy()->filterByCreationDateRange($start, $end)->toDateTimeCalendarEvents('date_creation', 'date_completed'); + } + else { + $events = $filter->copy()->filterByStartDateRange($start, $end)->toDateTimeCalendarEvents('date_started', 'date_completed'); + } + + // Tasks with due date + $events = array_merge($events, $filter->copy()->filterByDueDateRange($start, $end)->toAllDayCalendarEvents()); + + $this->response->json($events); + } + + /** + * 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'); + $filter = $this->taskFilter->create()->filterByOwner($user_id)->filterByStatus(TaskModel::STATUS_OPEN); + + // Task with due date + $events = $filter->copy()->filterByDueDateRange($start, $end)->toAllDayCalendarEvents(); + + // Tasks + if ($this->config->get('calendar_user_tasks', 'date_started') === 'date_creation') { + $events = array_merge($events, $filter->copy()->filterByCreationDateRange($start, $end)->toDateTimeCalendarEvents('date_creation', 'date_completed')); + } + else { + $events = array_merge($events, $filter->copy()->filterByStartDateRange($start, $end)->toDateTimeCalendarEvents('date_started', 'date_completed')); + } + + // Subtasks time tracking + if ($this->config->get('calendar_user_subtasks_time_tracking') == 1) { + $events = array_merge($events, $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end)); + } + + // Subtask estimates + if ($this->config->get('calendar_user_subtasks_forecast') == 1) { + $events = array_merge($events, $this->subtaskForecast->getCalendarEvents($user_id, $end)); + } + + $this->response->json($events); + } + + /** + * Update task due date + * + * @access public + */ + public function save() + { + if ($this->request->isAjax() && $this->request->isPost()) { + + $values = $this->request->getJson(); + + $this->taskModification->update(array( + 'id' => $values['task_id'], + 'date_due' => substr($values['date_due'], 0, 10), + )); + } + } +} diff --git a/app/Controller/Category.php b/app/Controller/Category.php index 38322294..515cc9c8 100644 --- a/app/Controller/Category.php +++ b/app/Controller/Category.php @@ -14,14 +14,14 @@ class Category extends Base * Get the category (common method between actions) * * @access private - * @param $project_id + * @param integer $project_id * @return array */ private function getCategory($project_id) { $category = $this->category->getById($this->request->getIntegerParam('category_id')); - if (! $category) { + if (empty($category)) { $this->session->flashError(t('Category not found.')); $this->response->redirect('?controller=category&action=index&project_id='.$project_id); } @@ -34,28 +34,27 @@ class Category extends Base * * @access public */ - public function index() + public function index(array $values = array(), array $errors = array()) { - $project = $this->getProjectManagement(); + $project = $this->getProject(); - $this->response->html($this->projectLayout('category_index', array( + $this->response->html($this->projectLayout('category/index', array( 'categories' => $this->category->getList($project['id'], false), - 'values' => array('project_id' => $project['id']), - 'errors' => array(), + 'values' => $values + array('project_id' => $project['id']), + 'errors' => $errors, 'project' => $project, - 'menu' => 'projects', 'title' => t('Categories') ))); } /** - * Validate and save a new project + * Validate and save a new category * * @access public */ public function save() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues(); list($valid, $errors) = $this->category->validateCreation($values); @@ -71,14 +70,7 @@ class Category extends Base } } - $this->response->html($this->projectLayout('category_index', array( - 'categories' => $this->category->getList($project['id'], false), - 'values' => $values, - 'errors' => $errors, - 'project' => $project, - 'menu' => 'projects', - 'title' => t('Categories') - ))); + $this->index($values, $errors); } /** @@ -86,16 +78,15 @@ class Category extends Base * * @access public */ - public function edit() + public function edit(array $values = array(), array $errors = array()) { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $category = $this->getCategory($project['id']); - $this->response->html($this->projectLayout('category_edit', array( - 'values' => $category, - 'errors' => array(), + $this->response->html($this->projectLayout('category/edit', array( + 'values' => empty($values) ? $category : $values, + 'errors' => $errors, 'project' => $project, - 'menu' => 'projects', 'title' => t('Categories') ))); } @@ -107,7 +98,7 @@ class Category extends Base */ public function update() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues(); list($valid, $errors) = $this->category->validateModification($values); @@ -123,13 +114,7 @@ class Category extends Base } } - $this->response->html($this->projectLayout('category_edit', array( - 'values' => $values, - 'errors' => $errors, - 'project' => $project, - 'menu' => 'projects', - 'title' => t('Categories') - ))); + $this->edit($values, $errors); } /** @@ -139,13 +124,12 @@ class Category extends Base */ public function confirm() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $category = $this->getCategory($project['id']); - $this->response->html($this->projectLayout('category_remove', array( + $this->response->html($this->projectLayout('category/remove', array( 'project' => $project, 'category' => $category, - 'menu' => 'projects', 'title' => t('Remove a category') ))); } @@ -158,7 +142,7 @@ class Category extends Base public function remove() { $this->checkCSRFParam(); - $project = $this->getProjectManagement(); + $project = $this->getProject(); $category = $this->getCategory($project['id']); if ($this->category->remove($category['id'])) { diff --git a/app/Controller/Column.php b/app/Controller/Column.php new file mode 100644 index 00000000..89c495a6 --- /dev/null +++ b/app/Controller/Column.php @@ -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']))); + } +} diff --git a/app/Controller/Comment.php b/app/Controller/Comment.php index a9032ed8..a5f6b1f8 100644 --- a/app/Controller/Comment.php +++ b/app/Controller/Comment.php @@ -20,13 +20,12 @@ class Comment extends Base { $comment = $this->comment->getById($this->request->getIntegerParam('comment_id')); - if (! $comment) { + if (empty($comment)) { $this->notfound(); } - if (! $this->acl->isAdminUser() && $comment['user_id'] != $this->acl->getUserId()) { - $this->response->html($this->template->layout('comment_forbidden', array( - 'menu' => 'tasks', + if (! $this->userSession->isAdmin() && $comment['user_id'] != $this->userSession->getId()) { + $this->response->html($this->template->layout('comment/forbidden', array( 'title' => t('Access Forbidden') ))); } @@ -39,19 +38,32 @@ class Comment extends Base * * @access public */ - public function create() + public function create(array $values = array(), array $errors = array()) { $task = $this->getTask(); + $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); - $this->response->html($this->taskLayout('comment_create', array( - 'values' => array( - 'user_id' => $this->acl->getUserId(), + if (empty($values)) { + $values = array( + 'user_id' => $this->userSession->getId(), 'task_id' => $task['id'], - ), - 'errors' => array(), + ); + } + + 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, - 'menu' => 'tasks', - 'title' => t('Add a comment') + 'title' => t('Add a comment'), ))); } @@ -64,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); @@ -76,16 +89,14 @@ class Comment extends Base $this->session->flashError(t('Unable to create your comment.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comments'); + 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'); } - $this->response->html($this->taskLayout('comment_create', array( - 'values' => $values, - 'errors' => $errors, - 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Add a comment') - ))); + $this->create($values, $errors); } /** @@ -93,17 +104,16 @@ class Comment extends Base * * @access public */ - public function edit() + public function edit(array $values = array(), array $errors = array()) { $task = $this->getTask(); $comment = $this->getComment(); - $this->response->html($this->taskLayout('comment_edit', array( - 'values' => $comment, - 'errors' => array(), + $this->response->html($this->taskLayout('comment/edit', array( + 'values' => empty($values) ? $comment : $values, + 'errors' => $errors, 'comment' => $comment, 'task' => $task, - 'menu' => 'tasks', 'title' => t('Edit a comment') ))); } @@ -130,17 +140,10 @@ class Comment extends Base $this->session->flashError(t('Unable to update your comment.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comment-'.$comment['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#comment-'.$comment['id']); } - $this->response->html($this->taskLayout('comment_edit', array( - 'values' => $values, - 'errors' => $errors, - 'comment' => $comment, - 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Edit a comment') - ))); + $this->edit($values, $errors); } /** @@ -153,10 +156,9 @@ class Comment extends Base $task = $this->getTask(); $comment = $this->getComment(); - $this->response->html($this->taskLayout('comment_remove', array( + $this->response->html($this->taskLayout('comment/remove', array( 'comment' => $comment, 'task' => $task, - 'menu' => 'tasks', 'title' => t('Remove a comment') ))); } @@ -179,6 +181,6 @@ class Comment extends Base $this->session->flashError(t('Unable to remove this comment.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comments'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#comments'); } } diff --git a/app/Controller/Config.php b/app/Controller/Config.php index 7c8373c3..fbd374ab 100644 --- a/app/Controller/Config.php +++ b/app/Controller/Config.php @@ -20,12 +20,12 @@ class Config extends Base */ private function layout($template, array $params) { + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); $params['values'] = $this->config->getAll(); $params['errors'] = array(); - $params['menu'] = 'config'; - $params['config_content_for_layout'] = $this->template->load($template, $params); + $params['config_content_for_layout'] = $this->template->render($template, $params); - return $this->template->layout('config_layout', $params); + return $this->template->layout('config/layout', $params); } /** @@ -38,7 +38,19 @@ class Config extends Base { if ($this->request->isPost()) { - $values = $this->request->getValues(); + $values = $this->request->getValues(); + + switch ($redirect) { + case 'project': + $values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0); + break; + case 'integrations': + $values += array('integration_slack_webhook' => 0, 'integration_hipchat' => 0, 'integration_gravatar' => 0, 'integration_jabber' => 0); + break; + case 'calendar': + $values += array('calendar_user_subtasks_forecast' => 0, 'calendar_user_subtasks_time_tracking' => 0); + break; + } if ($this->config->save($values)) { $this->config->reload(); @@ -59,9 +71,9 @@ class Config extends Base */ public function index() { - $this->response->html($this->layout('config_about', array( + $this->response->html($this->layout('config/about', array( 'db_size' => $this->config->getDatabaseSize(), - 'title' => t('About'), + 'title' => t('Settings').' > '.t('About'), ))); } @@ -74,11 +86,26 @@ class Config extends Base { $this->common('application'); - $this->response->html($this->layout('config_application', array( - 'title' => t('Application settings'), + $this->response->html($this->layout('config/application', array( 'languages' => $this->config->getLanguages(), 'timezones' => $this->config->getTimezones(), 'date_formats' => $this->dateParser->getAvailableFormats(), + 'title' => t('Settings').' > '.t('Application settings'), + ))); + } + + /** + * Display the project settings page + * + * @access public + */ + public function project() + { + $this->common('project'); + + $this->response->html($this->layout('config/project', array( + 'default_columns' => implode(', ', $this->board->getDefaultColumns()), + 'title' => t('Settings').' > '.t('Project settings'), ))); } @@ -91,9 +118,36 @@ class Config extends Base { $this->common('board'); - $this->response->html($this->layout('config_board', array( - 'title' => t('Board settings'), - 'default_columns' => implode(', ', $this->board->getDefaultColumns()), + $this->response->html($this->layout('config/board', array( + 'title' => t('Settings').' > '.t('Board settings'), + ))); + } + + /** + * Display the calendar settings page + * + * @access public + */ + public function calendar() + { + $this->common('calendar'); + + $this->response->html($this->layout('config/calendar', array( + 'title' => t('Settings').' > '.t('Calendar settings'), + ))); + } + + /** + * Display the integration settings page + * + * @access public + */ + public function integrations() + { + $this->common('integrations'); + + $this->response->html($this->layout('config/integrations', array( + 'title' => t('Settings').' > '.t('Integrations'), ))); } @@ -106,8 +160,8 @@ class Config extends Base { $this->common('webhook'); - $this->response->html($this->layout('config_webhook', array( - 'title' => t('Webhook settings'), + $this->response->html($this->layout('config/webhook', array( + 'title' => t('Settings').' > '.t('Webhook settings'), ))); } @@ -118,8 +172,8 @@ class Config extends Base */ public function api() { - $this->response->html($this->layout('config_api', array( - 'title' => t('API'), + $this->response->html($this->layout('config/api', array( + 'title' => t('Settings').' > '.t('API'), ))); } diff --git a/app/Controller/Currency.php b/app/Controller/Currency.php new file mode 100644 index 00000000..10fb90da --- /dev/null +++ b/app/Controller/Currency.php @@ -0,0 +1,89 @@ +<?php + +namespace Controller; + +/** + * Currency controller + * + * @package controller + * @author Frederic Guillot + */ +class Currency extends Base +{ + /** + * Common layout for config 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['config_content_for_layout'] = $this->template->render($template, $params); + + return $this->template->layout('config/layout', $params); + } + + /** + * Display all currency rates and form + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $this->response->html($this->layout('currency/index', array( + 'config_values' => array('application_currency' => $this->config->get('application_currency')), + 'values' => $values, + 'errors' => $errors, + 'rates' => $this->currency->getAll(), + 'currencies' => $this->config->getCurrencies(), + 'title' => t('Settings').' > '.t('Currency rates'), + ))); + } + + /** + * Validate and save a new currency rate + * + * @access public + */ + public function create() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->currency->validate($values); + + if ($valid) { + + if ($this->currency->create($values['currency'], $values['rate'])) { + $this->session->flash(t('The currency rate have been added successfully.')); + $this->response->redirect($this->helper->url->to('currency', 'index')); + } + else { + $this->session->flashError(t('Unable to add this currency rate.')); + } + } + + $this->index($values, $errors); + } + + /** + * Save reference currency + * + * @access public + */ + public function reference() + { + $values = $this->request->getValues(); + + if ($this->config->save($values)) { + $this->config->reload(); + $this->session->flash(t('Settings saved successfully.')); + } + else { + $this->session->flashError(t('Unable to save your settings.')); + } + + $this->response->redirect($this->helper->url->to('currency', 'index')); + } +} diff --git a/app/Controller/Export.php b/app/Controller/Export.php new file mode 100644 index 00000000..117fb5ee --- /dev/null +++ b/app/Controller/Export.php @@ -0,0 +1,85 @@ +<?php + +namespace Controller; + +/** + * Export controller + * + * @package controller + * @author Frederic Guillot + */ +class Export extends Base +{ + /** + * Common export method + * + * @access private + */ + private function common($model, $method, $filename, $action, $page_title) + { + $project = $this->getProject(); + $from = $this->request->getStringParam('from'); + $to = $this->request->getStringParam('to'); + + if ($from && $to) { + $data = $this->$model->$method($project['id'], $from, $to); + $this->response->forceDownload($filename.'.csv'); + $this->response->csv($data); + } + + $this->response->html($this->projectLayout('export/'.$action, array( + 'values' => array( + 'controller' => 'export', + 'action' => $action, + 'project_id' => $project['id'], + 'from' => $from, + 'to' => $to, + ), + 'errors' => array(), + 'date_format' => $this->config->get('application_date_format'), + 'date_formats' => $this->dateParser->getAvailableFormats(), + 'project' => $project, + 'title' => $page_title, + ), 'export/sidebar')); + } + + /** + * Task export + * + * @access public + */ + public function tasks() + { + $this->common('taskExport', 'export', t('Tasks'), 'tasks', t('Tasks Export')); + } + + /** + * Subtask export + * + * @access public + */ + public function subtasks() + { + $this->common('subtaskExport', 'export', t('Subtasks'), 'subtasks', t('Subtasks Export')); + } + + /** + * Daily project summary export + * + * @access public + */ + public function summary() + { + $this->common('projectDailySummary', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export')); + } + + /** + * Transition export + * + * @access public + */ + public function transitions() + { + $this->common('transition', 'export', t('Transitions'), 'transitions', t('Task transitions export')); + } +} diff --git a/app/Controller/File.php b/app/Controller/File.php index 3c8c32d1..f0367537 100644 --- a/app/Controller/File.php +++ b/app/Controller/File.php @@ -2,8 +2,6 @@ namespace Controller; -use Model\File as FileModel; - /** * File controller * @@ -13,6 +11,32 @@ use Model\File as FileModel; class File extends Base { /** + * Screenshot + * + * @access public + */ + public function screenshot() + { + $task = $this->getTask(); + + if ($this->request->isPost() && $this->file->uploadScreenshot($task['project_id'], $task['id'], $this->request->getValue('screenshot'))) { + + $this->session->flash(t('Screenshot uploaded successfully.')); + + if ($this->request->getStringParam('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']))); + } + + $this->response->html($this->taskLayout('file/screenshot', array( + 'task' => $task, + 'redirect' => 'task', + ))); + } + + /** * File upload form * * @access public @@ -21,11 +45,9 @@ class File extends Base { $task = $this->getTask(); - $this->response->html($this->taskLayout('file_new', array( + $this->response->html($this->taskLayout('file/new', array( 'task' => $task, - 'menu' => 'tasks', 'max_size' => ini_get('upload_max_filesize'), - 'title' => t('Attach a document') ))); } @@ -38,13 +60,11 @@ class File extends Base { $task = $this->getTask(); - if ($this->file->upload($task['project_id'], $task['id'], 'files') === true) { - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#attachments'); - } - else { + if (! $this->file->upload($task['project_id'], $task['id'], 'files')) { $this->session->flashError(t('Unable to upload the file.')); - $this->response->redirect('?controller=file&action=create&task_id='.$task['id']); } + + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); } /** @@ -56,14 +76,14 @@ 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']); $this->response->binary(file_get_contents($filename)); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); } /** @@ -77,8 +97,9 @@ class File extends Base $file = $this->file->getById($this->request->getIntegerParam('file_id')); if ($file['task_id'] == $task['id']) { - $this->response->html($this->template->load('file_open', array( - 'file' => $file + $this->response->html($this->template->render('file/open', array( + 'file' => $file, + 'task' => $task, ))); } } @@ -92,7 +113,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); @@ -105,6 +126,28 @@ class File extends Base } /** + * Return image thumbnails + * + * @access public + */ + public function thumbnail() + { + $task = $this->getTask(); + $file = $this->file->getById($this->request->getIntegerParam('file_id')); + $filename = FILES_DIR.$file['path']; + + if ($file['task_id'] == $task['id'] && file_exists($filename)) { + + $this->response->contentType('image/jpeg'); + $this->file->generateThumbnail( + $filename, + $this->request->getIntegerParam('width'), + $this->request->getIntegerParam('height') + ); + } + } + + /** * Remove a file * * @access public @@ -121,7 +164,7 @@ class File extends Base $this->session->flashError(t('Unable to remove this file.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); } /** @@ -134,11 +177,9 @@ class File extends Base $task = $this->getTask(); $file = $this->file->getById($this->request->getIntegerParam('file_id')); - $this->response->html($this->taskLayout('file_remove', array( + $this->response->html($this->taskLayout('file/remove', array( 'task' => $task, 'file' => $file, - 'menu' => 'tasks', - 'title' => t('Remove a file') ))); } } diff --git a/app/Controller/Hourlyrate.php b/app/Controller/Hourlyrate.php new file mode 100644 index 00000000..19650ede --- /dev/null +++ b/app/Controller/Hourlyrate.php @@ -0,0 +1,89 @@ +<?php + +namespace Controller; + +/** + * Hourly Rate controller + * + * @package controller + * @author Frederic Guillot + */ +class Hourlyrate extends User +{ + /** + * Display rate and form + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $user = $this->getUser(); + + $this->response->html($this->layout('hourlyrate/index', array( + 'rates' => $this->hourlyRate->getAllByUser($user['id']), + 'currencies_list' => $this->config->getCurrencies(), + 'values' => $values + array('user_id' => $user['id']), + 'errors' => $errors, + 'user' => $user, + ))); + } + + /** + * Validate and save a new rate + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->hourlyRate->validateCreation($values); + + if ($valid) { + + if ($this->hourlyRate->create($values['user_id'], $values['rate'], $values['currency'], $values['date_effective'])) { + $this->session->flash(t('Hourly rate created successfully.')); + $this->response->redirect($this->helper->url->to('hourlyrate', 'index', array('user_id' => $values['user_id']))); + } + else { + $this->session->flashError(t('Unable to save the hourly rate.')); + } + } + + $this->index($values, $errors); + } + + /** + * Confirmation dialag box to remove a row + * + * @access public + */ + public function confirm() + { + $user = $this->getUser(); + + $this->response->html($this->layout('hourlyrate/remove', array( + 'rate_id' => $this->request->getIntegerParam('rate_id'), + 'user' => $user, + ))); + } + + /** + * Remove a row + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $user = $this->getUser(); + + if ($this->hourlyRate->remove($this->request->getIntegerParam('rate_id'))) { + $this->session->flash(t('Rate removed successfully.')); + } + else { + $this->session->flash(t('Unable to remove this rate.')); + } + + $this->response->redirect($this->helper->url->to('hourlyrate', 'index', array('user_id' => $user['id']))); + } +} diff --git a/app/Controller/Ical.php b/app/Controller/Ical.php new file mode 100644 index 00000000..52e10fa1 --- /dev/null +++ b/app/Controller/Ical.php @@ -0,0 +1,99 @@ +<?php + +namespace Controller; + +use Model\TaskFilter; +use Model\Task as TaskModel; +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('-1 month')); + $end = $this->request->getStringParam('end', strtotime('+2 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(); + } +} diff --git a/app/Controller/Link.php b/app/Controller/Link.php new file mode 100644 index 00000000..482e415c --- /dev/null +++ b/app/Controller/Link.php @@ -0,0 +1,162 @@ +<?php + +namespace Controller; + +/** + * Link controller + * + * @package controller + * @author Olivier Maridat + * @author Frederic Guillot + */ +class Link extends Base +{ + /** + * Common layout for config 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['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 (empty($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']) !== false) { + $this->session->flash(t('Link added successfully.')); + $this->response->redirect($this->helper->url->to('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->to('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->to('link', 'index')); + } +} diff --git a/app/Controller/Project.php b/app/Controller/Project.php index d749ef53..ba039b7d 100644 --- a/app/Controller/Project.php +++ b/app/Controller/Project.php @@ -2,10 +2,8 @@ namespace Controller; -use Model\Task as TaskModel; - /** - * Project controller + * Project controller (Settings + creation/edition) * * @package controller * @author Frederic Guillot @@ -19,25 +17,26 @@ class Project extends Base */ public function index() { - $projects = $this->project->getAll($this->acl->isRegularUser()); - $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); - $this->response->html($this->template->layout('project_index', array( - 'active_projects' => $active_projects, - 'inactive_projects' => $inactive_projects, + $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()), + 'paginator' => $paginator, 'nb_projects' => $nb_projects, - 'menu' => 'projects', 'title' => t('Projects').' ('.$nb_projects.')' ))); } @@ -51,54 +50,21 @@ class Project extends Base { $project = $this->getProject(); - $this->response->html($this->projectLayout('project_show', array( + $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'], ))); } /** - * Task export - * - * @access public - */ - public function export() - { - $project = $this->getProjectManagement(); - $from = $this->request->getStringParam('from'); - $to = $this->request->getStringParam('to'); - - if ($from && $to) { - $data = $this->taskExport->export($project['id'], $from, $to); - $this->response->forceDownload('Export_'.date('Y_m_d_H_i_S').'.csv'); - $this->response->csv($data); - } - - $this->response->html($this->projectLayout('project_export', array( - 'values' => array( - 'controller' => 'project', - 'action' => 'export', - 'project_id' => $project['id'], - 'from' => $from, - 'to' => $to, - ), - 'errors' => array(), - 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), - 'project' => $project, - 'title' => t('Tasks Export') - ))); - } - - /** * Public access management * * @access public */ public function share() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $switch = $this->request->getStringParam('switch'); if ($switch === 'enable' || $switch === 'disable') { @@ -114,24 +80,51 @@ class Project extends Base $this->response->redirect('?controller=project&action=share&project_id='.$project['id']); } - $this->response->html($this->projectLayout('project_share', array( + $this->response->html($this->projectLayout('project/share', array( 'project' => $project, 'title' => t('Public access'), ))); } /** - * Display a form to edit a project + * Integrations page * * @access public */ - public function edit() + public function integration() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); + + if ($this->request->isPost()) { + $params = $this->request->getValues(); + $params += array('hipchat' => 0, 'slack' => 0, 'jabber' => 0); + $this->projectIntegration->saveParameters($project['id'], $params); + } - $this->response->html($this->projectLayout('project_edit', array( + $values = $this->projectIntegration->getParameters($project['id']); + $values += array('hipchat_api_url' => 'https://api.hipchat.com'); + + $this->response->html($this->projectLayout('project/integrations', array( + 'project' => $project, + 'title' => t('Integrations'), + 'webhook_token' => $this->config->get('webhook_token'), + 'values' => $values, 'errors' => array(), - 'values' => $project, + ))); + } + + /** + * Display a form to edit a project + * + * @access public + */ + public function edit(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('project/edit', array( + 'values' => empty($values) ? $project : $values, + 'errors' => $errors, 'project' => $project, 'title' => t('Edit project') ))); @@ -144,8 +137,13 @@ class Project extends Base */ public function update() { - $project = $this->getProjectManagement(); - $values = $this->request->getValues() + array('is_active' => 0); + $project = $this->getProject(); + $values = $this->request->getValues(); + + if ($project['is_private'] == 1 && $this->userSession->isAdmin() && ! isset($values['is_private'])) { + $values += array('is_private' => 0); + } + list($valid, $errors) = $this->project->validateModification($values); if ($valid) { @@ -159,12 +157,7 @@ class Project extends Base } } - $this->response->html($this->projectLayout('project_edit', array( - 'errors' => $errors, - 'values' => $values, - 'project' => $project, - 'title' => t('Edit Project') - ))); + $this->edit($values, $errors); } /** @@ -174,9 +167,9 @@ class Project extends Base */ public function users() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); - $this->response->html($this->projectLayout('project_users', array( + $this->response->html($this->projectLayout('project/users', array( 'project' => $project, 'users' => $this->projectPermission->getAllUsers($project['id']), 'title' => t('Edit project access list') @@ -190,7 +183,7 @@ class Project extends Base */ public function allowEverybody() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues() + array('is_everybody_allowed' => 0); list($valid,) = $this->projectPermission->validateProjectModification($values); @@ -219,7 +212,37 @@ class Project extends Base if ($valid) { - if ($this->projectPermission->allowUser($values['project_id'], $values['user_id'])) { + if ($this->projectPermission->addMember($values['project_id'], $values['user_id'])) { + $this->session->flash(t('Project updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update this project.')); + } + } + + $this->response->redirect('?controller=project&action=users&project_id='.$values['project_id']); + } + + /** + * Change the role of a project member + * + * @access public + */ + public function role() + { + $this->checkCSRFParam(); + + $values = array( + 'project_id' => $this->request->getIntegerParam('project_id'), + 'user_id' => $this->request->getIntegerParam('user_id'), + 'is_owner' => $this->request->getIntegerParam('is_owner'), + ); + + list($valid,) = $this->projectPermission->validateUserModification($values); + + if ($valid) { + + if ($this->projectPermission->changeRole($values['project_id'], $values['user_id'], $values['is_owner'])) { $this->session->flash(t('Project updated successfully.')); } else { @@ -248,7 +271,7 @@ class Project extends Base if ($valid) { - if ($this->projectPermission->revokeUser($values['project_id'], $values['user_id'])) { + if ($this->projectPermission->revokeMember($values['project_id'], $values['user_id'])) { $this->session->flash(t('Project updated successfully.')); } else { @@ -266,7 +289,7 @@ class Project extends Base */ public function remove() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); if ($this->request->getStringParam('remove') === 'yes') { @@ -281,7 +304,7 @@ class Project extends Base $this->response->redirect('?controller=project'); } - $this->response->html($this->projectLayout('project_remove', array( + $this->response->html($this->projectLayout('project/remove', array( 'project' => $project, 'title' => t('Remove project') ))); @@ -291,17 +314,16 @@ class Project extends Base * Duplicate a project * * @author Antonio Rabelo + * @author Michael Lüpkes * @access public */ public function duplicate() { - $project = $this->getProjectManagement(); + $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) !== false) { $this->session->flash(t('Project cloned successfully.')); } else { $this->session->flashError(t('Unable to clone this project.')); @@ -310,7 +332,7 @@ class Project extends Base $this->response->redirect('?controller=project'); } - $this->response->html($this->projectLayout('project_duplicate', array( + $this->response->html($this->projectLayout('project/duplicate', array( 'project' => $project, 'title' => t('Clone this project') ))); @@ -323,7 +345,7 @@ class Project extends Base */ public function disable() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); if ($this->request->getStringParam('disable') === 'yes') { @@ -338,7 +360,7 @@ class Project extends Base $this->response->redirect('?controller=project&action=show&project_id='.$project['id']); } - $this->response->html($this->projectLayout('project_disable', array( + $this->response->html($this->projectLayout('project/disable', array( 'project' => $project, 'title' => t('Project activation') ))); @@ -351,7 +373,7 @@ class Project extends Base */ public function enable() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); if ($this->request->getStringParam('enable') === 'yes') { @@ -366,7 +388,7 @@ class Project extends Base $this->response->redirect('?controller=project&action=show&project_id='.$project['id']); } - $this->response->html($this->projectLayout('project_enable', array( + $this->response->html($this->projectLayout('project/enable', array( 'project' => $project, 'title' => t('Project activation') ))); @@ -383,115 +405,13 @@ class Project extends Base $project = $this->project->getByToken($token); // Token verification - if (! $project) { + if (empty($project)) { $this->forbidden(true); } - $this->response->xml($this->template->load('project_feed', array( - 'events' => $this->projectActivity->getProject($project['id']), - 'project' => $project, - ))); - } - - /** - * Activity page for a project - * - * @access public - */ - public function activity() - { - $project = $this->getProject(); - - $this->response->html($this->template->layout('project_activity', array( + $this->response->xml($this->template->render('project/feed', array( 'events' => $this->projectActivity->getProject($project['id']), - 'menu' => 'projects', 'project' => $project, - 'title' => t('%s\'s activity', $project['name']) - ))); - } - - /** - * Task search for a given project - * - * @access public - */ - public function search() - { - $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; - - if ($search !== '') { - $tasks = $this->taskFinder->search($project['id'], $search, $offset, $limit, $order, $direction); - $nb_tasks = $this->taskFinder->countSearch($project['id'], $search); - } - - $this->response->html($this->template->layout('project_search', array( - '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'], - ), - 'project' => $project, - 'menu' => 'projects', - 'columns' => $this->board->getColumnsList($project['id']), - 'categories' => $this->category->getList($project['id'], false), - 'title' => $project['name'].($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '') - ))); - } - - /** - * List of completed tasks for a given project - * - * @access public - */ - 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->taskFinder->getClosedTasks($project['id'], $offset, $limit, $order, $direction); - $nb_tasks = $this->taskFinder->countByProjectId($project['id'], array(TaskModel::STATUS_CLOSED)); - - $this->response->html($this->template->layout('project_tasks', array( - '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, - 'menu' => 'projects', - 'columns' => $this->board->getColumnsList($project['id']), - 'categories' => $this->category->getList($project['id'], false), - 'tasks' => $tasks, - 'nb_tasks' => $nb_tasks, - 'title' => $project['name'].' ('.$nb_tasks.')' ))); } @@ -500,14 +420,16 @@ class Project extends Base * * @access public */ - public function create() + public function create(array $values = array(), array $errors = array()) { - $this->response->html($this->template->layout('project_new', array( - 'errors' => array(), - 'values' => array( - 'is_private' => $this->request->getIntegerParam('private', $this->acl->isRegularUser()), - ), - 'title' => t('New project') + $is_private = $this->request->getIntegerParam('private', $this->userSession->isAdmin() ? 0 : 1); + + $this->response->html($this->template->layout('project/new', array( + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + 'values' => empty($values) ? array('is_private' => $is_private) : $values, + 'errors' => $errors, + 'is_private' => $is_private, + 'title' => $is_private ? t('New private project') : t('New project'), ))); } @@ -523,19 +445,16 @@ class Project extends Base if ($valid) { - if ($this->project->create($values, $this->acl->getUserId())) { + $project_id = $this->project->create($values, $this->userSession->getId(), true); + + if ($project_id > 0) { $this->session->flash(t('Your project have been created successfully.')); - $this->response->redirect('?controller=project'); - } - else { - $this->session->flashError(t('Unable to create your project.')); + $this->response->redirect('?controller=project&action=show&project_id='.$project_id); } + + $this->session->flashError(t('Unable to create your project.')); } - $this->response->html($this->template->layout('project_new', array( - 'errors' => $errors, - 'values' => $values, - 'title' => t('New Project') - ))); + $this->create($values, $errors); } } diff --git a/app/Controller/Projectinfo.php b/app/Controller/Projectinfo.php new file mode 100644 index 00000000..a9498f43 --- /dev/null +++ b/app/Controller/Projectinfo.php @@ -0,0 +1,97 @@ +<?php + +namespace Controller; + +/** + * Project Info controller (ActivityStream + completed tasks) + * + * @package controller + * @author Frederic Guillot + */ +class Projectinfo extends Base +{ + /** + * Activity page for a project + * + * @access public + */ + public function activity() + { + $project = $this->getProject(); + + $this->response->html($this->template->layout('projectinfo/activity', array( + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + 'events' => $this->projectActivity->getProject($project['id']), + 'project' => $project, + 'title' => t('%s\'s activity', $project['name']) + ))); + } + + /** + * Task search for a given project + * + * @access public + */ + public function search() + { + $project = $this->getProject(); + $search = $this->request->getStringParam('search'); + $nb_tasks = 0; + + $paginator = $this->paginator + ->setUrl('projectinfo', 'search', array('search' => $search, 'project_id' => $project['id'])) + ->setMax(30) + ->setOrder('tasks.id') + ->setDirection('DESC'); + + if ($search !== '') { + + $paginator + ->setQuery($this->taskFinder->getSearchQuery($project['id'], $search)) + ->calculate(); + + $nb_tasks = $paginator->getTotal(); + } + + $this->response->html($this->template->layout('projectinfo/search', array( + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + 'values' => array( + 'search' => $search, + 'controller' => 'projectinfo', + 'action' => 'search', + 'project_id' => $project['id'], + ), + 'paginator' => $paginator, + 'project' => $project, + 'columns' => $this->board->getColumnsList($project['id']), + 'categories' => $this->category->getList($project['id'], false), + 'title' => t('Search in the project "%s"', $project['name']).($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '') + ))); + } + + /** + * List of completed tasks for a given project + * + * @access public + */ + public function tasks() + { + $project = $this->getProject(); + $paginator = $this->paginator + ->setUrl('projectinfo', '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('projectinfo/tasks', array( + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + 'project' => $project, + 'columns' => $this->board->getColumnsList($project['id']), + 'categories' => $this->category->getList($project['id'], false), + 'paginator' => $paginator, + 'title' => t('Completed tasks for "%s"', $project['name']).' ('.$paginator->getTotal().')' + ))); + } +} diff --git a/app/Controller/Subtask.php b/app/Controller/Subtask.php index 48f0d6e2..5baa6004 100644 --- a/app/Controller/Subtask.php +++ b/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,9 +20,9 @@ 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) { + if (empty($subtask)) { $this->notfound(); } @@ -32,20 +34,22 @@ class Subtask extends Base * * @access public */ - public function create() + public function create(array $values = array(), array $errors = array()) { $task = $this->getTask(); - $this->response->html($this->taskLayout('subtask_create', array( - 'values' => array( + if (empty($values)) { + $values = array( 'task_id' => $task['id'], 'another_subtask' => $this->request->getIntegerParam('another_subtask', 0) - ), - 'errors' => array(), - 'users_list' => $this->projectPermission->getUsersList($task['project_id']), + ); + } + + $this->response->html($this->taskLayout('subtask/create', array( + 'values' => $values, + 'errors' => $errors, + 'users_list' => $this->projectPermission->getMemberList($task['project_id']), 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Add a sub-task') ))); } @@ -59,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 { @@ -71,20 +75,13 @@ class Subtask extends Base } if (isset($values['another_subtask']) && $values['another_subtask'] == 1) { - $this->response->redirect('?controller=subtask&action=create&task_id='.$task['id'].'&another_subtask=1'); + $this->response->redirect('?controller=subtask&action=create&task_id='.$task['id'].'&another_subtask=1&project_id='.$task['project_id']); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#subtasks'); } - $this->response->html($this->taskLayout('subtask_create', array( - 'values' => $values, - 'errors' => $errors, - 'users_list' => $this->projectPermission->getUsersList($task['project_id']), - 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Add a sub-task') - ))); + $this->create($values, $errors); } /** @@ -92,20 +89,18 @@ class Subtask extends Base * * @access public */ - public function edit() + public function edit(array $values = array(), array $errors = array()) { $task = $this->getTask(); $subtask = $this->getSubTask(); - $this->response->html($this->taskLayout('subtask_edit', array( - 'values' => $subtask, - 'errors' => array(), - 'users_list' => $this->projectPermission->getUsersList($task['project_id']), - 'status_list' => $this->subTask->getStatusList(), + $this->response->html($this->taskLayout('subtask/edit', array( + 'values' => empty($values) ? $subtask : $values, + 'errors' => $errors, + 'users_list' => $this->projectPermission->getMemberList($task['project_id']), + 'status_list' => $this->subtask->getStatusList(), 'subtask' => $subtask, 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Edit a sub-task') ))); } @@ -117,33 +112,24 @@ class Subtask extends Base public function update() { $task = $this->getTask(); - $subtask = $this->getSubtask(); + $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 { $this->session->flashError(t('Unable to update your sub-task.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#subtasks'); } - $this->response->html($this->taskLayout('subtask_edit', array( - 'values' => $values, - 'errors' => $errors, - 'users_list' => $this->projectPermission->getUsersList($task['project_id']), - 'status_list' => $this->subTask->getStatusList(), - 'subtask' => $subtask, - 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Edit a sub-task') - ))); + $this->edit($values, $errors); } /** @@ -156,11 +142,9 @@ class Subtask extends Base $task = $this->getTask(); $subtask = $this->getSubtask(); - $this->response->html($this->taskLayout('subtask_remove', array( + $this->response->html($this->taskLayout('subtask/remove', array( 'subtask' => $subtask, 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Remove a sub-task') ))); } @@ -175,14 +159,14 @@ 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 { $this->session->flashError(t('Unable to remove this sub-task.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#subtasks'); } /** @@ -194,17 +178,103 @@ class Subtask extends Base { $task = $this->getTask(); $subtask = $this->getSubtask(); + $redirect = $this->request->getStringParam('redirect', '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->toggleRedirect($task, $redirect); + } + + /** + * Handle subtask restriction (popover) + * + * @access public + */ + public function subtaskRestriction() + { + $task = $this->getTask(); + $subtask = $this->getSubtask(); - $value = array( + $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' => ($subtask['status'] + 1) % 3, - 'task_id' => $task['id'], - ); + 'status' => SubtaskModel::STATUS_INPROGRESS, + )); - if (! $this->subTask->update($value)) { - $this->session->flashError(t('Unable to update your sub-task.')); + $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->to('board', 'show', array('project_id' => $task['project_id']))); + case 'dashboard': + $this->response->redirect($this->helper->url->to('app', 'index')); + default: + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); } + } + + /** + * Move subtask position + * + * @access public + */ + public function movePosition() + { + $this->checkCSRFParam(); + $project_id = $this->request->getIntegerParam('project_id'); + $task_id = $this->request->getIntegerParam('task_id'); + $subtask_id = $this->request->getIntegerParam('subtask_id'); + $direction = $this->request->getStringParam('direction'); + $method = $direction === 'up' ? 'moveUp' : 'moveDown'; - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks'); + $this->subtask->$method($task_id, $subtask_id); + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $project_id, 'task_id' => $task_id)).'#subtasks'); } } diff --git a/app/Controller/Swimlane.php b/app/Controller/Swimlane.php new file mode 100644 index 00000000..c6862d47 --- /dev/null +++ b/app/Controller/Swimlane.php @@ -0,0 +1,256 @@ +<?php + +namespace Controller; + +use Model\Swimlane as SwimlaneModel; + +/** + * Swimlanes + * + * @package controller + * @author Frederic Guillot + */ +class Swimlane extends Base +{ + /** + * Get the swimlane (common method between actions) + * + * @access private + * @param integer $project_id + * @return array + */ + private function getSwimlane($project_id) + { + $swimlane = $this->swimlane->getById($this->request->getIntegerParam('swimlane_id')); + + if (empty($swimlane)) { + $this->session->flashError(t('Swimlane not found.')); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project_id); + } + + return $swimlane; + } + + /** + * List of swimlanes for a given project + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('swimlane/index', array( + 'default_swimlane' => $this->swimlane->getDefault($project['id']), + 'active_swimlanes' => $this->swimlane->getAllByStatus($project['id'], SwimlaneModel::ACTIVE), + 'inactive_swimlanes' => $this->swimlane->getAllByStatus($project['id'], SwimlaneModel::INACTIVE), + 'values' => $values + array('project_id' => $project['id']), + 'errors' => $errors, + 'project' => $project, + 'title' => t('Swimlanes') + ))); + } + + /** + * Validate and save a new swimlane + * + * @access public + */ + public function save() + { + $project = $this->getProject(); + + $values = $this->request->getValues(); + list($valid, $errors) = $this->swimlane->validateCreation($values); + + if ($valid) { + + if ($this->swimlane->create($project['id'], $values['name'])) { + $this->session->flash(t('Your swimlane have been created successfully.')); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + else { + $this->session->flashError(t('Unable to create your swimlane.')); + } + } + + $this->index($values, $errors); + } + + /** + * Change the default swimlane + * + * @access public + */ + public function change() + { + $project = $this->getProject(); + + $values = $this->request->getValues() + array('show_default_swimlane' => 0); + list($valid,) = $this->swimlane->validateDefaultModification($values); + + if ($valid) { + + if ($this->swimlane->updateDefault($values)) { + $this->session->flash(t('The default swimlane have been updated successfully.')); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + else { + $this->session->flashError(t('Unable to update this swimlane.')); + } + } + + $this->index(); + } + + /** + * Edit a swimlane (display the form) + * + * @access public + */ + public function edit(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + $swimlane = $this->getSwimlane($project['id']); + + $this->response->html($this->projectLayout('swimlane/edit', array( + 'values' => empty($values) ? $swimlane : $values, + 'errors' => $errors, + 'project' => $project, + 'title' => t('Swimlanes') + ))); + } + + /** + * Edit a swimlane (validate the form and update the database) + * + * @access public + */ + public function update() + { + $project = $this->getProject(); + + $values = $this->request->getValues(); + list($valid, $errors) = $this->swimlane->validateModification($values); + + if ($valid) { + + if ($this->swimlane->rename($values['id'], $values['name'])) { + $this->session->flash(t('Swimlane updated successfully.')); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + else { + $this->session->flashError(t('Unable to update this swimlane.')); + } + } + + $this->edit($values, $errors); + } + + /** + * Confirmation dialog before removing a swimlane + * + * @access public + */ + public function confirm() + { + $project = $this->getProject(); + $swimlane = $this->getSwimlane($project['id']); + + $this->response->html($this->projectLayout('swimlane/remove', array( + 'project' => $project, + 'swimlane' => $swimlane, + 'title' => t('Remove a swimlane') + ))); + } + + /** + * Remove a swimlane + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + if ($this->swimlane->remove($project['id'], $swimlane_id)) { + $this->session->flash(t('Swimlane removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this swimlane.')); + } + + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + + /** + * Disable a swimlane + * + * @access public + */ + public function disable() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + if ($this->swimlane->disable($project['id'], $swimlane_id)) { + $this->session->flash(t('Swimlane updated successfully.')); + } else { + $this->session->flashError(t('Unable to update this swimlane.')); + } + + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + + /** + * Enable a swimlane + * + * @access public + */ + public function enable() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + if ($this->swimlane->enable($project['id'], $swimlane_id)) { + $this->session->flash(t('Swimlane updated successfully.')); + } else { + $this->session->flashError(t('Unable to update this swimlane.')); + } + + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + + /** + * Move up a swimlane + * + * @access public + */ + public function moveup() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + $this->swimlane->moveUp($project['id'], $swimlane_id); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + + /** + * Move down a swimlane + * + * @access public + */ + public function movedown() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + $this->swimlane->moveDown($project['id'], $swimlane_id); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } +} diff --git a/app/Controller/Task.php b/app/Controller/Task.php index 1b20cf15..dc83f7b1 100644 --- a/app/Controller/Task.php +++ b/app/Controller/Task.php @@ -22,20 +22,21 @@ class Task extends Base $project = $this->project->getByToken($this->request->getStringParam('token')); // Token verification - if (! $project) { + if (empty($project)) { $this->forbidden(true); } $task = $this->taskFinder->getDetails($this->request->getIntegerParam('task_id')); - if (! $task) { + if (empty($task)) { $this->notfound(true); } - $this->response->html($this->template->layout('task_public', array( + $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->getAllGroupedByLabel($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'], @@ -65,20 +66,41 @@ class Task extends Base $this->dateParser->format($values, array('date_started')); - $this->response->html($this->taskLayout('task_show', array( + $this->response->html($this->taskLayout('task/show', array( 'project' => $this->project->getById($task['project_id']), - 'files' => $this->file->getAll($task['id']), + 'files' => $this->file->getAllDocuments($task['id']), + 'images' => $this->file->getAllImages($task['id']), 'comments' => $this->comment->getAll($task['id']), 'subtasks' => $subtasks, + 'links' => $this->taskLink->getAllGroupedByLabel($task['id']), 'task' => $task, 'values' => $values, - 'timesheet' => $this->timeTracking->getTaskTimesheet($task, $subtasks), + 'link_label_list' => $this->link->getList(0, false), 'columns_list' => $this->board->getColumnsList($task['project_id']), 'colors_list' => $this->color->getList(), 'date_format' => $this->config->get('application_date_format'), 'date_formats' => $this->dateParser->getAvailableFormats(), - 'menu' => 'tasks', + 'title' => $task['project_name'].' > '.$task['title'], + 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), + 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), + 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), + ))); + } + + /** + * Display task activities + * + * @access public + */ + public function activites() + { + $task = $this->getTask(); + + $this->response->html($this->taskLayout('task/activity', array( 'title' => $task['title'], + 'task' => $task, + 'ajax' => $this->request->isAjax(), + 'events' => $this->projectActivity->getTask($task['id']), ))); } @@ -87,29 +109,36 @@ class Task extends Base * * @access public */ - public function create() + public function create(array $values = array(), array $errors = array()) { - $project_id = $this->request->getIntegerParam('project_id'); - $this->checkProjectPermissions($project_id); + $project = $this->getProject(); + $method = $this->request->isAjax() ? 'render' : 'layout'; + $swimlanes_list = $this->swimlane->getList($project['id'], false, true); - $this->response->html($this->template->layout('task_new', array( - 'errors' => array(), - 'values' => array( - 'project_id' => $project_id, + 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'), 'owner_id' => $this->request->getIntegerParam('owner_id'), 'another_task' => $this->request->getIntegerParam('another_task'), - ), + ); + } + + $this->response->html($this->template->$method('task/new', array( + 'ajax' => $this->request->isAjax(), + 'errors' => $errors, + 'values' => $values + array('project_id' => $project['id']), 'projects_list' => $this->project->getListByStatus(ProjectModel::ACTIVE), - 'columns_list' => $this->board->getColumnsList($project_id), - 'users_list' => $this->projectPermission->getUsersList($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), + 'categories_list' => $this->category->getList($project['id']), + 'swimlanes_list' => $swimlanes_list, 'date_format' => $this->config->get('application_date_format'), 'date_formats' => $this->dateParser->getAvailableFormats(), - 'menu' => 'tasks', - 'title' => t('New task') + 'title' => $project['name'].' > '.t('New task') ))); } @@ -120,16 +149,15 @@ class Task extends Base */ public function save() { + $project = $this->getProject(); $values = $this->request->getValues(); - $values['creator_id'] = $this->acl->getUserId(); - - $this->checkProjectPermissions($values['project_id']); + $values['creator_id'] = $this->userSession->getId(); list($valid, $errors) = $this->taskValidator->validateCreation($values); if ($valid) { - if ($this->task->create($values)) { + if ($this->taskCreation->create($values)) { $this->session->flash(t('Task created successfully.')); if (isset($values['another_task']) && $values['another_task'] == 1) { @@ -138,7 +166,7 @@ class Task extends Base $this->response->redirect('?controller=task&action=create&'.http_build_query($values)); } else { - $this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']); + $this->response->redirect('?controller=board&action=show&project_id='.$project['id']); } } else { @@ -146,19 +174,7 @@ class Task extends Base } } - $this->response->html($this->template->layout('task_new', array( - 'errors' => $errors, - 'values' => $values, - 'projects_list' => $this->project->getListByStatus(ProjectModel::ACTIVE), - 'columns_list' => $this->board->getColumnsList($values['project_id']), - 'users_list' => $this->projectPermission->getUsersList($values['project_id']), - 'colors_list' => $this->color->getList(), - 'categories_list' => $this->category->getList($values['project_id']), - 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), - 'menu' => 'tasks', - 'title' => t('New task') - ))); + $this->create($values, $errors); } /** @@ -166,32 +182,34 @@ class Task extends Base * * @access public */ - public function edit() + public function edit(array $values = array(), array $errors = array()) { $task = $this->getTask(); $ajax = $this->request->isAjax(); - $this->dateParser->format($task, array('date_due')); + if (empty($values)) { + $values = $task; + } + + $this->dateParser->format($values, array('date_due')); $params = array( - 'values' => $task, - 'errors' => array(), + 'values' => $values, + 'errors' => $errors, 'task' => $task, - 'users_list' => $this->projectPermission->getUsersList($task['project_id']), + '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, - 'menu' => 'tasks', - 'title' => t('Edit a task') ); if ($ajax) { - $this->response->html($this->template->load('task_edit', $params)); + $this->response->html($this->template->render('task/edit', $params)); } else { - $this->response->html($this->taskLayout('task_edit', $params)); + $this->response->html($this->taskLayout('task/edit', $params)); } } @@ -209,14 +227,14 @@ class Task extends Base if ($valid) { - if ($this->task->update($values)) { + if ($this->taskModification->update($values)) { $this->session->flash(t('Task updated successfully.')); if ($this->request->getIntegerParam('ajax')) { $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); } else { - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); } } else { @@ -224,20 +242,7 @@ class Task extends Base } } - $this->response->html($this->taskLayout('task_edit', array( - 'values' => $values, - 'errors' => $errors, - 'task' => $task, - 'columns_list' => $this->board->getColumnsList($values['project_id']), - 'users_list' => $this->projectPermission->getUsersList($values['project_id']), - 'colors_list' => $this->color->getList(), - 'categories_list' => $this->category->getList($values['project_id']), - 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), - 'menu' => 'tasks', - 'title' => t('Edit a task'), - 'ajax' => $this->request->isAjax(), - ))); + $this->edit($values, $errors); } /** @@ -250,16 +255,16 @@ class Task extends Base $task = $this->getTask(); $values = $this->request->getValues(); - list($valid, $errors) = $this->taskValidator->validateTimeModification($values); + list($valid,) = $this->taskValidator->validateTimeModification($values); - if ($valid && $this->task->update($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('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); } /** @@ -270,24 +275,35 @@ class Task extends Base public function close() { $task = $this->getTask(); + $redirect = $this->request->getStringParam('redirect'); if ($this->request->getStringParam('confirmation') === 'yes') { $this->checkCSRFParam(); - if ($this->task->close($task['id'])) { + if ($this->taskStatus->close($task['id'])) { $this->session->flash(t('Task closed successfully.')); } else { $this->session->flashError(t('Unable to close this task.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + 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']))); } - $this->response->html($this->taskLayout('task_close', array( + 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, - 'menu' => 'tasks', - 'title' => t('Close a task') + 'redirect' => $redirect, ))); } @@ -304,19 +320,17 @@ class Task extends Base $this->checkCSRFParam(); - if ($this->task->open($task['id'])) { + 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('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); } - $this->response->html($this->taskLayout('task_open', array( + $this->response->html($this->taskLayout('task/open', array( 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Open a task') ))); } @@ -346,10 +360,8 @@ class Task extends Base $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); } - $this->response->html($this->taskLayout('task_remove', array( + $this->response->html($this->taskLayout('task/remove', array( 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Remove a task') ))); } @@ -365,21 +377,19 @@ class Task extends Base if ($this->request->getStringParam('confirmation') === 'yes') { $this->checkCSRFParam(); - $task_id = $this->task->duplicateToSameProject($task); + $task_id = $this->taskDuplication->duplicate($task['id']); if ($task_id) { $this->session->flash(t('Task created successfully.')); - $this->response->redirect('?controller=task&action=show&task_id='.$task_id); + $this->response->redirect('?controller=task&action=show&task_id='.$task_id.'&project_id='.$task['project_id']); } else { $this->session->flashError(t('Unable to create this task.')); - $this->response->redirect('?controller=task&action=duplicate&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=duplicate&task_id='.$task['id'].'&project_id='.$task['project_id']); } } - $this->response->html($this->taskLayout('task_duplicate', array( + $this->response->html($this->taskLayout('task/duplicate', array( 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Duplicate a task') ))); } @@ -401,7 +411,7 @@ class Task extends Base if ($valid) { - if ($this->task->update($values)) { + if ($this->taskModification->update($values)) { $this->session->flash(t('Task updated successfully.')); } else { @@ -412,7 +422,7 @@ class Task extends Base $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); } else { - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); } } } @@ -426,49 +436,123 @@ class Task extends Base 'errors' => $errors, 'task' => $task, 'ajax' => $ajax, - 'menu' => 'tasks', - 'title' => t('Edit the description'), ); if ($ajax) { - $this->response->html($this->template->load('task_edit_description', $params)); + $this->response->html($this->template->render('task/edit_description', $params)); } else { - $this->response->html($this->taskLayout('task_edit_description', $params)); + $this->response->html($this->taskLayout('task/edit_description', $params)); } } /** - * Move a task to another project + * Edit recurrence form * * @access public */ - public function move() + public function recurrence() { - $this->toAnotherProject('move'); + $task = $this->getTask(); + $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); + + 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.')); + } + + if ($ajax) { + $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); + } + else { + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); + } + } + } + else { + $values = $task; + $errors = array(); + } + + $params = array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'ajax' => $ajax, + 'recurrence_status_list' => $this->task->getRecurrenceStatusList(), + 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), + 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), + 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), + ); + + if ($ajax) { + $this->response->html($this->template->render('task/edit_recurrence', $params)); + } + else { + $this->response->html($this->taskLayout('task/edit_recurrence', $params)); + } } /** - * Duplicate a task to another project + * Move a task to another project * * @access public */ - public function copy() + public function move() { - $this->toAnotherProject('duplicate'); + $task = $this->getTask(); + $values = $task; + $errors = array(); + $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId()); + + unset($projects_list[$task['project_id']]); + + if ($this->request->isPost()) { + + $values = $this->request->getValues(); + list($valid, $errors) = $this->taskValidator->validateProjectModification($values); + + if ($valid) { + + if ($this->taskDuplication->moveToProject($task['id'], $values['project_id'])) { + $this->session->flash(t('Task updated successfully.')); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$values['project_id']); + } + else { + $this->session->flashError(t('Unable to update your task.')); + } + } + } + + $this->response->html($this->taskLayout('task/move_project', array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'projects_list' => $projects_list, + ))); } /** - * Common methods between the actions "move" and "copy" + * Duplicate a task to another project * - * @access private + * @access public */ - private function toAnotherProject($action) + public function copy() { $task = $this->getTask(); $values = $task; $errors = array(); - $projects_list = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); + $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId()); unset($projects_list[$task['project_id']]); @@ -478,10 +562,10 @@ class Task extends Base list($valid, $errors) = $this->taskValidator->validateProjectModification($values); if ($valid) { - $task_id = $this->task->{$action.'ToAnotherProject'}($values['project_id'], $task); + $task_id = $this->taskDuplication->duplicateToProject($task['id'], $values['project_id']); if ($task_id) { $this->session->flash(t('Task created successfully.')); - $this->response->redirect('?controller=task&action=show&task_id='.$task_id); + $this->response->redirect('?controller=task&action=show&task_id='.$task_id.'&project_id='.$values['project_id']); } else { $this->session->flashError(t('Unable to create your task.')); @@ -489,13 +573,49 @@ class Task extends Base } } - $this->response->html($this->taskLayout('task_'.$action.'_project', array( + $this->response->html($this->taskLayout('task/duplicate_project', array( 'values' => $values, 'errors' => $errors, 'task' => $task, 'projects_list' => $projects_list, - 'menu' => 'tasks', - 'title' => t(ucfirst($action).' the task to another project') + ))); + } + + /** + * 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, + ))); + } + + /** + * Display the task transitions + * + * @access public + */ + public function transitions() + { + $task = $this->getTask(); + + $this->response->html($this->taskLayout('task/transitions', array( + 'task' => $task, + 'transitions' => $this->transition->getAllByTask($task['id']), ))); } } diff --git a/app/Controller/Tasklink.php b/app/Controller/Tasklink.php new file mode 100644 index 00000000..dd076802 --- /dev/null +++ b/app/Controller/Tasklink.php @@ -0,0 +1,179 @@ +<?php + +namespace Controller; + +/** + * TaskLink controller + * + * @package controller + * @author Olivier Maridat + * @author Frederic Guillot + */ +class Tasklink extends Base +{ + /** + * Get the current link + * + * @access private + * @return array + */ + private function getTaskLink() + { + $link = $this->taskLink->getById($this->request->getIntegerParam('link_id')); + + if (empty($link)) { + $this->notfound(); + } + + return $link; + } + + /** + * Creation form + * + * @access public + */ + public function create(array $values = array(), array $errors = array()) + { + $task = $this->getTask(); + $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); + + if ($ajax && empty($errors)) { + $this->response->html($this->template->render('tasklink/create', array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'labels' => $this->link->getList(0, false), + 'title' => t('Add a new link'), + 'ajax' => $ajax, + ))); + } + + $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(); + $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); + + 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.')); + + if ($ajax) { + $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'])).'#links'); + } + + $errors = array('title' => array(t('The exact same link already exists'))); + $this->session->flashError(t('Unable to create your link.')); + } + + $this->create($values, $errors); + } + + /** + * Edit form + * + * @access public + */ + public function edit(array $values = array(), array $errors = array()) + { + $task = $this->getTask(); + $task_link = $this->getTaskLink(); + + if (empty($values)) { + $opposite_task = $this->taskFinder->getById($task_link['opposite_task_id']); + $values = $task_link; + $values['title'] = '#'.$opposite_task['id'].' - '.$opposite_task['title']; + } + + $this->response->html($this->taskLayout('tasklink/edit', array( + 'values' => $values, + 'errors' => $errors, + 'task_link' => $task_link, + 'task' => $task, + 'labels' => $this->link->getList(0, false), + 'title' => t('Edit link') + ))); + } + + /** + * Validation and update + * + * @access public + */ + public function update() + { + $task = $this->getTask(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->taskLink->validateModification($values); + + if ($valid) { + + if ($this->taskLink->update($values['id'], $values['task_id'], $values['opposite_task_id'], $values['link_id'])) { + $this->session->flash(t('Link updated successfully.')); + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links'); + } + + $this->session->flashError(t('Unable to update your link.')); + } + + $this->edit($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->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links'); + } +} diff --git a/app/Controller/Timetable.php b/app/Controller/Timetable.php new file mode 100644 index 00000000..65edb44c --- /dev/null +++ b/app/Controller/Timetable.php @@ -0,0 +1,39 @@ +<?php + +namespace Controller; + +use DateTime; + +/** + * Timetable controller + * + * @package controller + * @author Frederic Guillot + */ +class Timetable extends User +{ + /** + * Display timetable for the user + * + * @access public + */ + public function index() + { + $user = $this->getUser(); + $from = $this->request->getStringParam('from', date('Y-m-d')); + $to = $this->request->getStringParam('to', date('Y-m-d', strtotime('next week'))); + $timetable = $this->timetable->calculate($user['id'], new DateTime($from), new DateTime($to)); + + $this->response->html($this->layout('timetable/index', array( + 'user' => $user, + 'timetable' => $timetable, + 'values' => array( + 'from' => $from, + 'to' => $to, + 'controller' => 'timetable', + 'action' => 'index', + 'user_id' => $user['id'], + ), + ))); + } +} diff --git a/app/Controller/Timetableday.php b/app/Controller/Timetableday.php new file mode 100644 index 00000000..c8f7ac8a --- /dev/null +++ b/app/Controller/Timetableday.php @@ -0,0 +1,88 @@ +<?php + +namespace Controller; + +/** + * Day Timetable controller + * + * @package controller + * @author Frederic Guillot + */ +class Timetableday extends User +{ + /** + * Display timetable for the user + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $user = $this->getUser(); + + $this->response->html($this->layout('timetable_day/index', array( + 'timetable' => $this->timetableDay->getByUser($user['id']), + 'values' => $values + array('user_id' => $user['id']), + 'errors' => $errors, + 'user' => $user, + ))); + } + + /** + * Validate and save + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->timetableDay->validateCreation($values); + + if ($valid) { + + if ($this->timetableDay->create($values['user_id'], $values['start'], $values['end'])) { + $this->session->flash(t('Time slot created successfully.')); + $this->response->redirect($this->helper->url->to('timetableday', 'index', array('user_id' => $values['user_id']))); + } + else { + $this->session->flashError(t('Unable to save this time slot.')); + } + } + + $this->index($values, $errors); + } + + /** + * Confirmation dialag box to remove a row + * + * @access public + */ + public function confirm() + { + $user = $this->getUser(); + + $this->response->html($this->layout('timetable_day/remove', array( + 'slot_id' => $this->request->getIntegerParam('slot_id'), + 'user' => $user, + ))); + } + + /** + * Remove a row + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $user = $this->getUser(); + + if ($this->timetableDay->remove($this->request->getIntegerParam('slot_id'))) { + $this->session->flash(t('Time slot removed successfully.')); + } + else { + $this->session->flash(t('Unable to remove this time slot.')); + } + + $this->response->redirect($this->helper->url->to('timetableday', 'index', array('user_id' => $user['id']))); + } +} diff --git a/app/Controller/Timetableextra.php b/app/Controller/Timetableextra.php new file mode 100644 index 00000000..7c6fe265 --- /dev/null +++ b/app/Controller/Timetableextra.php @@ -0,0 +1,16 @@ +<?php + +namespace Controller; + +/** + * Over-time Timetable controller + * + * @package controller + * @author Frederic Guillot + */ +class Timetableextra extends Timetableoff +{ + protected $model = 'timetableExtra'; + protected $controller_url = 'timetableextra'; + protected $template_dir = 'timetable_extra'; +} diff --git a/app/Controller/Timetableoff.php b/app/Controller/Timetableoff.php new file mode 100644 index 00000000..585014a3 --- /dev/null +++ b/app/Controller/Timetableoff.php @@ -0,0 +1,107 @@ +<?php + +namespace Controller; + +/** + * Time-off Timetable controller + * + * @package controller + * @author Frederic Guillot + */ +class Timetableoff extends User +{ + protected $model = 'timetableOff'; + protected $controller_url = 'timetableoff'; + protected $template_dir = 'timetable_off'; + + /** + * Display timetable for the user + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $user = $this->getUser(); + + $paginator = $this->paginator + ->setUrl($this->controller_url, 'index', array('user_id' => $user['id'])) + ->setMax(10) + ->setOrder('date') + ->setDirection('desc') + ->setQuery($this->{$this->model}->getUserQuery($user['id'])) + ->calculate(); + + $this->response->html($this->layout($this->template_dir.'/index', array( + 'values' => $values + array('user_id' => $user['id']), + 'errors' => $errors, + 'paginator' => $paginator, + 'user' => $user, + ))); + } + + /** + * Validate and save + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->{$this->model}->validateCreation($values); + + if ($valid) { + + if ($this->{$this->model}->create( + $values['user_id'], + $values['date'], + isset($values['all_day']) && $values['all_day'] == 1, + $values['start'], + $values['end'], + $values['comment'])) { + + $this->session->flash(t('Time slot created successfully.')); + $this->response->redirect($this->helper->url->to($this->controller_url, 'index', array('user_id' => $values['user_id']))); + } + else { + $this->session->flashError(t('Unable to save this time slot.')); + } + } + + $this->index($values, $errors); + } + + /** + * Confirmation dialag box to remove a row + * + * @access public + */ + public function confirm() + { + $user = $this->getUser(); + + $this->response->html($this->layout($this->template_dir.'/remove', array( + 'slot_id' => $this->request->getIntegerParam('slot_id'), + 'user' => $user, + ))); + } + + /** + * Remove a row + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $user = $this->getUser(); + + if ($this->{$this->model}->remove($this->request->getIntegerParam('slot_id'))) { + $this->session->flash(t('Time slot removed successfully.')); + } + else { + $this->session->flash(t('Unable to remove this time slot.')); + } + + $this->response->redirect($this->helper->url->to($this->controller_url, 'index', array('user_id' => $user['id']))); + } +} diff --git a/app/Controller/Timetableweek.php b/app/Controller/Timetableweek.php new file mode 100644 index 00000000..b8ce00e7 --- /dev/null +++ b/app/Controller/Timetableweek.php @@ -0,0 +1,99 @@ +<?php + +namespace Controller; + +/** + * Week Timetable controller + * + * @package controller + * @author Frederic Guillot + */ +class Timetableweek extends User +{ + /** + * Display timetable for the user + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $user = $this->getUser(); + + if (empty($values)) { + + $day = $this->timetableDay->getByUser($user['id']); + + $values = array( + 'user_id' => $user['id'], + 'start' => isset($day[0]['start']) ? $day[0]['start'] : null, + 'end' => isset($day[0]['end']) ? $day[0]['end'] : null, + ); + } + + $this->response->html($this->layout('timetable_week/index', array( + 'timetable' => $this->timetableWeek->getByUser($user['id']), + 'values' => $values, + 'errors' => $errors, + 'user' => $user, + ))); + } + + /** + * Validate and save + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->timetableWeek->validateCreation($values); + + if ($valid) { + + if ($this->timetableWeek->create($values['user_id'], $values['day'], $values['start'], $values['end'])) { + $this->session->flash(t('Time slot created successfully.')); + $this->response->redirect($this->helper->url->to('timetableweek', 'index', array('user_id' => $values['user_id']))); + } + else { + $this->session->flashError(t('Unable to save this time slot.')); + } + } + + $this->index($values, $errors); + } + + /** + * Confirmation dialag box to remove a row + * + * @access public + */ + public function confirm() + { + $user = $this->getUser(); + + $this->response->html($this->layout('timetable_week/remove', array( + 'slot_id' => $this->request->getIntegerParam('slot_id'), + 'user' => $user, + ))); + } + + /** + * Remove a row + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $user = $this->getUser(); + + if ($this->timetableWeek->remove($this->request->getIntegerParam('slot_id'))) { + $this->session->flash(t('Time slot removed successfully.')); + } + else { + $this->session->flash(t('Unable to remove this time slot.')); + } + + $this->response->redirect($this->helper->url->to('timetableweek', 'index', array('user_id' => $user['id']))); + } +} diff --git a/app/Controller/Twofactor.php b/app/Controller/Twofactor.php new file mode 100644 index 00000000..a8b0351f --- /dev/null +++ b/app/Controller/Twofactor.php @@ -0,0 +1,167 @@ +<?php + +namespace Controller; + +use Otp\Otp; +use Otp\GoogleAuthenticator; +use Base32\Base32; + +/** + * Two Factor Auth controller + * + * @package controller + * @author Frederic Guillot + */ +class Twofactor extends User +{ + /** + * Only the current user can access to 2FA settings + * + * @access private + */ + private function checkCurrentUser(array $user) + { + if ($user['id'] != $this->userSession->getId()) { + $this->forbidden(); + } + } + + /** + * Index + * + * @access public + */ + public function index() + { + $user = $this->getUser(); + $this->checkCurrentUser($user); + + $label = $user['email'] ?: $user['username']; + + $this->response->html($this->layout('twofactor/index', array( + 'user' => $user, + 'qrcode_url' => $user['twofactor_activated'] == 1 ? GoogleAuthenticator::getQrCodeUrl('totp', $label, $user['twofactor_secret']) : '', + 'key_url' => $user['twofactor_activated'] == 1 ? GoogleAuthenticator::getKeyUri('totp', $label, $user['twofactor_secret']) : '', + ))); + } + + /** + * Enable/disable 2FA + * + * @access public + */ + public function save() + { + $user = $this->getUser(); + $this->checkCurrentUser($user); + + $values = $this->request->getValues(); + + if (isset($values['twofactor_activated']) && $values['twofactor_activated'] == 1) { + $this->user->update(array( + 'id' => $user['id'], + 'twofactor_activated' => 1, + 'twofactor_secret' => GoogleAuthenticator::generateRandom(), + )); + } + else { + $this->user->update(array( + 'id' => $user['id'], + 'twofactor_activated' => 0, + 'twofactor_secret' => '', + )); + } + + // Allow the user to test or disable the feature + $_SESSION['user']['twofactor_activated'] = false; + + $this->session->flash(t('User updated successfully.')); + $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); + } + + /** + * Test 2FA + * + * @access public + */ + public function test() + { + $user = $this->getUser(); + $this->checkCurrentUser($user); + + $otp = new Otp; + $values = $this->request->getValues(); + + if (! empty($values['code']) && $otp->checkTotp(Base32::decode($user['twofactor_secret']), $values['code'])) { + $this->session->flash(t('The two factor authentication code is valid.')); + } + else { + $this->session->flashError(t('The two factor authentication code is not valid.')); + } + + $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); + } + + /** + * Check 2FA + * + * @access public + */ + public function check() + { + $user = $this->getUser(); + $this->checkCurrentUser($user); + + $otp = new Otp; + $values = $this->request->getValues(); + + if (! empty($values['code']) && $otp->checkTotp(Base32::decode($user['twofactor_secret']), $values['code'])) { + $this->session['2fa_validated'] = true; + $this->session->flash(t('The two factor authentication code is valid.')); + $this->response->redirect($this->helper->url->to('app', 'index')); + } + else { + $this->session->flashError(t('The two factor authentication code is not valid.')); + $this->response->redirect($this->helper->url->to('twofactor', 'code')); + } + } + + /** + * Ask the 2FA code + * + * @access public + */ + public function code() + { + $this->response->html($this->template->layout('twofactor/check', array( + 'title' => t('Check two factor authentication code'), + ))); + } + + /** + * Disable 2FA for a user + * + * @access public + */ + public function disable() + { + $user = $this->getUser(); + + if ($this->request->getStringParam('disable') === 'yes') { + + $this->checkCSRFParam(); + + $this->user->update(array( + 'id' => $user['id'], + 'twofactor_activated' => 0, + 'twofactor_secret' => '', + )); + + $this->response->redirect($this->helper->url->to('user', 'show', array('user_id' => $user['id']))); + } + + $this->response->html($this->layout('twofactor/disable', array( + 'user' => $user, + ))); + } +} diff --git a/app/Controller/User.php b/app/Controller/User.php index 834b2379..4cea06b1 100644 --- a/app/Controller/User.php +++ b/app/Controller/User.php @@ -11,103 +11,41 @@ namespace Controller; class User extends Base { /** - * Logout and destroy session - * - * @access public - */ - public function logout() - { - $this->checkCSRFParam(); - $this->authentication->backend('rememberMe')->destroy($this->acl->getUserId()); - $this->session->close(); - $this->response->redirect('?controller=user&action=login'); - } - - /** - * Display the form login - * - * @access public - */ - public function login() - { - if ($this->acl->isLogged()) { - $this->response->redirect('?controller=app'); - } - - $this->response->html($this->template->layout('user_login', array( - 'errors' => array(), - 'values' => array(), - 'no_layout' => true, - 'redirect_query' => $this->request->getStringParam('redirect_query'), - 'title' => t('Login') - ))); - } - - /** - * Check credentials - * - * @access public - */ - public function check() - { - $redirect_query = $this->request->getStringParam('redirect_query'); - $values = $this->request->getValues(); - list($valid, $errors) = $this->authentication->validateForm($values); - - if ($valid) { - if ($redirect_query !== '') { - $this->response->redirect('?'.$redirect_query); - } - else { - $this->response->redirect('?controller=app'); - } - } - - $this->response->html($this->template->layout('user_login', array( - 'errors' => $errors, - 'values' => $values, - 'no_layout' => true, - 'redirect_query' => $redirect_query, - 'title' => t('Login') - ))); - } - - /** * Common layout for user views * - * @access private + * @access protected * @param string $template Template name * @param array $params Template parameters * @return string */ - private function layout($template, array $params) + protected function layout($template, array $params) { - $content = $this->template->load($template, $params); + $content = $this->template->render($template, $params); $params['user_content_for_layout'] = $content; - $params['menu'] = 'users'; + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); if (isset($params['user'])) { - $params['title'] = $params['user']['name'] ?: $params['user']['username']; + $params['title'] = ($params['user']['name'] ?: $params['user']['username']).' (#'.$params['user']['id'].')'; } - return $this->template->layout('user_layout', $params); + return $this->template->layout('user/layout', $params); } /** * Common method to get the user * - * @access private + * @access protected * @return array */ - private function getUser() + protected function getUser() { $user = $this->user->getById($this->request->getIntegerParam('user_id')); - if (! $user) { + if (empty($user)) { $this->notfound(); } - if ($this->acl->isRegularUser() && $this->acl->getUserId() != $user['id']) { + if (! $this->userSession->isAdmin() && $this->userSession->getId() != $user['id']) { $this->forbidden(); } @@ -121,16 +59,19 @@ class User extends Base */ public function index() { - $users = $this->user->getAll(); - $nb_users = count($users); + $paginator = $this->paginator + ->setUrl('user', 'index') + ->setMax(30) + ->setOrder('username') + ->setQuery($this->user->getQuery()) + ->calculate(); $this->response->html( - $this->template->layout('user_index', array( + $this->template->layout('user/index', array( + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), 'projects' => $this->project->getList(), - 'users' => $users, - 'nb_users' => $nb_users, - 'menu' => 'users', - 'title' => t('Users').' ('.$nb_users.')' + 'title' => t('Users').' ('.$paginator->getTotal().')', + 'paginator' => $paginator, ))); } @@ -139,13 +80,15 @@ class User extends Base * * @access public */ - public function create() + public function create(array $values = array(), array $errors = array()) { - $this->response->html($this->template->layout('user_new', array( + $this->response->html($this->template->layout('user/new', array( + 'timezones' => $this->config->getTimezones(true), + 'languages' => $this->config->getLanguages(true), + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), 'projects' => $this->project->getList(), - 'errors' => array(), - 'values' => array(), - 'menu' => 'users', + 'errors' => $errors, + 'values' => $values, 'title' => t('New user') ))); } @@ -162,22 +105,18 @@ class User extends Base if ($valid) { - if ($this->user->create($values)) { + $user_id = $this->user->create($values); + + if ($user_id !== false) { $this->session->flash(t('User created successfully.')); - $this->response->redirect('?controller=user'); + $this->response->redirect($this->helper->url->to('user', 'show', array('user_id' => $user_id))); } else { $this->session->flashError(t('Unable to create your user.')); } } - $this->response->html($this->template->layout('user_new', array( - 'projects' => $this->project->getList(), - 'errors' => $errors, - 'values' => $values, - 'menu' => 'users', - 'title' => t('New user') - ))); + $this->create($values, $errors); } /** @@ -188,9 +127,48 @@ class User extends Base public function show() { $user = $this->getUser(); - $this->response->html($this->layout('user_show', array( + $this->response->html($this->layout('user/show', array( 'projects' => $this->projectPermission->getAllowedProjects($user['id']), 'user' => $user, + 'timezones' => $this->config->getTimezones(true), + 'languages' => $this->config->getLanguages(true), + ))); + } + + /** + * 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, ))); } @@ -202,7 +180,7 @@ class User extends Base public function last() { $user = $this->getUser(); - $this->response->html($this->layout('user_last', array( + $this->response->html($this->layout('user/last', array( 'last_logins' => $this->lastLogin->getAll($user['id']), 'user' => $user, ))); @@ -216,7 +194,7 @@ class User extends Base public function sessions() { $user = $this->getUser(); - $this->response->html($this->layout('user_sessions', array( + $this->response->html($this->layout('user/sessions', array( 'sessions' => $this->authentication->backend('rememberMe')->getAll($user['id']), 'user' => $user, ))); @@ -251,7 +229,7 @@ class User extends Base $this->response->redirect('?controller=user&action=notifications&user_id='.$user['id']); } - $this->response->html($this->layout('user_notifications', array( + $this->response->html($this->layout('user/notifications', array( 'projects' => $this->projectPermission->getAllowedProjects($user['id']), 'notifications' => $this->notification->readSettings($user['id']), 'user' => $user, @@ -266,13 +244,42 @@ class User extends Base public function external() { $user = $this->getUser(); - $this->response->html($this->layout('user_external', array( + $this->response->html($this->layout('user/external', array( 'last_logins' => $this->lastLogin->getAll($user['id']), 'user' => $user, ))); } /** + * Public access management + * + * @access public + */ + public function share() + { + $user = $this->getUser(); + $switch = $this->request->getStringParam('switch'); + + if ($switch === 'enable' || $switch === 'disable') { + + $this->checkCSRFParam(); + + if ($this->user->{$switch.'PublicAccess'}($user['id'])) { + $this->session->flash(t('User updated successfully.')); + } else { + $this->session->flashError(t('Unable to update this user.')); + } + + $this->response->redirect($this->helper->url->to('user', 'share', array('user_id' => $user['id']))); + } + + $this->response->html($this->layout('user/share', array( + 'user' => $user, + 'title' => t('Public access'), + ))); + } + + /** * Password modification * * @access public @@ -301,7 +308,7 @@ class User extends Base } } - $this->response->html($this->layout('user_password', array( + $this->response->html($this->layout('user/password', array( 'values' => $values, 'errors' => $errors, 'user' => $user, @@ -323,9 +330,9 @@ class User extends Base if ($this->request->isPost()) { - $values = $this->request->getValues(); + $values = $this->request->getValues() + array('disable_login_form' => 0); - if ($this->acl->isAdminUser()) { + if ($this->userSession->isAdmin()) { $values += array('is_admin' => 0); } else { @@ -350,11 +357,13 @@ class User extends Base } } - $this->response->html($this->layout('user_edit', array( + $this->response->html($this->layout('user/edit', array( 'values' => $values, 'errors' => $errors, 'projects' => $this->projectPermission->filterProjects($this->project->getList(), $user['id']), 'user' => $user, + 'timezones' => $this->config->getTimezones(true), + 'languages' => $this->config->getLanguages(true), ))); } @@ -380,7 +389,7 @@ class User extends Base $this->response->redirect('?controller=user'); } - $this->response->html($this->layout('user_remove', array( + $this->response->html($this->layout('user/remove', array( 'user' => $user, ))); } @@ -401,22 +410,22 @@ class User extends Base if (is_array($profile)) { // If the user is already logged, link the account otherwise authenticate - if ($this->acl->isLogged()) { + if ($this->userSession->isLogged()) { - if ($this->authentication->backend('google')->updateUser($this->acl->getUserId(), $profile)) { + if ($this->authentication->backend('google')->updateUser($this->userSession->getId(), $profile)) { $this->session->flash(t('Your Google Account is linked to your profile successfully.')); } else { $this->session->flashError(t('Unable to link your Google Account.')); } - $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); + $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId()); } else if ($this->authentication->backend('google')->authenticate($profile['id'])) { $this->response->redirect('?controller=app'); } else { - $this->response->html($this->template->layout('user_login', array( + $this->response->html($this->template->layout('auth/index', array( 'errors' => array('login' => t('Google authentication failed')), 'values' => array(), 'no_layout' => true, @@ -438,14 +447,14 @@ class User extends Base public function unlinkGoogle() { $this->checkCSRFParam(); - if ($this->authentication->backend('google')->unlink($this->acl->getUserId())) { + if ($this->authentication->backend('google')->unlink($this->userSession->getId())) { $this->session->flash(t('Your Google Account is not linked anymore to your profile.')); } else { $this->session->flashError(t('Unable to unlink your Google Account.')); } - $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); + $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId()); } /** @@ -453,7 +462,7 @@ class User extends Base * * @access public */ - public function gitHub() + public function github() { $code = $this->request->getStringParam('code'); @@ -463,22 +472,22 @@ class User extends Base if (is_array($profile)) { // If the user is already logged, link the account otherwise authenticate - if ($this->acl->isLogged()) { + if ($this->userSession->isLogged()) { - if ($this->authentication->backend('gitHub')->updateUser($this->acl->getUserId(), $profile)) { + if ($this->authentication->backend('gitHub')->updateUser($this->userSession->getId(), $profile)) { $this->session->flash(t('Your GitHub account was successfully linked to your profile.')); } else { $this->session->flashError(t('Unable to link your GitHub Account.')); } - $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); + $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId()); } else if ($this->authentication->backend('gitHub')->authenticate($profile['id'])) { $this->response->redirect('?controller=app'); } else { - $this->response->html($this->template->layout('user_login', array( + $this->response->html($this->template->layout('auth/index', array( 'errors' => array('login' => t('GitHub authentication failed')), 'values' => array(), 'no_layout' => true, @@ -497,19 +506,19 @@ class User extends Base * * @access public */ - public function unlinkGitHub() + public function unlinkGithub() { $this->checkCSRFParam(); $this->authentication->backend('gitHub')->revokeGitHubAccess(); - if ($this->authentication->backend('gitHub')->unlink($this->acl->getUserId())) { + if ($this->authentication->backend('gitHub')->unlink($this->userSession->getId())) { $this->session->flash(t('Your GitHub account is no longer linked to your profile.')); } else { $this->session->flashError(t('Unable to unlink your GitHub Account.')); } - $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); + $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId()); } } diff --git a/app/Controller/Webhook.php b/app/Controller/Webhook.php index 71acab08..10a24e47 100644 --- a/app/Controller/Webhook.php +++ b/app/Controller/Webhook.php @@ -35,7 +35,7 @@ class Webhook extends Base list($valid,) = $this->taskValidator->validateCreation($values); - if ($valid && $this->task->create($values)) { + if ($valid && $this->taskCreation->create($values)) { $this->response->text('OK'); } @@ -55,9 +55,91 @@ class Webhook extends Base $this->githubWebhook->setProjectId($this->request->getIntegerParam('project_id')); - $this->githubWebhook->parsePayload( + $result = $this->githubWebhook->parsePayload( $this->request->getHeader('X-Github-Event'), - $this->request->getBody() + $this->request->getJson() ?: array() ); + + echo $result ? 'PARSED' : 'IGNORED'; + } + + /** + * Handle Gitlab webhooks + * + * @access public + */ + public function gitlab() + { + if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) { + $this->response->text('Not Authorized', 401); + } + + $this->gitlabWebhook->setProjectId($this->request->getIntegerParam('project_id')); + + $result = $this->gitlabWebhook->parsePayload( + $this->request->getJson() ?: array() + ); + + 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) ?: array()); + + echo $result ? 'PARSED' : 'IGNORED'; + } + + /** + * Handle Postmark webhooks + * + * @access public + */ + public function postmark() + { + if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) { + $this->response->text('Not Authorized', 401); + } + + echo $this->postmark->receiveEmail($this->request->getJson() ?: array()) ? 'PARSED' : 'IGNORED'; + } + + /** + * Handle Mailgun webhooks + * + * @access public + */ + public function mailgun() + { + if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) { + $this->response->text('Not Authorized', 401); + } + + echo $this->mailgun->receiveEmail($_POST) ? 'PARSED' : 'IGNORED'; + } + + /** + * Handle Sendgrid webhooks + * + * @access public + */ + public function sendgrid() + { + if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) { + $this->response->text('Not Authorized', 401); + } + + echo $this->sendgridWebhook->parsePayload($_POST) ? 'PARSED' : 'IGNORED'; } } |