diff --git a/sources/app/Action/TaskClose.php b/sources/app/Action/TaskClose.php index 760dfd8..b7cd4db 100644 --- a/sources/app/Action/TaskClose.php +++ b/sources/app/Action/TaskClose.php @@ -4,6 +4,7 @@ namespace Action; use Integration\GitlabWebhook; use Integration\GithubWebhook; +use Integration\BitbucketWebhook; use Model\Task; /** @@ -28,6 +29,7 @@ class TaskClose extends Base GithubWebhook::EVENT_ISSUE_CLOSED, GitlabWebhook::EVENT_COMMIT, GitlabWebhook::EVENT_ISSUE_CLOSED, + BitbucketWebhook::EVENT_COMMIT, ); } @@ -44,6 +46,7 @@ class TaskClose extends Base case GithubWebhook::EVENT_ISSUE_CLOSED: case GitlabWebhook::EVENT_COMMIT: case GitlabWebhook::EVENT_ISSUE_CLOSED: + case BitbucketWebhook::EVENT_COMMIT: return array(); default: return array('column_id' => t('Column')); @@ -63,6 +66,7 @@ class TaskClose extends Base case GithubWebhook::EVENT_ISSUE_CLOSED: case GitlabWebhook::EVENT_COMMIT: case GitlabWebhook::EVENT_ISSUE_CLOSED: + case BitbucketWebhook::EVENT_COMMIT: return array('task_id'); default: return array('task_id', 'column_id'); @@ -95,6 +99,7 @@ class TaskClose extends Base case GithubWebhook::EVENT_ISSUE_CLOSED: case GitlabWebhook::EVENT_COMMIT: case GitlabWebhook::EVENT_ISSUE_CLOSED: + case BitbucketWebhook::EVENT_COMMIT: return true; default: return $data['column_id'] == $this->getParam('column_id'); diff --git a/sources/app/Action/TaskLogMoveAnotherColumn.php b/sources/app/Action/TaskLogMoveAnotherColumn.php new file mode 100644 index 0000000..621e8e6 --- /dev/null +++ b/sources/app/Action/TaskLogMoveAnotherColumn.php @@ -0,0 +1,84 @@ + t('Column')); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array('task_id', 'column_id'); + } + + /** + * Execute the action (append to the task description). + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + if (! $this->userSession->isLogged()) { + return false; + } + + $column = $this->board->getColumn($data['column_id']); + + return (bool) $this->comment->create(array( + 'comment' => t('Moved to column %s', $column['title']), + 'task_id' => $data['task_id'], + 'user_id' => $this->userSession->getId(), + )); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return $data['column_id'] == $this->getParam('column_id'); + } +} diff --git a/sources/app/Auth/Database.php b/sources/app/Auth/Database.php index 2804b9a..e69f18a 100644 --- a/sources/app/Auth/Database.php +++ b/sources/app/Auth/Database.php @@ -30,9 +30,14 @@ class Database extends Base */ public function authenticate($username, $password) { - $user = $this->db->table(User::TABLE)->eq('username', $username)->eq('is_ldap_user', 0)->findOne(); + $user = $this->db + ->table(User::TABLE) + ->eq('username', $username) + ->eq('disable_login_form', 0) + ->eq('is_ldap_user', 0) + ->findOne(); - if ($user && password_verify($password, $user['password'])) { + if (is_array($user) && password_verify($password, $user['password'])) { $this->userSession->refresh($user); $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id'])); return true; diff --git a/sources/app/Auth/Ldap.php b/sources/app/Auth/Ldap.php index b344061..376d16f 100644 --- a/sources/app/Auth/Ldap.php +++ b/sources/app/Auth/Ldap.php @@ -29,6 +29,7 @@ class Ldap extends Base */ public function authenticate($username, $password) { + $username = LDAP_USERNAME_CASE_SENSITIVE ? $username : strtolower($username); $result = $this->findUser($username, $password); if (is_array($result)) { @@ -199,11 +200,90 @@ class Ldap extends Base return array( 'username' => $username, - 'name' => isset($info[0][LDAP_ACCOUNT_FULLNAME][0]) ? $info[0][LDAP_ACCOUNT_FULLNAME][0] : '', - 'email' => isset($info[0][LDAP_ACCOUNT_EMAIL][0]) ? $info[0][LDAP_ACCOUNT_EMAIL][0] : '', + 'name' => $this->getFromInfo($info, LDAP_ACCOUNT_FULLNAME), + 'email' => $this->getFromInfo($info, LDAP_ACCOUNT_EMAIL), ); } return false; } + + /** + * Retrieve info on LDAP user + * + * @param string $username Username + * @param string $email Email address + */ + public function lookup($username = null, $email = null) + { + $query = $this->getQuery($username, $email); + if ($query === false) { + return false; + } + + // Connect and attempt anonymous bind + $ldap = $this->connect(); + if (! is_resource($ldap) || ! $this->bind($ldap, null, null)) { + return false; + } + + // Try to find user + $sr = @ldap_search($ldap, LDAP_ACCOUNT_BASE, $query, array(LDAP_ACCOUNT_FULLNAME, LDAP_ACCOUNT_EMAIL, LDAP_ACCOUNT_ID)); + if ($sr === false) { + return false; + } + + $info = ldap_get_entries($ldap, $sr); + + // User not found + if (count($info) == 0 || $info['count'] == 0) { + return false; + } + + // User id not retrieved: LDAP_ACCOUNT_ID not properly configured + if (! $username && ! isset($info[0][LDAP_ACCOUNT_ID][0])) { + return false; + } + + return array( + 'username' => $this->getFromInfo($info, LDAP_ACCOUNT_ID, $username), + 'name' => $this->getFromInfo($info, LDAP_ACCOUNT_FULLNAME), + 'email' => $this->getFromInfo($info, LDAP_ACCOUNT_EMAIL, $email), + ); + } + + /** + * Get the LDAP query to find a user + * + * @param string $username Username + * @param string $email Email address + */ + private function getQuery($username, $email) + { + if ($username && $email) { + return '(&('.sprintf(LDAP_USER_PATTERN, $username).')('.sprintf(LDAP_ACCOUNT_EMAIL, $email).')'; + } + else if ($username) { + return sprintf(LDAP_USER_PATTERN, $username); + } + else if ($email) { + return '('.LDAP_ACCOUNT_EMAIL.'='.$email.')'; + } + else { + return false; + } + } + + /** + * Return a value from the LDAP info + * + * @param array $info LDAP info + * @param string $key Key + * @param string $default Default value if key not set in entry + * @return string + */ + private function getFromInfo($info, $key, $default = '') + { + return isset($info[0][$key][0]) ? $info[0][$key][0] : $default; + } } diff --git a/sources/app/Auth/ReverseProxy.php b/sources/app/Auth/ReverseProxy.php index b84550c..6cd01b2 100644 --- a/sources/app/Auth/ReverseProxy.php +++ b/sources/app/Auth/ReverseProxy.php @@ -66,6 +66,7 @@ class ReverseProxy extends Base 'username' => $login, 'is_admin' => REVERSE_PROXY_DEFAULT_ADMIN === $login, 'is_ldap_user' => 1, + 'disable_login_form' => 1, )); } } diff --git a/sources/app/Controller/App.php b/sources/app/Controller/App.php index aa2673a..46731e7 100644 --- a/sources/app/Controller/App.php +++ b/sources/app/Controller/App.php @@ -2,7 +2,8 @@ namespace Controller; -use Model\SubTask as SubTaskModel; +use Model\Subtask as SubtaskModel; +use Model\Task as TaskModel; /** * Application controller @@ -22,164 +23,62 @@ class App extends Base $this->response->text('OK'); } + /** + * User dashboard view for admins + * + * @access public + */ + public function dashboard() + { + $this->index($this->request->getIntegerParam('user_id'), 'dashboard'); + } + /** * Dashboard for the current user * * @access public */ - public function index() + public function index($user_id = 0, $action = 'index') { - $paginate = $this->request->getStringParam('paginate', 'userTasks'); - $offset = $this->request->getIntegerParam('offset', 0); - $direction = $this->request->getStringParam('direction'); - $order = $this->request->getStringParam('order'); - - $user_id = $this->userSession->getId(); - $projects = $this->projectPermission->getMemberProjects($user_id); + $status = array(SubTaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS); + $user_id = $user_id ?: $this->userSession->getId(); + $projects = $this->projectPermission->getActiveMemberProjects($user_id); $project_ids = array_keys($projects); - $params = array( + $task_paginator = $this->paginator + ->setUrl('app', $action, array('pagination' => 'tasks')) + ->setMax(10) + ->setOrder('tasks.id') + ->setQuery($this->taskFinder->getUserQuery($user_id)) + ->calculateOnlyIf($this->request->getStringParam('pagination') === 'tasks'); + + $subtask_paginator = $this->paginator + ->setUrl('app', $action, array('pagination' => 'subtasks')) + ->setMax(10) + ->setOrder('tasks.id') + ->setQuery($this->subtask->getUserQuery($user_id, $status)) + ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); + + $project_paginator = $this->paginator + ->setUrl('app', $action, array('pagination' => 'projects')) + ->setMax(10) + ->setOrder('name') + ->setQuery($this->project->getQueryColumnStats($project_ids)) + ->calculateOnlyIf($this->request->getStringParam('pagination') === 'projects'); + + $this->response->html($this->template->layout('app/dashboard', array( 'title' => t('Dashboard'), 'board_selector' => $this->projectPermission->getAllowedProjects($user_id), - 'events' => $this->projectActivity->getProjects($project_ids, 10), - ); - - $params += $this->getTaskPagination($user_id, $paginate, $offset, $order, $direction); - $params += $this->getSubtaskPagination($user_id, $paginate, $offset, $order, $direction); - $params += $this->getProjectPagination($project_ids, $paginate, $offset, $order, $direction); - - $this->response->html($this->template->layout('app/dashboard', $params)); + 'events' => $this->projectActivity->getProjects($project_ids, 5), + 'task_paginator' => $task_paginator, + 'subtask_paginator' => $subtask_paginator, + 'project_paginator' => $project_paginator, + 'user_id' => $user_id, + ))); } /** - * Get tasks pagination - * - * @access public - * @param integer $user_id - * @param string $paginate - * @param integer $offset - * @param string $order - * @param string $direction - */ - private function getTaskPagination($user_id, $paginate, $offset, $order, $direction) - { - $limit = 10; - - if (! in_array($order, array('tasks.id', 'project_name', 'title', 'date_due'))) { - $order = 'tasks.id'; - $direction = 'ASC'; - } - - if ($paginate === 'userTasks') { - $tasks = $this->taskPaginator->userTasks($user_id, $offset, $limit, $order, $direction); - } - else { - $offset = 0; - $tasks = $this->taskPaginator->userTasks($user_id, $offset, $limit); - } - - return array( - 'tasks' => $tasks, - 'task_pagination' => array( - 'controller' => 'app', - 'action' => 'index', - 'params' => array('paginate' => 'userTasks'), - 'direction' => $direction, - 'order' => $order, - 'total' => $this->taskPaginator->countUserTasks($user_id), - 'offset' => $offset, - 'limit' => $limit, - ) - ); - } - - /** - * Get subtasks pagination - * - * @access public - * @param integer $user_id - * @param string $paginate - * @param integer $offset - * @param string $order - * @param string $direction - */ - private function getSubtaskPagination($user_id, $paginate, $offset, $order, $direction) - { - $status = array(SubTaskModel::STATUS_TODO, SubTaskModel::STATUS_INPROGRESS); - $limit = 10; - - if (! in_array($order, array('tasks.id', 'project_name', 'status', 'title'))) { - $order = 'tasks.id'; - $direction = 'ASC'; - } - - if ($paginate === 'userSubtasks') { - $subtasks = $this->subtaskPaginator->userSubtasks($user_id, $status, $offset, $limit, $order, $direction); - } - else { - $offset = 0; - $subtasks = $this->subtaskPaginator->userSubtasks($user_id, $status, $offset, $limit); - } - - return array( - 'subtasks' => $subtasks, - 'subtask_pagination' => array( - 'controller' => 'app', - 'action' => 'index', - 'params' => array('paginate' => 'userSubtasks'), - 'direction' => $direction, - 'order' => $order, - 'total' => $this->subtaskPaginator->countUserSubtasks($user_id, $status), - 'offset' => $offset, - 'limit' => $limit, - ) - ); - } - - /** - * Get projects pagination - * - * @access public - * @param array $project_ids - * @param string $paginate - * @param integer $offset - * @param string $order - * @param string $direction - */ - private function getProjectPagination(array $project_ids, $paginate, $offset, $order, $direction) - { - $limit = 10; - - if (! in_array($order, array('id', 'name'))) { - $order = 'name'; - $direction = 'ASC'; - } - - if ($paginate === 'projectSummaries') { - $projects = $this->projectPaginator->projectSummaries($project_ids, $offset, $limit, $order, $direction); - } - else { - $offset = 0; - $projects = $this->projectPaginator->projectSummaries($project_ids, $offset, $limit); - } - - return array( - 'projects' => $projects, - 'project_pagination' => array( - 'controller' => 'app', - 'action' => 'index', - 'params' => array('paginate' => 'projectSummaries'), - 'direction' => $direction, - 'order' => $order, - 'total' => count($project_ids), - 'offset' => $offset, - 'limit' => $limit, - ) - ); - } - - /** - * Render Markdown Text and reply with the HTML Code + * Render Markdown text and reply with the HTML Code * * @access public */ @@ -190,10 +89,34 @@ class App extends Base if (empty($payload['text'])) { $this->response->html('

'.t('Nothing to preview...').'

'); } - else { - $this->response->html( - $this->template->markdown($payload['text']) - ); - } + + $this->response->html($this->template->markdown($payload['text'])); + } + + /** + * Colors stylesheet + * + * @access public + */ + public function colors() + { + $this->response->css($this->color->getCss()); + } + + /** + * Task autocompletion (Ajax) + * + * @access public + */ + public function autocomplete() + { + $this->response->json( + $this->taskFilter + ->create() + ->filterByProjects($this->projectPermission->getActiveMemberProjectIds($this->userSession->getId())) + ->excludeTasks(array($this->request->getIntegerParam('exclude_task_id'))) + ->filterByTitle($this->request->getStringParam('term')) + ->toAutoCompletion() + ); } } diff --git a/sources/app/Controller/Base.php b/sources/app/Controller/Base.php index 8a5354a..d949048 100644 --- a/sources/app/Controller/Base.php +++ b/sources/app/Controller/Base.php @@ -17,8 +17,13 @@ use Symfony\Component\EventDispatcher\Event; * @package controller * @author Frederic Guillot * + * @property \Core\Helper $helper * @property \Core\Session $session * @property \Core\Template $template + * @property \Core\Paginator $paginator + * @property \Integration\GithubWebhook $githubWebhook + * @property \Integration\GitlabWebhook $gitlabWebhook + * @property \Integration\BitbucketWebhook $bitbucketWebhook * @property \Model\Acl $acl * @property \Model\Authentication $authentication * @property \Model\Action $action @@ -33,22 +38,29 @@ use Symfony\Component\EventDispatcher\Event; * @property \Model\Notification $notification * @property \Model\Project $project * @property \Model\ProjectPermission $projectPermission + * @property \Model\ProjectDuplication $projectDuplication * @property \Model\ProjectAnalytic $projectAnalytic + * @property \Model\ProjectActivity $projectActivity * @property \Model\ProjectDailySummary $projectDailySummary - * @property \Model\SubTask $subTask + * @property \Model\Subtask $subtask + * @property \Model\Swimlane $swimlane * @property \Model\Task $task + * @property \Model\Link $link * @property \Model\TaskCreation $taskCreation * @property \Model\TaskModification $taskModification * @property \Model\TaskDuplication $taskDuplication * @property \Model\TaskHistory $taskHistory * @property \Model\TaskExport $taskExport * @property \Model\TaskFinder $taskFinder + * @property \Model\TaskFilter $taskFilter * @property \Model\TaskPosition $taskPosition * @property \Model\TaskPermission $taskPermission * @property \Model\TaskStatus $taskStatus * @property \Model\TaskValidator $taskValidator + * @property \Model\TaskLink $taskLink * @property \Model\CommentHistory $commentHistory * @property \Model\SubtaskHistory $subtaskHistory + * @property \Model\SubtaskTimeTracking $subtaskTimeTracking * @property \Model\TimeTracking $timeTracking * @property \Model\User $user * @property \Model\UserSession $userSession @@ -107,7 +119,7 @@ abstract class Base } $this->container['logger']->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nb_queries)); - $this->container['logger']->debug('RENDERING={time}', array('time' => microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'])); + $this->container['logger']->debug('RENDERING={time}', array('time' => microtime(true) - @$_SERVER['REQUEST_TIME_FLOAT'])); } } @@ -131,7 +143,7 @@ abstract class Base private function sendHeaders($action) { // HTTP secure headers - $this->response->csp(array('style-src' => "'self' 'unsafe-inline'")); + $this->response->csp(array('style-src' => "'self' 'unsafe-inline'", 'img-src' => '*')); $this->response->nosniff(); $this->response->xss(); @@ -158,16 +170,19 @@ abstract class Base $this->container['dispatcher']->dispatch('session.bootstrap', new Event); if (! $this->acl->isPublicAction($controller, $action)) { - $this->handleAuthenticatedUser($controller, $action); + $this->handleAuthentication(); + $this->handleAuthorization($controller, $action); + + $this->session['has_subtask_inprogress'] = $this->subtask->hasSubtaskInProgress($this->userSession->getId()); } } /** - * Check page access and authentication + * Check authentication * * @access public */ - public function handleAuthenticatedUser($controller, $action) + public function handleAuthentication() { if (! $this->authentication->isAuthenticated()) { @@ -177,8 +192,24 @@ abstract class Base $this->response->redirect('?controller=user&action=login&redirect_query='.urlencode($this->request->getQueryString())); } + } - if (! $this->acl->isAllowed($controller, $action, $this->request->getIntegerParam('project_id', 0))) { + /** + * Check page access and authorization + * + * @access public + */ + public function handleAuthorization($controller, $action) + { + $project_id = $this->request->getIntegerParam('project_id'); + $task_id = $this->request->getIntegerParam('task_id'); + + // Allow urls without "project_id" + if ($task_id > 0 && $project_id === 0) { + $project_id = $this->taskFinder->getProjectId($task_id); + } + + if (! $this->acl->isAllowed($controller, $action, $project_id)) { $this->forbidden(); } } @@ -280,7 +311,7 @@ abstract class Base { $task = $this->taskFinder->getDetails($this->request->getIntegerParam('task_id')); - if (! $task || $task['project_id'] != $this->request->getIntegerParam('project_id')) { + if (! $task) { $this->notfound(); } diff --git a/sources/app/Controller/Board.php b/sources/app/Controller/Board.php index 48f2b51..90b7f35 100644 --- a/sources/app/Controller/Board.php +++ b/sources/app/Controller/Board.php @@ -205,6 +205,7 @@ class Board extends Base foreach ($columns as $column) { $values['title['.$column['id'].']'] = $column['title']; + $values['description['.$column['id'].']'] = $column['description']; $values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null; } @@ -218,28 +219,39 @@ class Board extends Base } /** - * Validate and update a board + * Display a form to edit a board * * @access public */ - public function update() + public function editColumn(array $values = array(), array $errors = array()) { $project = $this->getProject(); - $columns = $this->board->getColumns($project['id']); - $data = $this->request->getValues(); - $values = $columns_list = array(); + $column = $this->board->getColumn($this->request->getIntegerParam('column_id')); - foreach ($columns as $column) { - $columns_list[$column['id']] = $column['title']; - $values['title['.$column['id'].']'] = isset($data['title'][$column['id']]) ? $data['title'][$column['id']] : ''; - $values['task_limit['.$column['id'].']'] = isset($data['task_limit'][$column['id']]) ? $data['task_limit'][$column['id']] : 0; - } + $this->response->html($this->projectLayout('board/edit_column', array( + 'errors' => $errors, + 'values' => $values ?: $column, + 'project' => $project, + 'column' => $column, + 'title' => t('Edit column "%s"', $column['title']) + ))); + } - list($valid, $errors) = $this->board->validateModification($columns_list, $values); + /** + * Validate and update a column + * + * @access public + */ + public function updateColumn() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->board->validateModification($values); if ($valid) { - if ($this->board->update($data)) { + if ($this->board->updateColumn($values['id'], $values['title'], $values['task_limit'], $values['description'])) { $this->session->flash(t('Board updated successfully.')); $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']); } @@ -248,7 +260,7 @@ class Board extends Base } } - $this->edit($values, $errors); + $this->editcolumn($values, $errors); } /** @@ -271,7 +283,7 @@ class Board extends Base if ($valid) { - if ($this->board->addColumn($project['id'], $data['title'])) { + if ($this->board->addColumn($project['id'], $data['title'], $data['task_limit'], $data['description'])) { $this->session->flash(t('Board updated successfully.')); $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']); } @@ -389,6 +401,20 @@ class Board extends Base ); } + /** + * Get links on mouseover + * + * @access public + */ + public function tasklinks() + { + $task = $this->getTask(); + $this->response->html($this->template->render('board/tasklinks', array( + 'links' => $this->taskLink->getLinks($task['id']), + 'task' => $task, + ))); + } + /** * Get subtasks on mouseover * @@ -398,23 +424,7 @@ class Board extends Base { $task = $this->getTask(); $this->response->html($this->template->render('board/subtasks', array( - 'subtasks' => $this->subTask->getAll($task['id']), - 'task' => $task, - ))); - } - - /** - * Change the status of a subtask from the mouseover - * - * @access public - */ - public function toggleSubtask() - { - $task = $this->getTask(); - $this->subTask->toggleStatus($this->request->getIntegerParam('subtask_id')); - - $this->response->html($this->template->render('board/subtasks', array( - 'subtasks' => $this->subTask->getAll($task['id']), + 'subtasks' => $this->subtask->getAll($task['id']), 'task' => $task, ))); } @@ -449,7 +459,7 @@ class Board extends Base } /** - * Display the description + * Display task description * * @access public */ diff --git a/sources/app/Controller/Calendar.php b/sources/app/Controller/Calendar.php new file mode 100644 index 0000000..1c7ac7c --- /dev/null +++ b/sources/app/Controller/Calendar.php @@ -0,0 +1,107 @@ +getProject(); + + $this->response->html($this->template->layout('calendar/show', array( + 'check_interval' => $this->config->get('board_private_refresh_interval'), + 'users_list' => $this->projectPermission->getMemberList($project['id'], true, true), + 'categories_list' => $this->category->getList($project['id'], true, true), + 'columns_list' => $this->board->getColumnsList($project['id'], true), + 'swimlanes_list' => $this->swimlane->getList($project['id'], true), + 'colors_list' => $this->color->getList(true), + 'status_list' => $this->taskStatus->getList(true), + 'project' => $project, + 'title' => t('Calendar for "%s"', $project['name']), + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + ))); + } + + /** + * Get tasks to display on the calendar (project view) + * + * @access public + */ + public function project() + { + $project_id = $this->request->getIntegerParam('project_id'); + $start = $this->request->getStringParam('start'); + $end = $this->request->getStringParam('end'); + + $due_tasks = $this->taskFilter + ->create() + ->filterByProject($project_id) + ->filterByCategory($this->request->getIntegerParam('category_id', -1)) + ->filterByOwner($this->request->getIntegerParam('owner_id', -1)) + ->filterByColumn($this->request->getIntegerParam('column_id', -1)) + ->filterBySwimlane($this->request->getIntegerParam('swimlane_id', -1)) + ->filterByColor($this->request->getStringParam('color_id')) + ->filterByStatus($this->request->getIntegerParam('is_active', -1)) + ->filterByDueDateRange($start, $end) + ->toCalendarEvents(); + + $subtask_timeslots = $this->subtaskTimeTracking->getProjectCalendarEvents($project_id, $start, $end); + + $this->response->json(array_merge($due_tasks, $subtask_timeslots)); + } + + /** + * Get tasks to display on the calendar (user view) + * + * @access public + */ + public function user() + { + $user_id = $this->request->getIntegerParam('user_id'); + $start = $this->request->getStringParam('start'); + $end = $this->request->getStringParam('end'); + + $due_tasks = $this->taskFilter + ->create() + ->filterByOwner($user_id) + ->filterByStatus(TaskModel::STATUS_OPEN) + ->filterByDueDateRange($start, $end) + ->toCalendarEvents(); + + $subtask_timeslots = $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end); + + $this->response->json(array_merge($due_tasks, $subtask_timeslots)); + } + + /** + * Update task due date + * + * @access public + */ + public function save() + { + if ($this->request->isAjax() && $this->request->isPost()) { + + $values = $this->request->getJson(); + + $this->taskModification->update(array( + 'id' => $values['task_id'], + 'date_due' => $values['date_due'], + )); + } + } +} diff --git a/sources/app/Controller/Comment.php b/sources/app/Controller/Comment.php index 9796ea3..5003200 100644 --- a/sources/app/Controller/Comment.php +++ b/sources/app/Controller/Comment.php @@ -41,6 +41,7 @@ class Comment extends Base public function create(array $values = array(), array $errors = array()) { $task = $this->getTask(); + $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); if (empty($values)) { $values = array( @@ -49,11 +50,20 @@ class Comment extends Base ); } + if ($ajax) { + $this->response->html($this->template->render('comment/create', array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'ajax' => $ajax, + ))); + } + $this->response->html($this->taskLayout('comment/create', array( 'values' => $values, 'errors' => $errors, 'task' => $task, - 'title' => t('Add a comment') + 'title' => t('Add a comment'), ))); } @@ -66,6 +76,7 @@ class Comment extends Base { $task = $this->getTask(); $values = $this->request->getValues(); + $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); list($valid, $errors) = $this->comment->validateCreation($values); @@ -78,6 +89,10 @@ class Comment extends Base $this->session->flashError(t('Unable to create your comment.')); } + if ($ajax) { + $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); + } + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#comments'); } diff --git a/sources/app/Controller/Config.php b/sources/app/Controller/Config.php index 9005c30..01c7ad5 100644 --- a/sources/app/Controller/Config.php +++ b/sources/app/Controller/Config.php @@ -38,7 +38,7 @@ class Config extends Base { if ($this->request->isPost()) { - $values = $this->request->getValues(); + $values = $this->request->getValues() + array('subtask_restriction' => 0, 'subtask_time_tracking' => 0); if ($this->config->save($values)) { $this->config->reload(); diff --git a/sources/app/Controller/File.php b/sources/app/Controller/File.php index 6305261..3255fe8 100644 --- a/sources/app/Controller/File.php +++ b/sources/app/Controller/File.php @@ -2,8 +2,6 @@ namespace Controller; -use Model\File as FileModel; - /** * File controller * @@ -54,7 +52,7 @@ class File extends Base { $task = $this->getTask(); $file = $this->file->getById($this->request->getIntegerParam('file_id')); - $filename = FileModel::BASE_PATH.$file['path']; + $filename = FILES_DIR.$file['path']; if ($file['task_id'] == $task['id'] && file_exists($filename)) { $this->response->forceDownload($file['name']); @@ -91,7 +89,7 @@ class File extends Base { $task = $this->getTask(); $file = $this->file->getById($this->request->getIntegerParam('file_id')); - $filename = FileModel::BASE_PATH.$file['path']; + $filename = FILES_DIR.$file['path']; if ($file['task_id'] == $task['id'] && file_exists($filename)) { $metadata = getimagesize($filename); diff --git a/sources/app/Controller/Link.php b/sources/app/Controller/Link.php new file mode 100644 index 0000000..ec9c619 --- /dev/null +++ b/sources/app/Controller/Link.php @@ -0,0 +1,162 @@ +projectPermission->getAllowedProjects($this->userSession->getId()); + $params['config_content_for_layout'] = $this->template->render($template, $params); + + return $this->template->layout('config/layout', $params); + } + + /** + * Get the current link + * + * @access private + * @return array + */ + private function getLink() + { + $link = $this->link->getById($this->request->getIntegerParam('link_id')); + + if (! $link) { + $this->notfound(); + } + + return $link; + } + + /** + * List of links + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $this->response->html($this->layout('link/index', array( + 'links' => $this->link->getMergedList(), + 'values' => $values, + 'errors' => $errors, + 'title' => t('Settings').' > '.t('Task\'s links'), + ))); + } + + /** + * Validate and save a new link + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->link->validateCreation($values); + + if ($valid) { + + if ($this->link->create($values['label'], $values['opposite_label'])) { + $this->session->flash(t('Link added successfully.')); + $this->response->redirect($this->helper->url('link', 'index')); + } + else { + $this->session->flashError(t('Unable to create your link.')); + } + } + + $this->index($values, $errors); + } + + /** + * Edit form + * + * @access public + */ + public function edit(array $values = array(), array $errors = array()) + { + $link = $this->getLink(); + $link['label'] = t($link['label']); + + $this->response->html($this->layout('link/edit', array( + 'values' => $values ?: $link, + 'errors' => $errors, + 'labels' => $this->link->getList($link['id']), + 'link' => $link, + 'title' => t('Link modification') + ))); + } + + /** + * Edit a link (validate the form and update the database) + * + * @access public + */ + public function update() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->link->validateModification($values); + + if ($valid) { + if ($this->link->update($values)) { + $this->session->flash(t('Link updated successfully.')); + $this->response->redirect($this->helper->url('link', 'index')); + } + else { + $this->session->flashError(t('Unable to update your link.')); + } + } + + $this->edit($values, $errors); + } + + /** + * Confirmation dialog before removing a link + * + * @access public + */ + public function confirm() + { + $link = $this->getLink(); + + $this->response->html($this->layout('link/remove', array( + 'link' => $link, + 'title' => t('Remove a link') + ))); + } + + /** + * Remove a link + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $link = $this->getLink(); + + if ($this->link->remove($link['id'])) { + $this->session->flash(t('Link removed successfully.')); + } + else { + $this->session->flashError(t('Unable to remove this link.')); + } + + $this->response->redirect($this->helper->url('link', 'index')); + } +} diff --git a/sources/app/Controller/Project.php b/sources/app/Controller/Project.php index d0da53d..fb0a8d0 100644 --- a/sources/app/Controller/Project.php +++ b/sources/app/Controller/Project.php @@ -17,24 +17,25 @@ class Project extends Base */ public function index() { - $projects = $this->project->getAll(! $this->userSession->isAdmin()); - $nb_projects = count($projects); - $active_projects = array(); - $inactive_projects = array(); - - foreach ($projects as $project) { - if ($project['is_active'] == 1) { - $active_projects[] = $project; - } - else { - $inactive_projects[] = $project; - } + if ($this->userSession->isAdmin()) { + $project_ids = $this->project->getAllIds(); } + else { + $project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId()); + } + + $nb_projects = count($project_ids); + + $paginator = $this->paginator + ->setUrl('project', 'index') + ->setMax(20) + ->setOrder('name') + ->setQuery($this->project->getQueryColumnStats($project_ids)) + ->calculate(); $this->response->html($this->template->layout('project/index', array( 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), - 'active_projects' => $active_projects, - 'inactive_projects' => $inactive_projects, + 'paginator' => $paginator, 'nb_projects' => $nb_projects, 'title' => t('Projects').' ('.$nb_projects.')' ))); @@ -51,7 +52,7 @@ class Project extends Base $this->response->html($this->projectLayout('project/show', array( 'project' => $project, - 'stats' => $this->project->getStats($project['id']), + 'stats' => $this->project->getTaskStats($project['id']), 'title' => $project['name'], ))); } @@ -297,6 +298,7 @@ class Project extends Base * Duplicate a project * * @author Antonio Rabelo + * @author Michael Lüpkes * @access public */ public function duplicate() @@ -304,10 +306,8 @@ class Project extends Base $project = $this->getProject(); if ($this->request->getStringParam('duplicate') === 'yes') { - - $this->checkCSRFParam(); - - if ($this->project->duplicate($project['id'])) { + $values = array_keys($this->request->getValues()); + if ($this->projectDuplication->duplicate($project['id'], $values)) { $this->session->flash(t('Project cloned successfully.')); } else { $this->session->flashError(t('Unable to clone this project.')); @@ -425,38 +425,32 @@ class Project extends Base { $project = $this->getProject(); $search = $this->request->getStringParam('search'); - $direction = $this->request->getStringParam('direction', 'DESC'); - $order = $this->request->getStringParam('order', 'tasks.id'); - $offset = $this->request->getIntegerParam('offset', 0); - $tasks = array(); $nb_tasks = 0; - $limit = 25; + + $paginator = $this->paginator + ->setUrl('project', 'search', array('search' => $search, 'project_id' => $project['id'])) + ->setMax(30) + ->setOrder('tasks.id') + ->setDirection('DESC'); if ($search !== '') { - $tasks = $this->taskPaginator->searchTasks($project['id'], $search, $offset, $limit, $order, $direction); - $nb_tasks = $this->taskPaginator->countSearchTasks($project['id'], $search); + + $paginator + ->setQuery($this->taskFinder->getSearchQuery($project['id'], $search)) + ->calculate(); + + $nb_tasks = $paginator->getTotal(); } $this->response->html($this->template->layout('project/search', array( 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), - 'tasks' => $tasks, - 'nb_tasks' => $nb_tasks, - 'pagination' => array( - 'controller' => 'project', - 'action' => 'search', - 'params' => array('search' => $search, 'project_id' => $project['id']), - 'direction' => $direction, - 'order' => $order, - 'total' => $nb_tasks, - 'offset' => $offset, - 'limit' => $limit, - ), 'values' => array( 'search' => $search, 'controller' => 'project', 'action' => 'search', 'project_id' => $project['id'], ), + 'paginator' => $paginator, 'project' => $project, 'columns' => $this->board->getColumnsList($project['id']), 'categories' => $this->category->getList($project['id'], false), @@ -472,32 +466,21 @@ class Project extends Base public function tasks() { $project = $this->getProject(); - $direction = $this->request->getStringParam('direction', 'DESC'); - $order = $this->request->getStringParam('order', 'tasks.date_completed'); - $offset = $this->request->getIntegerParam('offset', 0); - $limit = 25; - - $tasks = $this->taskPaginator->closedTasks($project['id'], $offset, $limit, $order, $direction); - $nb_tasks = $this->taskPaginator->countClosedTasks($project['id']); + $paginator = $this->paginator + ->setUrl('project', 'tasks', array('project_id' => $project['id'])) + ->setMax(30) + ->setOrder('tasks.id') + ->setDirection('DESC') + ->setQuery($this->taskFinder->getClosedTaskQuery($project['id'])) + ->calculate(); $this->response->html($this->template->layout('project/tasks', array( 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), - 'pagination' => array( - 'controller' => 'project', - 'action' => 'tasks', - 'params' => array('project_id' => $project['id']), - 'direction' => $direction, - 'order' => $order, - 'total' => $nb_tasks, - 'offset' => $offset, - 'limit' => $limit, - ), 'project' => $project, 'columns' => $this->board->getColumnsList($project['id']), 'categories' => $this->category->getList($project['id'], false), - 'tasks' => $tasks, - 'nb_tasks' => $nb_tasks, - 'title' => t('Completed tasks for "%s"', $project['name']).' ('.$nb_tasks.')' + 'paginator' => $paginator, + 'title' => t('Completed tasks for "%s"', $project['name']).' ('.$paginator->getTotal().')' ))); } diff --git a/sources/app/Controller/Subtask.php b/sources/app/Controller/Subtask.php index 0521b89..c7ec00d 100644 --- a/sources/app/Controller/Subtask.php +++ b/sources/app/Controller/Subtask.php @@ -2,8 +2,10 @@ namespace Controller; +use Model\Subtask as SubtaskModel; + /** - * SubTask controller + * Subtask controller * * @package controller * @author Frederic Guillot @@ -18,7 +20,7 @@ class Subtask extends Base */ private function getSubtask() { - $subtask = $this->subTask->getById($this->request->getIntegerParam('subtask_id')); + $subtask = $this->subtask->getById($this->request->getIntegerParam('subtask_id')); if (! $subtask) { $this->notfound(); @@ -61,11 +63,11 @@ class Subtask extends Base $task = $this->getTask(); $values = $this->request->getValues(); - list($valid, $errors) = $this->subTask->validateCreation($values); + list($valid, $errors) = $this->subtask->validateCreation($values); if ($valid) { - if ($this->subTask->create($values)) { + if ($this->subtask->create($values)) { $this->session->flash(t('Sub-task added successfully.')); } else { @@ -96,7 +98,7 @@ class Subtask extends Base 'values' => empty($values) ? $subtask : $values, 'errors' => $errors, 'users_list' => $this->projectPermission->getMemberList($task['project_id']), - 'status_list' => $this->subTask->getStatusList(), + 'status_list' => $this->subtask->getStatusList(), 'subtask' => $subtask, 'task' => $task, ))); @@ -113,11 +115,11 @@ class Subtask extends Base $this->getSubtask(); $values = $this->request->getValues(); - list($valid, $errors) = $this->subTask->validateModification($values); + list($valid, $errors) = $this->subtask->validateModification($values); if ($valid) { - if ($this->subTask->update($values)) { + if ($this->subtask->update($values)) { $this->session->flash(t('Sub-task updated successfully.')); } else { @@ -157,7 +159,7 @@ class Subtask extends Base $task = $this->getTask(); $subtask = $this->getSubtask(); - if ($this->subTask->remove($subtask['id'])) { + if ($this->subtask->remove($subtask['id'])) { $this->session->flash(t('Sub-task removed successfully.')); } else { @@ -175,12 +177,86 @@ class Subtask extends Base public function toggleStatus() { $task = $this->getTask(); - $subtask_id = $this->request->getIntegerParam('subtask_id'); + $subtask = $this->getSubtask(); + $redirect = $this->request->getStringParam('redirect', 'task'); - if (! $this->subTask->toggleStatus($subtask_id)) { - $this->session->flashError(t('Unable to update your sub-task.')); + $this->subtask->toggleStatus($subtask['id']); + + if ($redirect === 'board') { + + $this->session['has_subtask_inprogress'] = $this->subtask->hasSubtaskInProgress($this->userSession->getId()); + + $this->response->html($this->template->render('board/subtasks', array( + 'subtasks' => $this->subtask->getAll($task['id']), + 'task' => $task, + ))); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#subtasks'); + $this->toggleRedirect($task, $redirect); + } + + /** + * Handle subtask restriction (popover) + * + * @access public + */ + public function subtaskRestriction() + { + $task = $this->getTask(); + $subtask = $this->getSubtask(); + + $this->response->html($this->template->render('subtask/restriction_change_status', array( + 'status_list' => array( + SubtaskModel::STATUS_TODO => t('Todo'), + SubtaskModel::STATUS_DONE => t('Done'), + ), + 'subtask_inprogress' => $this->subtask->getSubtaskInProgress($this->userSession->getId()), + 'subtask' => $subtask, + 'task' => $task, + 'redirect' => $this->request->getStringParam('redirect'), + ))); + } + + /** + * Change status of the in progress subtask and the other subtask + * + * @access public + */ + public function changeRestrictionStatus() + { + $task = $this->getTask(); + $subtask = $this->getSubtask(); + $values = $this->request->getValues(); + + // Change status of the previous in progress subtask + $this->subtask->update(array( + 'id' => $values['id'], + 'status' => $values['status'], + )); + + // Set the current subtask to in pogress + $this->subtask->update(array( + 'id' => $subtask['id'], + 'status' => SubtaskModel::STATUS_INPROGRESS, + )); + + $this->toggleRedirect($task, $values['redirect']); + } + + /** + * Redirect to the right page + * + * @access private + */ + private function toggleRedirect(array $task, $redirect) + { + switch ($redirect) { + case 'board': + $this->response->redirect($this->helper->url('board', 'show', array('project_id' => $task['project_id']))); + case 'dashboard': + $this->response->redirect($this->helper->url('app', 'index')); + default: + $this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + } } } diff --git a/sources/app/Controller/Task.php b/sources/app/Controller/Task.php index 7f85f36..741db61 100644 --- a/sources/app/Controller/Task.php +++ b/sources/app/Controller/Task.php @@ -35,7 +35,8 @@ class Task extends Base $this->response->html($this->template->layout('task/public', array( 'project' => $project, 'comments' => $this->comment->getAll($task['id']), - 'subtasks' => $this->subTask->getAll($task['id']), + 'subtasks' => $this->subtask->getAll($task['id']), + 'links' => $this->taskLink->getLinks($task['id']), 'task' => $task, 'columns_list' => $this->board->getColumnsList($task['project_id']), 'colors_list' => $this->color->getList(), @@ -54,7 +55,7 @@ class Task extends Base public function show() { $task = $this->getTask(); - $subtasks = $this->subTask->getAll($task['id']); + $subtasks = $this->subtask->getAll($task['id']); $values = array( 'id' => $task['id'], @@ -70,9 +71,9 @@ class Task extends Base 'files' => $this->file->getAll($task['id']), 'comments' => $this->comment->getAll($task['id']), 'subtasks' => $subtasks, + 'links' => $this->taskLink->getLinks($task['id']), 'task' => $task, 'values' => $values, - 'timesheet' => $this->timeTracking->getTaskTimesheet($task, $subtasks), 'columns_list' => $this->board->getColumnsList($task['project_id']), 'colors_list' => $this->color->getList(), 'date_format' => $this->config->get('application_date_format'), @@ -250,6 +251,7 @@ class Task extends Base public function close() { $task = $this->getTask(); + $redirect = $this->request->getStringParam('redirect'); if ($this->request->getStringParam('confirmation') === 'yes') { @@ -261,11 +263,23 @@ class Task extends Base $this->session->flashError(t('Unable to close this task.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); + if ($redirect === 'board') { + $this->response->redirect($this->helper->url('board', 'show', array('project_id' => $task['project_id']))); + } + + $this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + } + + if ($this->request->isAjax()) { + $this->response->html($this->template->render('task/close', array( + 'task' => $task, + 'redirect' => $redirect, + ))); } $this->response->html($this->taskLayout('task/close', array( 'task' => $task, + 'redirect' => $redirect, ))); } @@ -418,7 +432,7 @@ class Task extends Base $task = $this->getTask(); $values = $task; $errors = array(); - $projects_list = $this->projectPermission->getMemberProjects($this->userSession->getId()); + $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId()); unset($projects_list[$task['project_id']]); @@ -457,7 +471,7 @@ class Task extends Base $task = $this->getTask(); $values = $task; $errors = array(); - $projects_list = $this->projectPermission->getMemberProjects($this->userSession->getId()); + $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId()); unset($projects_list[$task['project_id']]); @@ -485,4 +499,27 @@ class Task extends Base 'projects_list' => $projects_list, ))); } + + /** + * Display the time tracking details + * + * @access public + */ + public function timesheet() + { + $task = $this->getTask(); + + $subtask_paginator = $this->paginator + ->setUrl('task', 'timesheet', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'pagination' => 'subtasks')) + ->setMax(15) + ->setOrder('start') + ->setDirection('DESC') + ->setQuery($this->subtaskTimeTracking->getTaskQuery($task['id'])) + ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); + + $this->response->html($this->taskLayout('task/time_tracking', array( + 'task' => $task, + 'subtask_paginator' => $subtask_paginator, + ))); + } } diff --git a/sources/app/Controller/Tasklink.php b/sources/app/Controller/Tasklink.php new file mode 100644 index 0000000..61b7fab --- /dev/null +++ b/sources/app/Controller/Tasklink.php @@ -0,0 +1,116 @@ +taskLink->getById($this->request->getIntegerParam('link_id')); + + if (! $link) { + $this->notfound(); + } + + return $link; + } + + /** + * Creation form + * + * @access public + */ + public function create(array $values = array(), array $errors = array()) + { + $task = $this->getTask(); + + if (empty($values)) { + $values = array( + 'task_id' => $task['id'], + ); + } + + $this->response->html($this->taskLayout('tasklink/create', array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'labels' => $this->link->getList(0, false), + 'title' => t('Add a new link') + ))); + } + + /** + * Validation and creation + * + * @access public + */ + public function save() + { + $task = $this->getTask(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->taskLink->validateCreation($values); + + if ($valid) { + + if ($this->taskLink->create($values['task_id'], $values['opposite_task_id'], $values['link_id'])) { + $this->session->flash(t('Link added successfully.')); + $this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links'); + } + else { + $this->session->flashError(t('Unable to create your link.')); + } + } + + $this->create($values, $errors); + } + + /** + * Confirmation dialog before removing a link + * + * @access public + */ + public function confirm() + { + $task = $this->getTask(); + $link = $this->getTaskLink(); + + $this->response->html($this->taskLayout('tasklink/remove', array( + 'link' => $link, + 'task' => $task, + ))); + } + + /** + * Remove a link + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $task = $this->getTask(); + + if ($this->taskLink->remove($this->request->getIntegerParam('link_id'))) { + $this->session->flash(t('Link removed successfully.')); + } + else { + $this->session->flashError(t('Unable to remove this link.')); + } + + $this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + } +} diff --git a/sources/app/Controller/User.php b/sources/app/Controller/User.php index 7fddf70..decdb64 100644 --- a/sources/app/Controller/User.php +++ b/sources/app/Controller/User.php @@ -56,7 +56,7 @@ class User extends Base if ($valid) { if ($redirect_query !== '') { - $this->response->redirect('?'.$redirect_query); + $this->response->redirect('?'.urldecode($redirect_query)); } else { $this->response->redirect('?controller=app'); @@ -115,31 +115,19 @@ class User extends Base */ public function index() { - $direction = $this->request->getStringParam('direction', 'ASC'); - $order = $this->request->getStringParam('order', 'username'); - $offset = $this->request->getIntegerParam('offset', 0); - $limit = 25; - - $users = $this->user->paginate($offset, $limit, $order, $direction); - $nb_users = $this->user->count(); + $paginator = $this->paginator + ->setUrl('user', 'index') + ->setMax(30) + ->setOrder('username') + ->setQuery($this->user->getQuery()) + ->calculate(); $this->response->html( $this->template->layout('user/index', array( 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), 'projects' => $this->project->getList(), - 'nb_users' => $nb_users, - 'users' => $users, - 'title' => t('Users').' ('.$nb_users.')', - 'pagination' => array( - 'controller' => 'user', - 'action' => 'index', - 'direction' => $direction, - 'order' => $order, - 'total' => $nb_users, - 'offset' => $offset, - 'limit' => $limit, - 'params' => array(), - ), + 'title' => t('Users').' ('.$paginator->getTotal().')', + 'paginator' => $paginator, ))); } @@ -201,6 +189,43 @@ class User extends Base ))); } + /** + * Display user calendar + * + * @access public + */ + public function calendar() + { + $user = $this->getUser(); + + $this->response->html($this->layout('user/calendar', array( + 'user' => $user, + ))); + } + + /** + * Display timesheet + * + * @access public + */ + public function timesheet() + { + $user = $this->getUser(); + + $subtask_paginator = $this->paginator + ->setUrl('user', 'timesheet', array('user_id' => $user['id'], 'pagination' => 'subtasks')) + ->setMax(20) + ->setOrder('start') + ->setDirection('DESC') + ->setQuery($this->subtaskTimeTracking->getUserQuery($user['id'])) + ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); + + $this->response->html($this->layout('user/timesheet', array( + 'subtask_paginator' => $subtask_paginator, + 'user' => $user, + ))); + } + /** * Display last connections * @@ -330,7 +355,7 @@ class User extends Base if ($this->request->isPost()) { - $values = $this->request->getValues(); + $values = $this->request->getValues() + array('disable_login_form' => 0); if ($this->userSession->isAdmin()) { $values += array('is_admin' => 0); @@ -462,7 +487,7 @@ class User extends Base * * @access public */ - public function gitHub() + public function github() { $code = $this->request->getStringParam('code'); @@ -506,7 +531,7 @@ class User extends Base * * @access public */ - public function unlinkGitHub() + public function unlinkGithub() { $this->checkCSRFParam(); diff --git a/sources/app/Controller/Webhook.php b/sources/app/Controller/Webhook.php index 1ae3b0a..ef79379 100644 --- a/sources/app/Controller/Webhook.php +++ b/sources/app/Controller/Webhook.php @@ -82,4 +82,22 @@ class Webhook extends Base echo $result ? 'PARSED' : 'IGNORED'; } + + /** + * Handle Bitbucket webhooks + * + * @access public + */ + public function bitbucket() + { + if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) { + $this->response->text('Not Authorized', 401); + } + + $this->bitbucketWebhook->setProjectId($this->request->getIntegerParam('project_id')); + + $result = $this->bitbucketWebhook->parsePayload(json_decode(@$_POST['payload'], true)); + + echo $result ? 'PARSED' : 'IGNORED'; + } } diff --git a/sources/app/Core/Helper.php b/sources/app/Core/Helper.php index e9fa186..01ebb08 100644 --- a/sources/app/Core/Helper.php +++ b/sources/app/Core/Helper.php @@ -3,7 +3,6 @@ namespace Core; use Pimple\Container; -use Parsedown; /** * Template helpers @@ -13,6 +12,7 @@ use Parsedown; * * @property \Core\Session $session * @property \Model\Acl $acl + * @property \Model\Config $config * @property \Model\User $user * @property \Model\UserSession $userSession */ @@ -49,6 +49,33 @@ class Helper return $this->container[$name]; } + /** + * Get the age of an item in quasi human readable format. + * It's in this format: <1h , NNh, NNd + * + * @access public + * @param integer $timestamp Unix timestamp of the artifact for which age will be calculated + * @param integer $now Compare with this timestamp (Default value is the current unix timestamp) + * @return string + */ + public function getTaskAge($timestamp, $now = null) + { + if ($now === null) { + $now = time(); + } + + $diff = $now - $timestamp; + + if ($diff < 3600) { + return t('<1h'); + } + else if ($diff < 86400) { + return t('%dh', $diff / 3600); + } + + return t('%dd', ($now - $timestamp) / 86400); + } + /** * Proxy cache helper for acl::isManagerActionAllowed() * @@ -104,9 +131,9 @@ class Helper * @param string $filename Filename * @return string */ - public function css($filename) + public function css($filename, $is_file = true) { - return ''; + return ''; } /** @@ -194,9 +221,9 @@ class Helper * @param string $class CSS class * @return string */ - public function formSelect($name, array $options, array $values = array(), array $errors = array(), $class = '') + public function formSelect($name, array $options, array $values = array(), array $errors = array(), array $attributes = array(), $class = '') { - $html = ''; foreach ($options as $id => $value) { @@ -245,7 +272,7 @@ class Helper */ public function formRadio($name, $label, $value, $selected = false, $class = '') { - return ''; + return ''; } /** @@ -417,7 +444,7 @@ class Helper } /** - * URL query string + * Generate controller/action url for templates * * u('task', 'show', array('task_id' => $task_id)) * @@ -429,83 +456,40 @@ class Helper */ public function u($controller, $action, array $params = array(), $csrf = false) { - $html = '?controller='.$controller.'&action='.$action; + $values = array( + 'controller' => $controller, + 'action' => $action, + ); if ($csrf) { $params['csrf_token'] = Security::getCSRFToken(); } - foreach ($params as $key => $value) { - $html .= '&'.$key.'='.$value; - } + $values += $params; - return $html; + return '?'.http_build_query($values, '', '&'); } /** - * Pagination links + * Generate controller/action url * - * @param array $pagination Pagination information + * l('task', 'show', array('task_id' => $task_id)) + * + * @param string $controller Controller name + * @param string $action Action name + * @param array $params Url parameters * @return string */ - public function paginate(array $pagination) + public function url($controller, $action, array $params = array()) { - extract($pagination); + $values = array( + 'controller' => $controller, + 'action' => $action, + ); - if ($pagination['offset'] === 0 && ($total - $pagination['offset']) <= $limit) { - return ''; - } + $values += $params; - $html = ''; - - return $html; - } - - /** - * Column sorting (work with pagination) - * - * @param string $label Column title - * @param string $column SQL column name - * @param array $pagination Pagination information - * @return string - */ - public function order($label, $column, array $pagination) - { - extract($pagination); - - $prefix = ''; - - if ($order === $column) { - $prefix = $direction === 'DESC' ? '▼ ' : '▲ '; - $direction = $direction === 'DESC' ? 'ASC' : 'DESC'; - } - - $order = $column; - - return $prefix.$this->a($label, $controller, $action, $params + compact('offset', 'order', 'direction')); + return '?'.http_build_query($values); } /** @@ -517,24 +501,9 @@ class Helper */ public function markdown($text, array $link = array()) { - $html = Parsedown::instance() - ->setMarkupEscaped(true) # escapes markup (HTML) - ->text($text); - - // Replace task #123 by a link to the task - if (! empty($link) && preg_match_all('!#(\d+)!i', $html, $matches, PREG_SET_ORDER)) { - - foreach ($matches as $match) { - - $html = str_replace( - $match[0], - $this->a($match[0], $link['controller'], $link['action'], $link['params'] + array('task_id' => $match[1])), - $html - ); - } - } - - return $html; + $parser = new Markdown($link, $this); + $parser->setMarkupEscaped(true); + return $parser->text($text); } /** @@ -656,4 +625,56 @@ class Helper return $default_value; } + + /** + * Get javascript language code + * + * @access public + * @return string + */ + public function jsLang() + { + return $this->config->getJsLanguageCode(); + } + + /** + * Get current timezone + * + * @access public + * @return string + */ + public function getTimezone() + { + return $this->config->getCurrentTimezone(); + } + + /** + * Get the link to toggle subtask status + * + * @access public + * @param array $subtask + * @param string $redirect + * @return string + */ + public function toggleSubtaskStatus(array $subtask, $redirect) + { + if ($subtask['status'] == 0 && isset($this->session['has_subtask_inprogress']) && $this->session['has_subtask_inprogress'] === true) { + + return $this->a( + trim($this->render('subtask/icons', array('subtask' => $subtask))) . $this->e($subtask['title']), + 'subtask', + 'subtaskRestriction', + array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'redirect' => $redirect), + false, + 'popover task-board-popover' + ); + } + + return $this->a( + trim($this->render('subtask/icons', array('subtask' => $subtask))) . $this->e($subtask['title']), + 'subtask', + 'toggleStatus', + array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'redirect' => $redirect) + ); + } } diff --git a/sources/app/Core/Markdown.php b/sources/app/Core/Markdown.php new file mode 100644 index 0000000..3dd9861 --- /dev/null +++ b/sources/app/Core/Markdown.php @@ -0,0 +1,43 @@ +link = $link; + $this->helper = $helper; + $this->InlineTypes['#'][] = 'TaskLink'; + $this->inlineMarkerList .= '#'; + } + + protected function inlineTaskLink($Excerpt) + { + // Replace task #123 by a link to the task + if (! empty($this->link) && preg_match('!#(\d+)!i', $Excerpt['text'], $matches)) { + + $url = $this->helper->u($this->link['controller'], + $this->link['action'], + $this->link['params'] + array('task_id' => $matches[1])); + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[0], + 'attributes' => array('href' => $url))); + } + } +} diff --git a/sources/app/Core/Paginator.php b/sources/app/Core/Paginator.php new file mode 100644 index 0000000..4d4364d --- /dev/null +++ b/sources/app/Core/Paginator.php @@ -0,0 +1,461 @@ +container = $container; + } + + /** + * Set a PicoDb query + * + * @access public + * @param \PicoDb\Table + * @return Paginator + */ + public function setQuery(Table $query) + { + $this->query = $query; + $this->total = $this->query->count(); + return $this; + } + + /** + * Execute a PicoDb query + * + * @access public + * @return array + */ + public function executeQuery() + { + if ($this->query !== null) { + return $this->query + ->offset($this->offset) + ->limit($this->limit) + ->orderBy($this->order, $this->direction) + ->findAll(); + } + + return array(); + } + + /** + * Set url parameters + * + * @access public + * @param string $controller + * @param string $action + * @param array $params + * @return Paginator + */ + public function setUrl($controller, $action, array $params = array()) + { + $this->controller = $controller; + $this->action = $action; + $this->params = $params; + return $this; + } + + /** + * Add manually items + * + * @access public + * @param array $items + * @return Paginator + */ + public function setCollection(array $items) + { + $this->items = $items; + return $this; + } + + /** + * Return the items + * + * @access public + * @return array + */ + public function getCollection() + { + return $this->items ?: $this->executeQuery(); + } + + /** + * Set the total number of items + * + * @access public + * @param integer $total + * @return Paginator + */ + public function setTotal($total) + { + $this->total = $total; + return $this; + } + + /** + * Get the total number of items + * + * @access public + * @return integer + */ + public function getTotal() + { + return $this->total; + } + + /** + * Set the default page number + * + * @access public + * @param integer $page + * @return Paginator + */ + public function setPage($page) + { + $this->page = $page; + return $this; + } + + /** + * Set the default column order + * + * @access public + * @param string $order + * @return Paginator + */ + public function setOrder($order) + { + $this->order = $order; + return $this; + } + + /** + * Set the default sorting direction + * + * @access public + * @param string $direction + * @return Paginator + */ + public function setDirection($direction) + { + $this->direction = $direction; + return $this; + } + + /** + * Set the maximum number of items per page + * + * @access public + * @param integer $limit + * @return Paginator + */ + public function setMax($limit) + { + $this->limit = $limit; + return $this; + } + + /** + * Return true if the collection is empty + * + * @access public + * @return boolean + */ + public function isEmpty() + { + return $this->total === 0; + } + + /** + * Execute the offset calculation only if the $condition is true + * + * @access public + * @param boolean $condition + * @return Paginator + */ + public function calculateOnlyIf($condition) + { + if ($condition) { + $this->calculate(); + } + + return $this; + } + + /** + * Calculate the offset value accoring to url params and the page number + * + * @access public + * @return Paginator + */ + public function calculate() + { + $this->page = $this->container['request']->getIntegerParam('page', 1); + $this->direction = $this->container['request']->getStringParam('direction', $this->direction); + $this->order = $this->container['request']->getStringParam('order', $this->order); + + if ($this->page < 1) { + $this->page = 1; + } + + $this->offset = ($this->page - 1) * $this->limit; + + return $this; + } + + /** + * Get url params for link generation + * + * @access public + * @param integer $page + * @param string $order + * @param string $direction + * @return string + */ + public function getUrlParams($page, $order, $direction) + { + $params = array( + 'page' => $page, + 'order' => $order, + 'direction' => $direction, + ); + + return array_merge($this->params, $params); + } + + /** + * Generate the previous link + * + * @access public + * @return string + */ + public function generatePreviousLink() + { + $html = ''; + + if ($this->offset > 0) { + $html .= $this->container['helper']->a( + '← '.t('Previous'), + $this->controller, + $this->action, + $this->getUrlParams($this->page - 1, $this->order, $this->direction) + ); + } + else { + $html .= '← '.t('Previous'); + } + + $html .= ''; + + return $html; + } + + /** + * Generate the next link + * + * @access public + * @return string + */ + public function generateNextLink() + { + $html = ''; + + if (($this->total - $this->offset) > $this->limit) { + $html .= $this->container['helper']->a( + t('Next').' →', + $this->controller, + $this->action, + $this->getUrlParams($this->page + 1, $this->order, $this->direction) + ); + } + else { + $html .= t('Next').' →'; + } + + $html .= ''; + + return $html; + } + + /** + * Return true if there is no pagination to show + * + * @access public + * @return boolean + */ + public function hasNothingtoShow() + { + return $this->offset === 0 && ($this->total - $this->offset) <= $this->limit; + } + + /** + * Generation pagination links + * + * @access public + * @return string + */ + public function toHtml() + { + $html = ''; + + if (! $this->hasNothingtoShow()) { + $html .= ''; + } + + return $html; + } + + /** + * Magic method to output pagination links + * + * @access public + * @return string + */ + public function __toString() + { + return $this->toHtml(); + } + + /** + * Column sorting + * + * @param string $label Column title + * @param string $column SQL column name + * @return string + */ + public function order($label, $column) + { + $prefix = ''; + $direction = 'ASC'; + + if ($this->order === $column) { + $prefix = $this->direction === 'DESC' ? '▼ ' : '▲ '; + $direction = $this->direction === 'DESC' ? 'ASC' : 'DESC'; + } + + return $prefix.$this->container['helper']->a( + $label, + $this->controller, + $this->action, + $this->getUrlParams($this->page, $column, $direction) + ); + } +} diff --git a/sources/app/Core/Response.php b/sources/app/Core/Response.php index 6534d64..d42a8f1 100644 --- a/sources/app/Core/Response.php +++ b/sources/app/Core/Response.php @@ -167,6 +167,23 @@ class Response exit; } + /** + * Send a css response + * + * @access public + * @param string $data Raw data + * @param integer $status_code HTTP status code + */ + public function css($data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Type: text/css; charset=utf-8'); + echo $data; + + exit; + } + /** * Send a binary response * @@ -195,24 +212,7 @@ class Response $policies['default-src'] = "'self'"; $values = ''; - foreach ($policies as $policy => $hosts) { - - if (is_array($hosts)) { - - $acl = ''; - - foreach ($hosts as &$host) { - - if ($host === '*' || $host === 'self' || strpos($host, 'http') === 0) { - $acl .= $host.' '; - } - } - } - else { - - $acl = $hosts; - } - + foreach ($policies as $policy => $acl) { $values .= $policy.' '.trim($acl).'; '; } diff --git a/sources/app/Integration/BitbucketWebhook.php b/sources/app/Integration/BitbucketWebhook.php new file mode 100644 index 0000000..ccb89e1 --- /dev/null +++ b/sources/app/Integration/BitbucketWebhook.php @@ -0,0 +1,97 @@ +project_id = $project_id; + } + + /** + * Parse events + * + * @access public + * @param array $payload Gitlab event + * @return boolean + */ + public function parsePayload(array $payload) + { + if (! empty($payload['commits'])) { + + foreach ($payload['commits'] as $commit) { + + if ($this->handleCommit($commit)) { + return true; + } + } + } + + return false; + } + + /** + * Parse commit + * + * @access public + * @param array $commit Gitlab commit + * @return boolean + */ + public function handleCommit(array $commit) + { + $task_id = $this->task->getTaskIdFromText($commit['message']); + + if (! $task_id) { + return false; + } + + $task = $this->taskFinder->getById($task_id); + + if (! $task) { + return false; + } + + if ($task['is_active'] == Task::STATUS_OPEN && $task['project_id'] == $this->project_id) { + + $this->container['dispatcher']->dispatch( + self::EVENT_COMMIT, + new TaskEvent(array('task_id' => $task_id) + $task) + ); + + return true; + } + + return false; + } +} diff --git a/sources/app/Integration/GitlabWebhook.php b/sources/app/Integration/GitlabWebhook.php index f5df32a..e920f33 100644 --- a/sources/app/Integration/GitlabWebhook.php +++ b/sources/app/Integration/GitlabWebhook.php @@ -148,10 +148,10 @@ class GitlabWebhook extends Base */ public function handleIssueEvent(array $payload) { - switch ($payload['object_attributes']['state']) { - case 'opened': + switch ($payload['object_attributes']['action']) { + case 'open': return $this->handleIssueOpened($payload['object_attributes']); - case 'closed': + case 'close': return $this->handleIssueClosed($payload['object_attributes']); } diff --git a/sources/app/Libs/password.php b/sources/app/Libs/password.php new file mode 100644 index 0000000..c6e84cb --- /dev/null +++ b/sources/app/Libs/password.php @@ -0,0 +1,227 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @copyright 2012 The Authors + */ + +if (!defined('PASSWORD_BCRYPT')) { + + define('PASSWORD_BCRYPT', 1); + define('PASSWORD_DEFAULT', PASSWORD_BCRYPT); + + if (version_compare(PHP_VERSION, '5.3.7', '<')) { + + define('PASSWORD_PREFIX', '$2a$'); + } + else { + + define('PASSWORD_PREFIX', '$2y$'); + } + + /** + * Hash the password using the specified algorithm + * + * @param string $password The password to hash + * @param int $algo The algorithm to use (Defined by PASSWORD_* constants) + * @param array $options The options for the algorithm to use + * + * @return string|false The hashed password, or false on error. + */ + function password_hash($password, $algo, array $options = array()) { + if (!function_exists('crypt')) { + trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING); + return null; + } + if (!is_string($password)) { + trigger_error("password_hash(): Password must be a string", E_USER_WARNING); + return null; + } + if (!is_int($algo)) { + trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING); + return null; + } + switch ($algo) { + case PASSWORD_BCRYPT: + // Note that this is a C constant, but not exposed to PHP, so we don't define it here. + $cost = 10; + if (isset($options['cost'])) { + $cost = $options['cost']; + if ($cost < 4 || $cost > 31) { + trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING); + return null; + } + } + $required_salt_len = 22; + $hash_format = sprintf("%s%02d$", PASSWORD_PREFIX, $cost); + break; + default: + trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING); + return null; + } + if (isset($options['salt'])) { + switch (gettype($options['salt'])) { + case 'NULL': + case 'boolean': + case 'integer': + case 'double': + case 'string': + $salt = (string) $options['salt']; + break; + case 'object': + if (method_exists($options['salt'], '__tostring')) { + $salt = (string) $options['salt']; + break; + } + case 'array': + case 'resource': + default: + trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING); + return null; + } + if (strlen($salt) < $required_salt_len) { + trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING); + return null; + } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) { + $salt = str_replace('+', '.', base64_encode($salt)); + } + } else { + $buffer = ''; + $raw_length = (int) ($required_salt_len * 3 / 4 + 1); + $buffer_valid = false; + if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) { + $buffer = mcrypt_create_iv($raw_length, MCRYPT_DEV_URANDOM); + if ($buffer) { + $buffer_valid = true; + } + } + if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) { + $buffer = openssl_random_pseudo_bytes($raw_length); + if ($buffer) { + $buffer_valid = true; + } + } + if (!$buffer_valid && is_readable('/dev/urandom')) { + $f = fopen('/dev/urandom', 'r'); + $read = strlen($buffer); + while ($read < $raw_length) { + $buffer .= fread($f, $raw_length - $read); + $read = strlen($buffer); + } + fclose($f); + if ($read >= $raw_length) { + $buffer_valid = true; + } + } + if (!$buffer_valid || strlen($buffer) < $raw_length) { + $bl = strlen($buffer); + for ($i = 0; $i < $raw_length; $i++) { + if ($i < $bl) { + $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255)); + } else { + $buffer .= chr(mt_rand(0, 255)); + } + } + } + $salt = str_replace('+', '.', base64_encode($buffer)); + + } + $salt = substr($salt, 0, $required_salt_len); + + $hash = $hash_format . $salt; + + $ret = crypt($password, $hash); + + if (!is_string($ret) || strlen($ret) <= 13) { + return false; + } + + return $ret; + } + + /** + * Get information about the password hash. Returns an array of the information + * that was used to generate the password hash. + * + * array( + * 'algo' => 1, + * 'algoName' => 'bcrypt', + * 'options' => array( + * 'cost' => 10, + * ), + * ) + * + * @param string $hash The password hash to extract info from + * + * @return array The array of information about the hash. + */ + function password_get_info($hash) { + $return = array( + 'algo' => 0, + 'algoName' => 'unknown', + 'options' => array(), + ); + if (substr($hash, 0, 4) == PASSWORD_PREFIX && strlen($hash) == 60) { + $return['algo'] = PASSWORD_BCRYPT; + $return['algoName'] = 'bcrypt'; + list($cost) = sscanf($hash, PASSWORD_PREFIX."%d$"); + $return['options']['cost'] = $cost; + } + return $return; + } + + /** + * Determine if the password hash needs to be rehashed according to the options provided + * + * If the answer is true, after validating the password using password_verify, rehash it. + * + * @param string $hash The hash to test + * @param int $algo The algorithm used for new password hashes + * @param array $options The options array passed to password_hash + * + * @return boolean True if the password needs to be rehashed. + */ + function password_needs_rehash($hash, $algo, array $options = array()) { + $info = password_get_info($hash); + if ($info['algo'] != $algo) { + return true; + } + switch ($algo) { + case PASSWORD_BCRYPT: + $cost = isset($options['cost']) ? $options['cost'] : 10; + if ($cost != $info['options']['cost']) { + return true; + } + break; + } + return false; + } + + /** + * Verify a password against a hash using a timing attack resistant approach + * + * @param string $password The password to verify + * @param string $hash The hash to verify against + * + * @return boolean If the password matches the hash + */ + function password_verify($password, $hash) { + if (!function_exists('crypt')) { + trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING); + return false; + } + $ret = crypt($password, $hash); + if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) { + return false; + } + + $status = 0; + for ($i = 0; $i < strlen($ret); $i++) { + $status |= (ord($ret[$i]) ^ ord($hash[$i])); + } + + return $status === 0; + } +} diff --git a/sources/app/Locale/da_DK/translations.php b/sources/app/Locale/da_DK/translations.php index 3287aa5..75f27a7 100644 --- a/sources/app/Locale/da_DK/translations.php +++ b/sources/app/Locale/da_DK/translations.php @@ -408,15 +408,13 @@ return array( 'Comment updated' => 'Kommentar opdateret', 'New comment posted by %s' => 'Ny kommentar af %s', 'List of due tasks for the project "%s"' => 'Udestående opgaver for projektet "%s"', - '[%s][New attachment] %s (#%d)' => '[%s][Ny vedhæftning] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][Ny kommentar] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][Kommentar opdateret] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][Ny under-opgave] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][Under-opgave opdateret] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][Ny opgave] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][Opgave opdateret] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][Opgave lukket] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][Opgave åbnet] %s (#%d)', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + // 'Task closed' => '', + // 'Task opened' => '', '[%s][Due tasks]' => 'Udestående opgaver', '[Kanboard] Notification' => '[Kanboard] Notifikation', 'I want to receive notifications only for those projects:' => 'Jeg vil kun have notifikationer for disse projekter:', @@ -500,9 +498,9 @@ return array( 'Task assignee change' => 'Opgaven ansvarlig ændring', '%s change the assignee of the task #%d to %s' => '%s skrift ansvarlig for opgaven #%d til %s', '%s changed the assignee of the task %s to %s' => '%s skift ansvarlig for opgaven %s til %s', - '[%s][Column Change] %s (#%d)' => '[%s][Kolonne Skift] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][Position Skift] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][Ansvarlig Skift] %s (#%d)', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', 'New password for the user "%s"' => 'Ny adgangskode for brugeren', 'Choose an event' => 'Vælg et event', 'Github commit received' => 'Github commit modtaget', @@ -647,5 +645,93 @@ return array( // 'Application default' => '', // 'Language:' => '', // 'Timezone:' => '', + // 'All columns' => '', + // 'Calendar for "%s"' => '', + // 'Filter by column' => '', + // 'Filter by status' => '', + // 'Calendar' => '', // 'Next' => '', + // '#%d' => '', + // 'Filter by color' => '', + // 'Filter by swimlane' => '', + // 'All swimlanes' => '', + // 'All colors' => '', + // 'All status' => '', + // 'Add a comment logging moving the task between columns' => '', + // 'Moved to column %s' => '', + // 'Change description' => '', + // 'User dashboard' => '', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Enable time tracking for subtasks' => '', + // 'Select the new status of the subtask: "%s"' => '', + // 'Subtask timesheet' => '', + // 'There is nothing to show.' => '', + // 'Time Tracking' => '', + // 'You already have one subtask in progress' => '', + // 'Which parts of the project do you want to duplicate?' => '', + // 'Change dashboard view' => '', + // 'Show/hide activities' => '', + // 'Show/hide projects' => '', + // 'Show/hide subtasks' => '', + // 'Show/hide tasks' => '', + // 'Disable login form' => '', + // 'Show/hide calendar' => '', + // 'User calendar' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + // 'Task age in days' => '', + // 'Days in this column' => '', + // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + // 'Expand tasks' => '', + // 'Collapse tasks' => '', + // 'Expand/collapse tasks' => '', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', ); diff --git a/sources/app/Locale/de_DE/translations.php b/sources/app/Locale/de_DE/translations.php index c85c97c..54503b6 100644 --- a/sources/app/Locale/de_DE/translations.php +++ b/sources/app/Locale/de_DE/translations.php @@ -408,15 +408,13 @@ return array( 'Comment updated' => 'Kommentar wurde aktualisiert', 'New comment posted by %s' => 'Neuer Kommentar verfasst durch %s', 'List of due tasks for the project "%s"' => 'Liste der fälligen Aufgaben für das Projekt "%s"', - '[%s][New attachment] %s (#%d)' => '[%s][Neuer Anhang] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][Neuer Kommentar] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][Kommentar aktualisisiert] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][Neue Teilaufgabe] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][Teilaufgabe aktualisisert] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][Neue Aufgabe] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][Aufgabe aktualisiert] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][Aufgabe geschlossen] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][Aufgabe geöffnet] %s (#%d)', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + // 'Task closed' => '', + // 'Task opened' => '', '[%s][Due tasks]' => '[%s][Fällige Aufgaben]', '[Kanboard] Notification' => '[Kanboard] Benachrichtigung', 'I want to receive notifications only for those projects:' => 'Ich möchte nur für diese Projekte Benachrichtigungen erhalten:', @@ -500,9 +498,9 @@ return array( 'Task assignee change' => 'Zuständigkeit geändert', '%s change the assignee of the task #%d to %s' => '%s hat die Zusständigkeit der Aufgabe #%d geändert um %s', '%s changed the assignee of the task %s to %s' => '%s hat die Zuständigkeit der Aufgabe %s geändert um %s', - '[%s][Column Change] %s (#%d)' => '[%s][Spaltenänderung] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][Positionsänderung] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][Zuständigkeitsänderung] %s (#%d)', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', 'New password for the user "%s"' => 'Neues Passwort des Benutzers "%s"', 'Choose an event' => 'Aktion wählen', 'Github commit received' => 'Github commit empfangen', @@ -647,5 +645,93 @@ return array( 'Application default' => 'Anwendungsstandard', 'Language:' => 'Sprache:', 'Timezone:' => 'Zeitzone:', - // 'Next' => '', + 'All columns' => 'Alle Spalten', + 'Calendar for "%s"' => 'Kalender für "%s"', + 'Filter by column' => 'Spalte filtern', + 'Filter by status' => 'Status filtern', + 'Calendar' => 'Kalender', + 'Next' => 'Nächste', + // '#%d' => '', + 'Filter by color' => 'Farbe filtern', + 'Filter by swimlane' => 'Swimlane filtern', + 'All swimlanes' => 'Alle Swimlanes', + 'All colors' => 'Alle Farben', + 'All status' => 'Alle Status', + 'Add a comment logging moving the task between columns' => 'Kommentar hinzufügen wenn die Aufgabe verschoben wird', + 'Moved to column %s' => 'In Spalte %s verschoben', + 'Change description' => 'Beschreibung ändern', + 'User dashboard' => 'Benutzer Dashboard', + 'Allow only one subtask in progress at the same time for a user' => 'Erlaube nur eine Teilaufgabe pro Benutzer zu bearbeiten', + 'Edit column "%s"' => 'Spalte "%s" bearbeiten', + 'Enable time tracking for subtasks' => 'Aktiviere Zeiterfassung für Teilaufgaben', + 'Select the new status of the subtask: "%s"' => 'Wähle einen neuen Status für Teilaufgabe: "%s"', + 'Subtask timesheet' => 'Teilaufgaben Zeiterfassung', + 'There is nothing to show.' => 'Es ist nichts zum Anzeigen vorhanden.', + 'Time Tracking' => 'Zeiterfassung', + 'You already have one subtask in progress' => 'Bereits eine Teilaufgabe in bearbeitung', + 'Which parts of the project do you want to duplicate?' => 'Welcher Teil des Projekts soll kopiert werden?', + 'Change dashboard view' => 'Dashboardansicht ändern', + 'Show/hide activities' => 'Aktivitäten anzeigen/verbergen', + 'Show/hide projects' => 'Projekte anzeigen/verbergen', + 'Show/hide subtasks' => 'Teilaufgaben anzeigen/verbergen', + 'Show/hide tasks' => 'Aufgaben anzeigen/verbergen', + 'Disable login form' => 'Anmeldeformular deaktivieren', + 'Show/hide calendar' => 'Kalender anzeigen/verbergen', + 'User calendar' => 'Benutzer Kalender', + 'Bitbucket commit received' => 'Bitbucket commit erhalten', + 'Bitbucket webhooks' => 'Bitbucket webhooks', + 'Help on Bitbucket webhooks' => 'Hilfe für Bitbucket webhooks', + 'Start' => 'Start', + 'End' => 'Ende', + 'Task age in days' => 'Aufgabenalter in Tagen', + 'Days in this column' => 'Tage in dieser Spalte', + '%dd' => '%dT', + 'Add a link' => 'Verbindung hinzufügen', + 'Add a new link' => 'Neue Verbindung hinzufügen', + 'Do you really want to remove this link: "%s"?' => 'Die Verbindung "%s" wirklich löschen?', + 'Do you really want to remove this link with task #%d?' => 'Die Verbindung mit der Aufgabe #%d wirklich löschen?', + 'Field required' => 'Feld erforderlich', + 'Link added successfully.' => 'Verbindung erfolgreich hinzugefügt.', + 'Link updated successfully.' => 'Verbindung erfolgreich aktualisiert.', + 'Link removed successfully.' => 'Verbindung erfolgreich gelöscht.', + 'Link labels' => 'Verbindungsbeschriftung', + 'Link modification' => 'Verbindung ändern', + 'Links' => 'Verbindungen', + 'Link settings' => 'Verbindungseinstellungen', + 'Opposite label' => 'Gegenteil', + 'Remove a link' => 'Verbindung entfernen', + 'Task\'s links' => 'Aufgaben Verbindungen', + 'The labels must be different' => 'Die Beschriftung muss unterschiedlich sein', + 'There is no link.' => 'Es gibt keine Verbindung', + 'This label must be unique' => 'Die Beschriftung muss einzigartig sein', + 'Unable to create your link.' => 'Verbindung kann nicht erstellt werden.', + 'Unable to update your link.' => 'Verbindung kann nicht aktualisiert werden.', + 'Unable to remove this link.' => 'Verbindung kann nicht entfernt werden', + 'relates to' => 'gehört zu', + 'blocks' => 'blockiert', + 'is blocked by' => 'ist blockiert von', + 'duplicates' => 'doppelt', + 'is duplicated by' => 'ist gedoppelt von', + 'is a child of' => 'ist untergeordnet', + 'is a parent of' => 'ist übergeordnet', + 'targets milestone' => 'betrifft Meilenstein', + 'is a milestone of' => 'ist ein Meilenstein von', + 'fixes' => 'behebt', + 'is fixed by' => 'wird behoben von', + 'This task' => 'Diese Aufgabe', + '<1h' => '<1Std', + '%dh' => '%dStd', + // '%b %e' => '', + 'Expand tasks' => 'Aufgaben aufklappen', + 'Collapse tasks' => 'Aufgaben zusammenklappen', + 'Expand/collapse tasks' => 'Aufgaben auf/zuklappen', + 'Close dialog box' => 'Dialog schließen', + 'Submit a form' => 'Formular abschicken', + 'Board view' => 'Pinnwand Ansicht', + 'Keyboard shortcuts' => 'Tastaturkürzel', + 'Open board switcher' => 'Pinnwandauswahl öffnen', + 'Application' => 'Anwendung', + 'Filter recently updated' => 'Zuletzt geänderte anzeigen', + 'since %B %e, %Y at %k:%M %p' => 'seit %B %e, %Y um %k:%M %p', + 'More filters' => 'Mehr Filter', ); diff --git a/sources/app/Locale/es_ES/translations.php b/sources/app/Locale/es_ES/translations.php index 5dafde9..3981f12 100644 --- a/sources/app/Locale/es_ES/translations.php +++ b/sources/app/Locale/es_ES/translations.php @@ -408,15 +408,13 @@ return array( 'Comment updated' => 'Comentario actualizado', 'New comment posted by %s' => 'Nuevo comentario agregado por %s', 'List of due tasks for the project "%s"' => 'Lista de tareas para el proyecto "%s"', - '[%s][New attachment] %s (#%d)' => '[%s][uevo adjunto] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][Nuevo comentario] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][Comentario actualizado] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][Nueva subtarea] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][Subtarea actualizada] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][Nueva tarea] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][Tarea actualizada] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][Tarea cerrada] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][Tarea abierta] %s (#%d)', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + // 'Task closed' => '', + // 'Task opened' => '', '[%s][Due tasks]' => '[%s][Tareas vencidas]', '[Kanboard] Notification' => '[Kanboard] Notificación', 'I want to receive notifications only for those projects:' => 'Quiero recibir notificaciones sólo de estos proyectos:', @@ -500,131 +498,131 @@ return array( 'Task assignee change' => 'Cambiar persona asignada a la tarea', // '%s change the assignee of the task #%d to %s' => '', // '%s changed the assignee of the task %s to %s' => '', - '[%s][Column Change] %s (#%d)' => '[%s][Cambia Columna] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][Cambia Posición] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][Cambia Persona Asignada] %s (#%d)', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', 'New password for the user "%s"' => 'Nueva contraseña para el usuario "%s"', - // 'Choose an event' => '', - // 'Github commit received' => '', - // 'Github issue opened' => '', - // 'Github issue closed' => '', - // 'Github issue reopened' => '', - // 'Github issue assignee change' => '', - // 'Github issue label change' => '', - // 'Create a task from an external provider' => '', - // 'Change the assignee based on an external username' => '', - // 'Change the category based on an external label' => '', - // 'Reference' => '', - // 'Reference: %s' => '', - // 'Label' => '', - // 'Database' => '', - // 'About' => '', - // 'Database driver:' => '', - // 'Board settings' => '', - // 'URL and token' => '', - // 'Webhook settings' => '', - // 'URL for task creation:' => '', - // 'Reset token' => '', - // 'API endpoint:' => '', - // 'Refresh interval for private board' => '', - // 'Refresh interval for public board' => '', - // 'Task highlight period' => '', - // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '', - // 'Frequency in second (60 seconds by default)' => '', - // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '', - // 'Application URL' => '', - // 'Example: http://example.kanboard.net/ (used by email notifications)' => '', - // 'Token regenerated.' => '', - // 'Date format' => '', - // 'ISO format is always accepted, example: "%s" and "%s"' => '', - // 'New private project' => '', - // 'This project is private' => '', - // 'Type here to create a new sub-task' => '', - // 'Add' => '', - // 'Estimated time: %s hours' => '', - // 'Time spent: %s hours' => '', - // 'Started on %B %e, %Y' => '', - // 'Start date' => '', - // 'Time estimated' => '', - // 'There is nothing assigned to you.' => '', - // 'My tasks' => '', - // 'Activity stream' => '', - // 'Dashboard' => '', - // 'Confirmation' => '', - // 'Allow everybody to access to this project' => '', - // 'Everybody have access to this project.' => '', - // 'Webhooks' => '', - // 'API' => '', - // 'Integration' => '', - // 'Github webhooks' => '', - // 'Help on Github webhooks' => '', - // 'Create a comment from an external provider' => '', - // 'Github issue comment created' => '', - // 'Configure' => '', - // 'Project management' => '', - // 'My projects' => '', - // 'Columns' => '', - // 'Task' => '', - // 'Your are not member of any project.' => '', - // 'Percentage' => '', - // 'Number of tasks' => '', - // 'Task distribution' => '', - // 'Reportings' => '', - // 'Task repartition for "%s"' => '', - // 'Analytics' => '', - // 'Subtask' => '', - // 'My subtasks' => '', - // 'User repartition' => '', - // 'User repartition for "%s"' => '', - // 'Clone this project' => '', - // 'Column removed successfully.' => '', - // 'Edit Project' => '', - // 'Github Issue' => '', - // 'Not enough data to show the graph.' => '', - // 'Previous' => '', - // 'The id must be an integer' => '', - // 'The project id must be an integer' => '', - // 'The status must be an integer' => '', - // 'The subtask id is required' => '', - // 'The subtask id must be an integer' => '', - // 'The task id is required' => '', - // 'The task id must be an integer' => '', - // 'The user id must be an integer' => '', - // 'This value is required' => '', - // 'This value must be numeric' => '', - // 'Unable to create this task.' => '', - // 'Cumulative flow diagram' => '', - // 'Cumulative flow diagram for "%s"' => '', - // 'Daily project summary' => '', - // 'Daily project summary export' => '', - // 'Daily project summary export for "%s"' => '', - // 'Exports' => '', - // 'This export contains the number of tasks per column grouped per day.' => '', - // 'Nothing to preview...' => '', - // 'Preview' => '', - // 'Write' => '', - // 'Active swimlanes' => '', - // 'Add a new swimlane' => '', - // 'Change default swimlane' => '', - // 'Default swimlane' => '', - // 'Do you really want to remove this swimlane: "%s"?' => '', - // 'Inactive swimlanes' => '', - // 'Set project manager' => '', - // 'Set project member' => '', - // 'Remove a swimlane' => '', - // 'Rename' => '', - // 'Show default swimlane' => '', - // 'Swimlane modification for the project "%s"' => '', - // 'Swimlane not found.' => '', - // 'Swimlane removed successfully.' => '', - // 'Swimlanes' => '', - // 'Swimlane updated successfully.' => '', - // 'The default swimlane have been updated successfully.' => '', - // 'Unable to create your swimlane.' => '', - // 'Unable to remove this swimlane.' => '', - // 'Unable to update this swimlane.' => '', - // 'Your swimlane have been created successfully.' => '', - // 'Example: "Bug, Feature Request, Improvement"' => '', + 'Choose an event' => 'Escoga un evento', + 'Github commit received' => 'Envío a Github recibido', + 'Github issue opened' => 'Problema en Github abierto', + 'Github issue closed' => 'Problema en Github cerrado', + 'Github issue reopened' => 'Problema en Github reabierto', + 'Github issue assignee change' => 'Cambio en signación de problema en Github', + 'Github issue label change' => 'Cambio en etiqueta del problema', + 'Create a task from an external provider' => 'Crear una tarea a partir de un proveedor externo', + 'Change the assignee based on an external username' => 'Cambiar la asignación basado en un nombre de usuario externo', + 'Change the category based on an external label' => 'Cambiar la categoría basado en una etiqueta externa', + 'Reference' => 'Referencia', + 'Reference: %s' => 'Referencia: %s', + 'Label' => 'Etiqueta', + 'Database' => 'Base de Datos', + 'About' => 'Acerca de', + 'Database driver:' => 'Driver de la base de datos', + 'Board settings' => 'Configuraciones del Tablero', + 'URL and token' => 'URL y token', + 'Webhook settings' => 'Configuraciones del Webhook', + 'URL for task creation:' => 'URL para la creación de tareas', + 'Reset token' => 'Resetear token', + 'API endpoint:' => 'Punto final del API', + 'Refresh interval for private board' => 'Intervalo de refrescamiento del tablero privado', + 'Refresh interval for public board' => 'Intervalo de refrescamiento del tablero público', + 'Task highlight period' => 'Periodo del realce de la tarea', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Periodo (en segundos) para considerar que una tarea fué modificada recientemente (0 para deshabilitar, 2 días por defecto)', + 'Frequency in second (60 seconds by default)' => 'Frecuencia en segundos (60 segundos por defecto)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frecuencia en segundos (0 para deshabilitar esta característica, 10 segundos por defecto)', + 'Application URL' => 'URL de la aplicación', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Ejemplo: http://ejemplo.kanboard.net/ (usado por las notificaciones de correo)', + 'Token regenerated.' => 'Token regenerado', + 'Date format' => 'Formato de la fecha', + 'ISO format is always accepted, example: "%s" and "%s"' => 'El formato ISO siempre es aceptado, ejemplo: "%s" y "%s"', + 'New private project' => 'Nuevo proyecto privado', + 'This project is private' => 'Este proyecto es privado', + 'Type here to create a new sub-task' => 'Escriba aquí para crear una nueva sub-tarea', + 'Add' => 'Añadir', + 'Estimated time: %s hours' => 'Tiempo estimado: % horas', + 'Time spent: %s hours' => 'Tiempo invertido: %s horas', + 'Started on %B %e, %Y' => 'Iniciado en %B %e, %Y', + 'Start date' => 'Fecha de inicio', + 'Time estimated' => 'Tiempo estimado', + 'There is nothing assigned to you.' => 'Esto no le está asignado', + 'My tasks' => 'Mis tareas', + 'Activity stream' => 'Flujo de actividad', + 'Dashboard' => 'Tablero', + 'Confirmation' => 'Confirmación', + 'Allow everybody to access to this project' => 'Permitir a cualquier acceder a este proyecto', + 'Everybody have access to this project.' => 'Cualquier tiene acceso a este proyecto', + 'Webhooks' => 'Webhooks', + 'API' => 'API', + 'Integration' => 'Integración', + 'Github webhooks' => 'Webhooks de Github', + 'Help on Github webhooks' => 'Ayuda con los Webhook de Github', + 'Create a comment from an external provider' => 'Crear un comentario a partir de un proveedor externo', + 'Github issue comment created' => 'Creado el comentario del problema en Github', + 'Configure' => 'Configurar', + 'Project management' => 'Administración del proyecto', + 'My projects' => 'Mis proyectos', + 'Columns' => 'Columnas', + 'Task' => 'Tarea', + 'Your are not member of any project.' => 'No es miembro de ningún proyecto', + 'Percentage' => 'Porcentaje', + 'Number of tasks' => 'Número de tareas', + 'Task distribution' => 'Distribución de tareas', + 'Reportings' => 'Reportes', + 'Task repartition for "%s"' => 'Repartición de tareas para "%s"', + 'Analytics' => 'Analítica', + 'Subtask' => 'Subtarea', + 'My subtasks' => 'Mis subtareas', + 'User repartition' => 'Repartición de usuarios', + 'User repartition for "%s"' => 'Repartición para "%s"', + 'Clone this project' => 'Clonar este proyecto', + 'Column removed successfully.' => 'Columna removida correctamente', + 'Edit Project' => 'Editar Proyecto', + 'Github Issue' => 'Problema Github', + 'Not enough data to show the graph.' => 'No hay suficiente información para mostrar el gráfico', + 'Previous' => 'Anterior', + 'The id must be an integer' => 'El id debe ser un entero', + 'The project id must be an integer' => 'El id del proyecto debe ser un entero', + 'The status must be an integer' => 'El estado debe ser un entero', + 'The subtask id is required' => 'El id de la subtarea es requerido', + 'The subtask id must be an integer' => 'El id de la subtarea debe ser un entero', + 'The task id is required' => 'El id de la tarea es requerido', + 'The task id must be an integer' => 'El id de la tarea debe ser un entero', + 'The user id must be an integer' => 'El id del usuario debe ser un entero', + 'This value is required' => 'El valor es requerido', + 'This value must be numeric' => 'Este valor debe ser numérico', + 'Unable to create this task.' => 'Imposible crear esta tarea', + 'Cumulative flow diagram' => 'Diagrama de flujo acumulativo', + 'Cumulative flow diagram for "%s"' => 'Diagrama de flujo acumulativo para "%s"', + 'Daily project summary' => 'Sumario diario del proyecto', + 'Daily project summary export' => 'Exportar sumario diario del proyecto', + 'Daily project summary export for "%s"' => 'Exportar sumario diario del proyecto para "%s"', + 'Exports' => 'Exportar', + 'This export contains the number of tasks per column grouped per day.' => 'Esta exportación contiene el número de tereas por columna agrupada por día', + 'Nothing to preview...' => 'Nada que previsualizar...', + 'Preview' => 'Previsualizar', + 'Write' => 'Escribir', + 'Active swimlanes' => 'Carriles activos', + 'Add a new swimlane' => 'Añadir nuevo carril', + 'Change default swimlane' => 'Cambiar el carril por defecto', + 'Default swimlane' => 'Carril por defecto', + 'Do you really want to remove this swimlane: "%s"?' => '¿Realmente quiere remover este carril: "%s"?', + 'Inactive swimlanes' => 'Carriles inactivos', + 'Set project manager' => 'Asignar administrador del proyecto', + 'Set project member' => 'Asignar miembro del proyecto', + 'Remove a swimlane' => 'Remover un carril', + 'Rename' => 'Renombrar', + 'Show default swimlane' => 'Mostrar carril por defecto', + 'Swimlane modification for the project "%s"' => '', + 'Swimlane not found.' => 'Carril no encontrado', + 'Swimlane removed successfully.' => 'Carril removido correctamente', + 'Swimlanes' => 'Carriles', + 'Swimlane updated successfully.' => 'Carril actualizado correctamente', + 'The default swimlane have been updated successfully.' => 'El carril por defecto ha sido actualizado correctamente', + 'Unable to create your swimlane.' => 'Imposible crear su carril', + 'Unable to remove this swimlane.' => 'Imposible remover este carril', + 'Unable to update this swimlane.' => 'Imposible actualizar este carril', + 'Your swimlane have been created successfully.' => 'Su carril ha sido creado correctamente', + 'Example: "Bug, Feature Request, Improvement"' => 'Ejemplo: "Error, Solicitud de característica, Mejora', // 'Default categories for new projects (Comma-separated)' => '', // 'Gitlab commit received' => '', // 'Gitlab issue opened' => '', @@ -642,10 +640,98 @@ return array( // 'Subtasks' => '', // 'Subtasks Export' => '', // 'Subtasks exportation for "%s"' => '', - // 'Task Title' => '', - // 'Untitled' => '', - // 'Application default' => '', - // 'Language:' => '', - // 'Timezone:' => '', - // 'Next' => '', + 'Task Title' => 'Título de la tarea', + 'Untitled' => 'Sin título', + 'Application default' => 'Predefinido de la aplicación', + 'Language:' => 'Idioma', + 'Timezone:' => 'Zona horaria', + 'All columns' => 'Todas las columnas', + 'Calendar for "%s"' => 'Calendario para "%s"', + 'Filter by column' => 'Filtrar por columna', + 'Filter by status' => 'Filtrar por estado', + 'Calendar' => 'Calendario', + 'Next' => 'Siguiente', + '#%d' => '', + 'Filter by color' => 'Filtrar por color', + 'Filter by swimlane' => 'Filtrar por carril', + 'All swimlanes' => 'Todos los carriles', + 'All colors' => 'Todos los colores', + 'All status' => 'Todos los estados', + // 'Add a comment logging moving the task between columns' => '', + 'Moved to column %s' => 'Movido a columna %s', + // 'Change description' => '', + // 'User dashboard' => '', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Enable time tracking for subtasks' => '', + // 'Select the new status of the subtask: "%s"' => '', + // 'Subtask timesheet' => '', + // 'There is nothing to show.' => '', + // 'Time Tracking' => '', + // 'You already have one subtask in progress' => '', + // 'Which parts of the project do you want to duplicate?' => '', + // 'Change dashboard view' => '', + // 'Show/hide activities' => '', + // 'Show/hide projects' => '', + // 'Show/hide subtasks' => '', + // 'Show/hide tasks' => '', + // 'Disable login form' => '', + // 'Show/hide calendar' => '', + // 'User calendar' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + // 'Task age in days' => '', + // 'Days in this column' => '', + // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + // 'Expand tasks' => '', + // 'Collapse tasks' => '', + // 'Expand/collapse tasks' => '', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', ); diff --git a/sources/app/Locale/fi_FI/translations.php b/sources/app/Locale/fi_FI/translations.php index c3f1fbd..06b2195 100644 --- a/sources/app/Locale/fi_FI/translations.php +++ b/sources/app/Locale/fi_FI/translations.php @@ -408,15 +408,13 @@ return array( 'Comment updated' => 'Kommentti päivitetty', 'New comment posted by %s' => '%s lisäsi uuden kommentin', // 'List of due tasks for the project "%s"' => '', - // '[%s][New attachment] %s (#%d)' => '', - // '[%s][New comment] %s (#%d)' => '', - // '[%s][Comment updated] %s (#%d)' => '', - // '[%s][New subtask] %s (#%d)' => '', - // '[%s][Subtask updated] %s (#%d)' => '', - // '[%s][New task] %s (#%d)' => '', - // '[%s][Task updated] %s (#%d)' => '', - // '[%s][Task closed] %s (#%d)' => '', - // '[%s][Task opened] %s (#%d)' => '', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + // 'Task closed' => '', + // 'Task opened' => '', // '[%s][Due tasks]' => '', // '[Kanboard] Notification' => '', 'I want to receive notifications only for those projects:' => 'Haluan vastaanottaa ilmoituksia ainoastaan näistä projekteista:', @@ -500,9 +498,9 @@ return array( 'Task assignee change' => 'Tehtävän saajan vaihto', '%s change the assignee of the task #%d to %s' => '%s vaihtoi tehtävän #%d saajaksi %s', '%s changed the assignee of the task %s to %s' => '%s vaihtoi tehtävän %s saajaksi %s', - // '[%s][Column Change] %s (#%d)' => '', - // '[%s][Position Change] %s (#%d)' => '', - // '[%s][Assignee Change] %s (#%d)' => '', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', 'New password for the user "%s"' => 'Uusi salasana käyttäjälle "%s"', 'Choose an event' => 'Valitse toiminta', 'Github commit received' => 'Github-kommitti vastaanotettu', @@ -647,5 +645,93 @@ return array( // 'Application default' => '', // 'Language:' => '', // 'Timezone:' => '', + // 'All columns' => '', + // 'Calendar for "%s"' => '', + // 'Filter by column' => '', + // 'Filter by status' => '', + // 'Calendar' => '', // 'Next' => '', + // '#%d' => '', + // 'Filter by color' => '', + // 'Filter by swimlane' => '', + // 'All swimlanes' => '', + // 'All colors' => '', + // 'All status' => '', + // 'Add a comment logging moving the task between columns' => '', + // 'Moved to column %s' => '', + // 'Change description' => '', + // 'User dashboard' => '', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Enable time tracking for subtasks' => '', + // 'Select the new status of the subtask: "%s"' => '', + // 'Subtask timesheet' => '', + // 'There is nothing to show.' => '', + // 'Time Tracking' => '', + // 'You already have one subtask in progress' => '', + // 'Which parts of the project do you want to duplicate?' => '', + // 'Change dashboard view' => '', + // 'Show/hide activities' => '', + // 'Show/hide projects' => '', + // 'Show/hide subtasks' => '', + // 'Show/hide tasks' => '', + // 'Disable login form' => '', + // 'Show/hide calendar' => '', + // 'User calendar' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + // 'Task age in days' => '', + // 'Days in this column' => '', + // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + // 'Expand tasks' => '', + // 'Collapse tasks' => '', + // 'Expand/collapse tasks' => '', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', ); diff --git a/sources/app/Locale/fr_FR/translations.php b/sources/app/Locale/fr_FR/translations.php index d067304..b05f707 100644 --- a/sources/app/Locale/fr_FR/translations.php +++ b/sources/app/Locale/fr_FR/translations.php @@ -278,7 +278,7 @@ return array( 'Remember Me' => 'Connexion automatique', 'Creation date' => 'Date de création', 'Filter by user' => 'Filtrer par utilisateur', - 'Filter by due date' => 'Filtrer par date d\'échéance', + 'Filter by due date' => 'Avec une date d\'échéance', 'Everybody' => 'Tout le monde', 'Open' => 'Ouvert', 'Closed' => 'Fermé', @@ -341,7 +341,7 @@ return array( 'Add a comment' => 'Ajouter un commentaire', 'Edit a comment' => 'Modifier un commentaire', 'Summary' => 'Résumé', - 'Time tracking' => 'Gestion du temps', + 'Time tracking' => 'Suivi du temps', 'Estimate:' => 'Estimation :', 'Spent:' => 'Passé :', 'Do you really want to remove this sub-task?' => 'Voulez-vous vraiment supprimer cette sous-tâche ?', @@ -408,15 +408,15 @@ return array( 'Comment updated' => 'Commentaire ajouté', 'New comment posted by %s' => 'Nouveau commentaire ajouté par « %s »', 'List of due tasks for the project "%s"' => 'Liste des tâches expirées pour le projet « %s »', - '[%s][New attachment] %s (#%d)' => '[%s][Pièce-jointe] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][Nouveau commentaire] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][Commentaire mis à jour] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][Nouvelle sous-tâche] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][Sous-tâche mise à jour] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][Nouvelle tâche] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][Tâche mise à jour] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][Tâche fermée] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][Tâche ouverte] %s (#%d)', + 'New attachment' => 'Nouveau document', + 'New comment' => 'Nouveau commentaire', + 'Comment updated' => 'Commentaire mis à jour', + 'New subtask' => 'Nouvelle sous-tâche', + 'Subtask updated' => 'Sous-tâche mise à jour', + 'New task' => 'Nouvelle tâche', + 'Task updated' => 'Tâche mise à jour', + 'Task closed' => 'Tâche fermée', + 'Task opened' => 'Tâche ouverte', '[%s][Due tasks]' => '[%s][Tâches expirées]', '[Kanboard] Notification' => '[Kanboard] Notification', 'I want to receive notifications only for those projects:' => 'Je souhaite reçevoir les notifications uniquement pour les projets sélectionnés :', @@ -500,9 +500,9 @@ return array( 'Task assignee change' => 'Modification de la personne assignée sur une tâche', '%s change the assignee of the task #%d to %s' => '%s a changé la personne assignée sur la tâche n˚%d pour %s', '%s changed the assignee of the task %s to %s' => '%s a changé la personne assignée sur la tâche %s pour %s', - '[%s][Column Change] %s (#%d)' => '[%s][Changement de colonne] %s (n˚%d)', - '[%s][Position Change] %s (#%d)' => '[%s][Changement de position] %s (n˚%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][Changement d\'assigné] %s (n˚%d)', + 'Column Change' => 'Changement de colonne', + 'Position Change' => 'Changement de position', + 'Assignee Change' => 'Changement d\'assigné', 'New password for the user "%s"' => 'Nouveau mot de passe pour l\'utilisateur « %s »', 'Choose an event' => 'Choisir un événement', 'Github commit received' => '« Commit » reçu via Github', @@ -647,6 +647,93 @@ return array( 'Application default' => 'Valeur par défaut de l\'application', 'Language:' => 'Langue :', 'Timezone:' => 'Fuseau horaire :', + 'All columns' => 'Toutes les colonnes', + 'Calendar for "%s"' => 'Agenda pour le projet « %s »', + 'Filter by column' => 'Filtrer par colonne', + 'Filter by status' => 'Filtrer par status', + 'Calendar' => 'Agenda', 'Next' => 'Suivant', '#%d' => 'n˚%d', + 'Filter by color' => 'Filtrer par couleur', + 'Filter by swimlane' => 'Filtrer par swimlanes', + 'All swimlanes' => 'Toutes les swimlanes', + 'All colors' => 'Toutes les couleurs', + 'All status' => 'Tous les états', + 'Add a comment logging moving the task between columns' => 'Ajouter un commentaire de log lorsqu\'une tâche est déplacée dans une autre colonne', + 'Moved to column %s' => 'Tâche déplacée à la colonne %s', + 'Change description' => 'Changer la description', + 'User dashboard' => 'Tableau de bord de l\'utilisateur', + 'Allow only one subtask in progress at the same time for a user' => 'Autoriser une seule sous-tâche en progrès en même temps pour un utilisateur', + 'Edit column "%s"' => 'Modifier la colonne « %s »', + 'Enable time tracking for subtasks' => 'Activer la feuille de temps pour les sous-tâches', + 'Select the new status of the subtask: "%s"' => 'Selectionnez le nouveau statut de la sous-tâche : « %s »', + 'Subtask timesheet' => 'Feuille de temps des sous-tâches', + 'There is nothing to show.' => 'Il n\'y a rien à montrer.', + 'Time Tracking' => 'Feuille de temps', + 'You already have one subtask in progress' => 'Vous avez déjà une sous-tâche en progrès', + 'Which parts of the project do you want to duplicate?' => 'Quelles parties du projet voulez-vous dupliquer ?', + 'Change dashboard view' => 'Changer la vue du tableau de bord', + 'Show/hide activities' => 'Afficher/cacher les activités', + 'Show/hide projects' => 'Afficher/cacher les projets', + 'Show/hide subtasks' => 'Afficher/cacher les sous-tâches', + 'Show/hide tasks' => 'Afficher/cacher les tâches', + 'Disable login form' => 'Désactiver le formulaire d\'authentification', + 'Show/hide calendar' => 'Afficher/cacher le calendrier', + 'User calendar' => 'Calendrier de l\'utilisateur', + 'Bitbucket commit received' => '« Commit » reçu via Bitbucket', + 'Bitbucket webhooks' => 'Webhook Bitbucket', + 'Help on Bitbucket webhooks' => 'Aide sur les webhooks Bitbucket', + 'Start' => 'Début', + 'End' => 'Fin', + 'Task age in days' => 'Age de la tâche en jours', + 'Days in this column' => 'Jours dans cette colonne', + '%dd' => '%dj', + 'Add a link' => 'Ajouter un lien', + 'Add a new link' => 'Ajouter un nouveau lien', + 'Do you really want to remove this link: "%s"?' => 'Voulez-vous vraiment supprimer ce lien : « %s » ?', + 'Do you really want to remove this link with task #%d?' => 'Voulez-vous vraiment supprimer ce lien avec la tâche n°%d ?', + 'Field required' => 'Champ obligatoire', + 'Link added successfully.' => 'Lien créé avec succès.', + 'Link updated successfully.' => 'Lien mis à jour avec succès.', + 'Link removed successfully.' => 'Lien supprimé avec succès.', + 'Link labels' => 'Libellé des liens', + 'Link modification' => 'Modification d\'un lien', + 'Links' => 'Liens', + 'Link settings' => 'Paramètres des liens', + 'Opposite label' => 'Nom du libellé opposé', + 'Remove a link' => 'Supprimer un lien', + 'Task\'s links' => 'Liens des tâches', + 'The labels must be different' => 'Les libellés doivent être différents', + 'There is no link.' => 'Il n\'y a aucun lien.', + 'This label must be unique' => 'Ce libellé doit être unique', + 'Unable to create your link.' => 'Impossible d\'ajouter ce lien.', + 'Unable to update your link.' => 'Impossible de mettre à jour ce lien.', + 'Unable to remove this link.' => 'Impossible de supprimer ce lien.', + 'relates to' => 'est liée à', + 'blocks' => 'bloque', + 'is blocked by' => 'est bloquée par', + 'duplicates' => 'duplique', + 'is duplicated by' => 'est dupliquée par', + 'is a child of' => 'est un enfant de', + 'is a parent of' => 'est un parent de', + 'targets milestone' => 'vise l\'étape importante', + 'is a milestone of' => 'est une étape importante de', + 'fixes' => 'corrige', + 'is fixed by' => 'est corrigée par', + 'This task' => 'Cette tâche', + '<1h' => '<1h', + '%dh' => '%dh', + '%b %e' => '%e %b', + 'Expand tasks' => 'Déplier les tâches', + 'Collapse tasks' => 'Replier les tâches', + 'Expand/collapse tasks' => 'Plier/déplier les tâches', + 'Close dialog box' => 'Fermer une boite de dialogue', + 'Submit a form' => 'Enregistrer un formulaire', + 'Board view' => 'Page du tableau', + 'Keyboard shortcuts' => 'Raccourcis clavier', + 'Open board switcher' => 'Ouvrir le sélecteur de tableau', + 'Application' => 'Application', + 'Filter recently updated' => 'Récemment modifié', + 'since %B %e, %Y at %k:%M %p' => 'depuis le %d/%m/%Y à %H:%M', + 'More filters' => 'Plus de filtres', ); diff --git a/sources/app/Locale/hu_HU/translations.php b/sources/app/Locale/hu_HU/translations.php index b879b78..1780505 100644 --- a/sources/app/Locale/hu_HU/translations.php +++ b/sources/app/Locale/hu_HU/translations.php @@ -1,23 +1,23 @@ 'Semelyik', + 'None' => 'Nincs', 'edit' => 'szerkesztés', 'Edit' => 'Szerkesztés', - 'remove' => 'eltávolít', - 'Remove' => 'Eltávolít', + 'remove' => 'eltávolítás', + 'Remove' => 'Eltávolítás', 'Update' => 'Frissítés', 'Yes' => 'Igen', - 'No' => 'Nincs', - 'cancel' => 'mégsem', + 'No' => 'Nem', + 'cancel' => 'Mégsem', 'or' => 'vagy', - 'Yellow' => 'sárga', - 'Blue' => 'kék', - 'Green' => 'zöld', - 'Purple' => 'ibolya', - 'Red' => 'piros', - 'Orange' => 'narancs', - 'Grey' => 'szürke', + 'Yellow' => 'Sárga', + 'Blue' => 'Kék', + 'Green' => 'Zöld', + 'Purple' => 'Lila', + 'Red' => 'Piros', + 'Orange' => 'Narancs', + 'Grey' => 'Szürke', 'Save' => 'Mentés', 'Login' => 'Bejelentkezés', 'Official website:' => 'Hivatalos honlap:', @@ -25,7 +25,7 @@ return array( 'View this task' => 'Feladat megtekintése', 'Remove user' => 'Felhasználó törlése', 'Do you really want to remove this user: "%s"?' => 'Tényleg törli ezt a felhasználót: "%s"?', - 'New user' => 'új felhasználó', + 'New user' => 'Új felhasználó', 'All users' => 'Minden felhasználó', 'Username' => 'Felhasználónév', 'Password' => 'Jelszó', @@ -56,17 +56,17 @@ return array( 'Active' => 'Aktív', 'Column %d' => 'Oszlop %d', 'Add this column' => 'Oszlop hozzáadása', - '%d tasks on the board' => 'A táblán %d feladat', + '%d tasks on the board' => '%d feladat a táblán', '%d tasks in total' => 'Összesen %d feladat', 'Unable to update this board.' => 'Nem lehet frissíteni a táblát.', 'Edit board' => 'Tábla szerkesztése', - 'Disable' => 'Letilt', - 'Enable' => 'Engedélyez', + 'Disable' => 'Letiltás', + 'Enable' => 'Engedélyezés', 'New project' => 'Új projekt', 'Do you really want to remove this project: "%s"?' => 'Valóban törölni akarja ezt a projektet: "%s"?', 'Remove project' => 'Projekt törlése', 'Boards' => 'Táblák', - 'Edit the board for "%s"' => 'Tábla szerkesztése "%s"', + 'Edit the board for "%s"' => 'Tábla szerkesztése: "%s"', 'All projects' => 'Minden projekt', 'Change columns' => 'Oszlop módosítása', 'Add a new column' => 'Új oszlop', @@ -92,7 +92,7 @@ return array( '(VACUUM command)' => '(VACUUM parancs)', '(Gzip compressed Sqlite file)' => '(Gzip tömörített SQLite fájl)', 'User settings' => 'Felhasználói beállítások', - 'My default project:' => 'Alapértelmezett project:', + 'My default project:' => 'Alapértelmezett projekt: ', 'Close a task' => 'Feladat lezárása', 'Do you really want to close this task: "%s"?' => 'Tényleg le akarja zárni ezt a feladatot: "%s"?', 'Edit a task' => 'Feladat módosítása', @@ -100,17 +100,17 @@ return array( 'Color' => 'Szín', 'Assignee' => 'Felelős', 'Create another task' => 'Új feladat létrehozása', - 'New task' => 'új feladat', - 'Open a task' => 'Feladat megnyitása', + 'New task' => 'Új feladat', + 'Open a task' => 'Feladat felnyitás', 'Do you really want to open this task: "%s"?' => 'Tényleg meg akarja nyitni ezt a feladatot: "%s"?', 'Back to the board' => 'Vissza a táblához', - 'Created on %B %e, %Y at %k:%M %p' => 'Létrehozva: %Y.%m.%d %k:%M %p', + 'Created on %B %e, %Y at %k:%M %p' => 'Létrehozva: %Y.%m.%d %H:%M', 'There is nobody assigned' => 'Nincs felelős', - 'Column on the board:' => 'Tábla oszlopa:', - 'Status is open' => 'Állapot nyitva', - 'Status is closed' => 'Állapot zárva', - 'Close this task' => 'Feladat bezárása', - 'Open this task' => 'Feladat megnyitása', + 'Column on the board:' => 'Tábla oszlopa: ', + 'Status is open' => 'Nyitott állapot', + 'Status is closed' => 'Zárt állapot', + 'Close this task' => 'Feladat lezárása', + 'Open this task' => 'Feladat felnyitása', 'There is no description.' => 'Nincs elérhető leírás.', 'Add a new task' => 'Új feladat hozzáadása', 'The username is required' => 'Felhasználói név szükséges', @@ -133,37 +133,37 @@ return array( 'The title is required' => 'A címet meg kell adni', 'The language is required' => 'A nyelvet meg kell adni', 'There is no active project, the first step is to create a new project.' => 'Nincs aktív projekt. Először létre kell hozni egy projektet.', - 'Settings saved successfully.' => 'A beállítások sikeresen mentve.', - 'Unable to save your settings.' => 'Beállítások mentése nem sikerült.', + 'Settings saved successfully.' => 'A beállítások mentése sikeres.', + 'Unable to save your settings.' => 'A beállítások mentése sikertelen.', 'Database optimization done.' => 'Adatbázis optimalizálás kész.', - 'Your project have been created successfully.' => 'A projekt sikeresen elkészült.', - 'Unable to create your project.' => 'Projekt létrehozása nem sikerült.', - 'Project updated successfully.' => 'Projekt sikeres frissítve.', - 'Unable to update this project.' => 'Projekt frissítése nem sikerült.', - 'Unable to remove this project.' => 'Projekt törlése nem sikerült.', + 'Your project have been created successfully.' => 'Projekt sikeresen létrehozva', + 'Unable to create your project.' => 'Projekt létrehozása sikertelen.', + 'Project updated successfully.' => 'Projekt frissítése sikeres.', + 'Unable to update this project.' => 'Projekt frissítése sikertelen.', + 'Unable to remove this project.' => 'Projekt törlése sikertelen.', 'Project removed successfully.' => 'Projekt sikeresen törölve.', - 'Project activated successfully.' => 'Projekt sikeresen aktiválta.', - 'Unable to activate this project.' => 'Projekt aktiválása nem sikerült.', + 'Project activated successfully.' => 'Projekt sikeresen aktiválva.', + 'Unable to activate this project.' => 'Projekt aktiválása sikertelen.', 'Project disabled successfully.' => 'Projekt sikeresen letiltva.', - 'Unable to disable this project.' => 'Projekt letiltása nem sikerült.', - 'Unable to open this task.' => 'A feladat megnyitása nem sikerült.', + 'Unable to disable this project.' => 'Projekt letiltása sikertelen.', + 'Unable to open this task.' => 'A feladat felnyitása nem sikerült.', 'Task opened successfully.' => 'Feladat sikeresen megnyitva .', - 'Unable to close this task.' => 'A feladat lezárása nem sikerült.', + 'Unable to close this task.' => 'A feladat lezárása sikertelen.', 'Task closed successfully.' => 'Feladat sikeresen lezárva.', - 'Unable to update your task.' => 'A feladat frissítése nem sikerült.', + 'Unable to update your task.' => 'Feladat frissítése sikertelen.', 'Task updated successfully.' => 'Feladat sikeresen frissítve.', - 'Unable to create your task.' => 'A feladat létrehozása nem sikerült.', + 'Unable to create your task.' => 'Feladat létrehozása sikertelen.', 'Task created successfully.' => 'Feladat sikeresen létrehozva.', - 'User created successfully.' => 'Felhasználó létrehozva .', - 'Unable to create your user.' => 'Felhasználó létrehozása nem sikerült.', + 'User created successfully.' => 'Felhasználó létrehozva.', + 'Unable to create your user.' => 'Felhasználó létrehozása sikertelen.', 'User updated successfully.' => 'Felhasználó sikeresen frissítve.', - 'Unable to update your user.' => 'Felhasználó frissítése nem sikerült.', + 'Unable to update your user.' => 'Felhasználó frissítése sikertelen.', 'User removed successfully.' => 'Felhasználó sikeresen törölve.', - 'Unable to remove this user.' => 'Felhasználó törlése nem sikerült.', + 'Unable to remove this user.' => 'Felhasználó törlése sikertelen.', 'Board updated successfully.' => 'Tábla sikeresen frissítve.', - 'Ready' => 'Kész', + 'Ready' => 'Előkészítés', 'Backlog' => 'Napló', - 'Work in progress' => 'Dolgozom', + 'Work in progress' => 'Folyamatban', 'Done' => 'Kész', 'Application version:' => 'Alkalmazás verzió:', 'Completed on %B %e, %Y at %k:%M %p' => 'Elkészült %Y.%m.%d %H:%M ..', @@ -174,7 +174,7 @@ return array( 'No task' => 'Nincs feladat', 'Completed tasks' => 'Elvégzett feladatok', 'List of projects' => 'Projektek listája', - 'Completed tasks for "%s"' => 'Elvégzett feladatok "%s"', + 'Completed tasks for "%s"' => 'Elvégzett feladatok: %s', '%d closed tasks' => '%d lezárt feladat', 'No task for this project' => 'Nincs feladat ebben a projektben', 'Public link' => 'Nyilvános link', @@ -213,14 +213,14 @@ return array( 'Invalid date' => 'Érvénytelen dátum', 'Must be done before %B %e, %Y' => 'Kész kell lennie %Y.%m.%d előtt', '%B %e, %Y' => '%Y.%m.%d', - // '%b %e, %Y' => '', + '%b %e, %Y' => '%Y.%m.%d', 'Automatic actions' => 'Automatikus intézkedések', 'Your automatic action have been created successfully.' => 'Az automatikus intézkedés sikeresen elkészült.', 'Unable to create your automatic action.' => 'Automatikus intézkedés létrehozása nem lehetséges.', 'Remove an action' => 'Intézkedés törlése', 'Unable to remove this action.' => 'Intézkedés törlése nem lehetséges.', 'Action removed successfully.' => 'Intézkedés sikeresen törölve.', - 'Automatic actions for the project "%s"' => 'Automatikus intézkedések a projektben "%s"', + 'Automatic actions for the project "%s"' => 'Automatikus intézkedések a projektben: "%s"', 'Defined actions' => 'Intézkedések', 'Add an action' => 'Intézkedés létrehozása', 'Event name' => 'Esemény neve', @@ -242,15 +242,15 @@ return array( 'Move a task to another position in the same column' => 'Feladat mozgatása oszlopon belül', 'Task modification' => 'Feladat módosítása', 'Task creation' => 'Feladat létrehozása', - 'Open a closed task' => 'Lezárt feladat megnyitása', + 'Open a closed task' => 'Lezárt feladat felnyitása', 'Closing a task' => 'Feladat lezárása', 'Assign a color to a specific user' => 'Szín hozzárendelése a felhasználóhoz', 'Column title' => 'Oszlopfejléc', 'Position' => 'Pozíció', 'Move Up' => 'Fel', 'Move Down' => 'Le', - 'Duplicate to another project' => 'Másold egy másik projektbe', - 'Duplicate' => 'Másolat', + 'Duplicate to another project' => 'Másolás másik projektbe', + 'Duplicate' => 'Másolás', 'link' => 'link', 'Update this comment' => 'Hozzászólás frissítése', 'Comment updated successfully.' => 'Megjegyzés sikeresen frissítve.', @@ -261,7 +261,7 @@ return array( 'Do you really want to remove this comment?' => 'Valóban törölni szeretné ezt a megjegyzést?', 'Only administrators or the creator of the comment can access to this page.' => 'Csak a rendszergazdák és a megjegyzés létrehozója férhet hozzá az oldalhoz.', 'Details' => 'Részletek', - 'Current password for the user "%s"' => 'Felhasználó jelenlegi jelszava "%s"', + 'Current password for the user "%s"' => 'Felhasználó jelenlegi jelszava: "%s"', 'The current password is required' => 'A jelenlegi jelszót meg kell adni', 'Wrong password' => 'Hibás jelszó', 'Reset all tokens' => 'Reseteld az összes tokent', @@ -279,14 +279,14 @@ return array( 'Creation date' => 'Létrehozás dátuma', 'Filter by user' => 'Szűrés felhasználó szerint', 'Filter by due date' => 'Szűrés határidő szerint', - 'Everybody' => 'Mindenki', + 'Everybody' => 'Minden felhasználó', 'Open' => 'Nyitott', 'Closed' => 'Lezárt', - 'Search' => 'Keres', - 'Nothing found.' => 'Semmit sem találtam.', - 'Search in the project "%s"' => 'Keresés a projektben "%s"', + 'Search' => 'Keresés', + 'Nothing found.' => 'Nincs találat.', + 'Search in the project "%s"' => 'Keresés a projektben: "%s"', 'Due date' => 'Határidő', - 'Others formats accepted: %s and %s' => 'Egyéb érvényes formátumok: %s és %s', + 'Others formats accepted: %s and %s' => 'Egyéb érvényes formátumok: "%s" és "%s"', 'Description' => 'Leírás', '%d comments' => '%d megjegyzés', '%d comment' => '%d megjegyzés', @@ -302,7 +302,7 @@ return array( 'Login with my Google Account' => 'Jelentkezzen be Google fiókkal', 'Project not found.' => 'A projekt nem található.', 'Task #%d' => 'Feladat #%d.', - 'Task removed successfully.' => 'Feladat törlése sikerült.', + 'Task removed successfully.' => 'Feladat sikeresen törölve.', 'Unable to remove this task.' => 'A feladatot nem lehet törölni.', 'Remove a task' => 'Feladat törlése', 'Do you really want to remove this task: "%s"?' => 'Valóban törölni akarja ezt a feladatot: "%s"?', @@ -313,7 +313,7 @@ return array( 'Category:' => 'Kategória:', 'Categories' => 'Kategóriák', 'Category not found.' => 'Kategória nem található.', - 'Your category have been created successfully.' => 'Kategória sikeresen létrejött.', + 'Your category have been created successfully.' => 'Kategória sikeresen létrehozva.', 'Unable to create your category.' => 'A kategória létrehozása nem lehetséges.', 'Your category have been updated successfully.' => 'Kategória sikeresen frissítve.', 'Unable to update your category.' => 'Kategória frissítése nem lehetséges.', @@ -324,14 +324,14 @@ return array( 'Category Name' => 'Kategória neve', 'Categories for the project "%s"' => 'Projekt kategóriák "%s"', 'Add a new category' => 'Új kategória', - 'Do you really want to remove this category: "%s"?' => 'Valóban törölni akarja ezt a kategóriát "%s"?', - 'Filter by category' => 'Szűrés kategóriára', + 'Do you really want to remove this category: "%s"?' => 'Valóban törölni akarja ezt a kategóriát: "%s"?', + 'Filter by category' => 'Szűrés kategória szerint', 'All categories' => 'Minden kategória', 'No category' => 'Nincs kategória', 'The name is required' => 'A név megadása kötelező', 'Remove a file' => 'Fájl törlése', 'Unable to remove this file.' => 'Fájl törlése nem lehetséges.', - 'File removed successfully.' => 'A fájl törlése sikerült.', + 'File removed successfully.' => 'Fájl sikeresen törölve.', 'Attach a document' => 'Fájl csatolása', 'Do you really want to remove this file: "%s"?' => 'Valóban törölni akarja a fájlt: "%s"?', 'open' => 'nyitott', @@ -344,12 +344,12 @@ return array( 'Time tracking' => 'Idő követés', 'Estimate:' => 'Becsült:', 'Spent:' => 'Eltöltött:', - 'Do you really want to remove this sub-task?' => 'Valóban törölni akarja ezt a részfeladatot "%s"?', + 'Do you really want to remove this sub-task?' => 'Valóban törölni akarja ezt a részfeladatot?', 'Remaining:' => 'Hátralévő:', 'hours' => 'óra', 'spent' => 'eltöltött', 'estimated' => 'becsült', - 'Sub-Tasks' => 'részfeladatok', + 'Sub-Tasks' => 'Részfeladatok', 'Add a sub-task' => 'Részfeladat létrehozása', 'Original estimate' => 'Eredeti időbecslés', 'Create another sub-task' => 'További részfeladat létrehozása', @@ -364,8 +364,8 @@ return array( 'Sub-task updated successfully.' => 'Részfeladat sikeresen frissítve.', 'Unable to update your sub-task.' => 'Részfeladat frissítése nem lehetséges.', 'Unable to create your sub-task.' => 'Részfeladat létrehozása nem lehetséges.', - 'Sub-task added successfully.' => 'Részfeladat sikeresen létrejött.', - 'Maximum size: ' => 'Maximális méret:', + 'Sub-task added successfully.' => 'Részfeladat sikeresen létrehozva.', + 'Maximum size: ' => 'Maximális méret: ', 'Unable to upload the file.' => 'Fájl feltöltése nem lehetséges.', 'Display another project' => 'Másik projekt megjelenítése', 'Your GitHub account was successfully linked to your profile.' => 'GitHub fiók sikeresen csatolva a profilhoz.', @@ -377,9 +377,9 @@ return array( 'Link my GitHub Account' => 'GitHub fiók csatolása', 'Unlink my GitHub Account' => 'GitHub fiók leválasztása', 'Created by %s' => 'Készítette: %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Utolsó módosítás %Y.%m.%d %H:%M', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Utolsó módosítás: %Y.%m.%d %H:%M', 'Tasks Export' => 'Feladatok exportálása', - 'Tasks exportation for "%s"' => 'Feladatok exportálása "%s" részére', + 'Tasks exportation for "%s"' => 'Feladatok exportálása: "%s"', 'Start Date' => 'Kezdés dátuma', 'End Date' => 'Befejezés dátuma', 'Execute' => 'Végrehajt', @@ -390,33 +390,31 @@ return array( 'Webhook URL for task creation' => 'Webhook URL a feladat létrehozásakor', 'Webhook URL for task modification' => 'Webhook URL a feladatot módosításakor', 'Clone' => 'Másolat', - 'Clone Project' => 'Projekt megkettőzése', - 'Project cloned successfully.' => 'A projekt sikeresen megkettőzve.', - 'Unable to clone this project.' => 'Projekt megkettőzése nem sikerült.', + 'Clone Project' => 'Projekt másolása', + 'Project cloned successfully.' => 'A projekt másolása sikeres', + 'Unable to clone this project.' => 'A projekt másolása sikertelen.', 'Email notifications' => 'E-mail értesítések', - 'Enable email notifications' => 'Engedélyezze az e-mail értesítéseket', + 'Enable email notifications' => 'E-mail értesítések engedélyezése', 'Task position:' => 'Feladat helye:', 'The task #%d have been opened.' => 'Feladat #%d megnyitva.', 'The task #%d have been closed.' => 'Feladat #%d lezárva.', 'Sub-task updated' => 'Részfeladat frissítve', 'Title:' => 'Cím', - 'Status:' => 'Állapot', + 'Status:' => 'Állapot:', 'Assignee:' => 'Felelős:', 'Time tracking:' => 'Idő követés:', 'New sub-task' => 'Új részfeladat', 'New attachment added "%s"' => 'Új melléklet "%s" hozzáadva.', 'Comment updated' => 'Megjegyzés frissítve', 'New comment posted by %s' => 'Új megjegyzés %s', - 'List of due tasks for the project "%s"' => 'Projekt esedékes feladatai "%s"', - '[%s][New attachment] %s (#%d)' => '[%s] [Új csatolmány] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s] [Új hozzászólás] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s] [Megjegyzés frissítve] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s] [Új részfeladat] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s] [Részfeladat frissítve] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s] [Új feladat] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s] [Feladat frissítve] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s] [Feladat lezárva]%s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s] [Feladat megnyitva] %s (#%d)', + 'List of due tasks for the project "%s"' => 'Projekt esedékes feladatai: "%s"', + 'New attachment' => 'Új melléklet', + 'New comment' => 'Új megjegyzés', + 'New subtask' => 'Új részfeladat', + 'Subtask updated' => 'Részfeladat frissítve', + 'Task updated' => 'Feladat frissítve', + 'Task closed' => 'Feladat lezárva', + 'Task opened' => 'Feladat megnyitva', '[%s][Due tasks]' => '[%s] [Esedékes feladatok]', '[Kanboard] Notification' => '[Kanboard] értesítés', 'I want to receive notifications only for those projects:' => 'Csak ezekről a projektekről kérek értesítést:', @@ -434,27 +432,27 @@ return array( 'Do you really want to duplicate this project: "%s"?' => 'Tényleg szeretné megkettőzni ezt a projektet: "%s"', 'Do you really want to enable this project: "%s"?' => 'Tényleg szeretné engedélyezni ezt a projektet: "%s"', 'Project activation' => 'Projekt aktiválás', - 'Move the task to another project' => 'Feladatot mozgatása másik projektbe', - 'Move to another project' => 'Másik projektbe', + 'Move the task to another project' => 'Feladat áthelyezése másik projektbe', + 'Move to another project' => 'Áthelyezés másik projektbe', 'Do you really want to duplicate this task?' => 'Tényleg szeretné megkettőzni ezt a feladatot?', 'Duplicate a task' => 'Feladat megkettőzése', 'External accounts' => 'Külső fiókok', - 'Account type' => 'Fiók típus', + 'Account type' => 'Fiók típusa', 'Local' => 'Helyi', 'Remote' => 'Távoli', 'Enabled' => 'Engedélyezve', 'Disabled' => 'Letiltva', 'Google account linked' => 'Google fiók összekapcsolva', 'Github account linked' => 'GitHub fiók összekapcsolva', - 'Username:' => 'Felhasználónév', - 'Name:' => 'Név', - 'Email:' => 'E-mail', + 'Username:' => 'Felhasználónév:', + 'Name:' => 'Név:', + 'Email:' => 'E-mail:', 'Default project:' => 'Alapértelmezett projekt:', 'Notifications:' => 'Értesítések:', 'Notifications' => 'Értesítések', 'Group:' => 'Csoport:', 'Regular user' => 'Default User', - 'Account type:' => 'Fiók típus:', + 'Account type:' => 'Fiók típusa:', 'Edit profile' => 'Profil szerkesztése', 'Change password' => 'Jelszó módosítása', 'Password modification' => 'Jelszó módosítása', @@ -464,9 +462,9 @@ return array( 'Never connected.' => 'Sosem csatlakozva.', 'No account linked.' => 'Nincs csatlakoztatott fiók.', 'Account linked.' => 'Fiók csatlakoztatva.', - 'No external authentication enabled.' => 'Külső azonosítás nincs engedélyezve.', - 'Password modified successfully.' => 'Jelszó sikeresen módosítva.', - 'Unable to change the password.' => 'Jelszó módosítás sikertelen.', + 'No external authentication enabled.' => 'A külső azonosítás nincs engedélyezve.', + 'Password modified successfully.' => 'A jelszó sikeresen módosítva.', + 'Unable to change the password.' => 'A jelszó módosítása sikertelen.', 'Change category for the task "%s"' => 'Feladat kategória módosítása "%s"', 'Change category' => 'Kategória módosítása', '%s updated the task %s' => '%s frissítette a feladatot %s', @@ -481,7 +479,7 @@ return array( 'Not assigned, estimate of %sh' => 'Nincs kiosztva, becsült idő: %s óra', '%s updated a comment on the task %s' => '%s frissítette a megjegyzését a feladatban %s', '%s commented the task %s' => '%s megjegyzést fűzött a feladathoz %s', - '%s\'s activity' => '%s tevékenysége', + '%s\'s activity' => 'Tevékenységek: %s', 'No activity.' => 'Nincs tevékenység.', 'RSS feed' => 'RSS feed', '%s updated a comment on the task #%d' => '%s frissített egy megjegyzést a feladatban #%d', @@ -494,20 +492,20 @@ return array( '%s open the task #%d' => '%s megnyitotta a feladatot #%d', '%s moved the task #%d to the column "%s"' => '%s átmozgatta a feladatot #%d a "%s" oszlopba', '%s moved the task #%d to the position %d in the column "%s"' => '%s átmozgatta a feladatot #%d a %d pozícióba a "%s" oszlopban', - 'Activity' => 'Tevékenység', - 'Default values are "%s"' => 'Az alapértelmezett értékek "%s"', + 'Activity' => 'Tevékenységek', + 'Default values are "%s"' => 'Az alapértelmezett értékek: %s', 'Default columns for new projects (Comma-separated)' => 'Alapértelmezett oszlopok az új projektekben (vesszővel elválasztva)', 'Task assignee change' => 'Felelős módosítása', '%s change the assignee of the task #%d to %s' => '%s a felelőst módosította #%d %s', '%s changed the assignee of the task %s to %s' => '%s a felelőst %s módosította: %s', - '[%s][Column Change] %s (#%d)' => '[%s] [Oszlop módosítás] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s] [Pozíció módosítás] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s] [Felelős módosítás] %s (#%d)', - 'New password for the user "%s"' => 'Felhasználó új jelszava "%s"', + 'Column Change' => 'Oszlop változtatás', + 'Position Change' => 'Pozíció változtatás', + 'Assignee Change' => 'Felelős változtatás', + 'New password for the user "%s"' => 'Felhasználó új jelszava: %s', 'Choose an event' => 'Válasszon eseményt', 'Github commit received' => 'GitHub commit érkezett', - 'Github issue opened' => 'GitHub issue nyílt', - 'Github issue closed' => 'GitHub issue zárt', + 'Github issue opened' => 'GitHub issue nyitás', + 'Github issue closed' => 'GitHub issue zárás', 'Github issue reopened' => 'GitHub issue újranyitva', 'Github issue assignee change' => 'GitHub issue felelős változás', 'Github issue label change' => 'GitHub issue címke változás', @@ -524,23 +522,23 @@ return array( 'URL and token' => 'URL és tokenek', 'Webhook settings' => 'Webhook beállítások', 'URL for task creation:' => 'Feladat létrehozás URL:', - 'Reset token' => 'Reset token', - 'API endpoint:' => 'API endpoint:', + 'Reset token' => 'Token újragenerálása', + 'API endpoint:' => 'API végpont:', 'Refresh interval for private board' => 'Privát táblák frissítési intervalluma', 'Refresh interval for public board' => 'Nyilvános táblák frissítési intervalluma', 'Task highlight period' => 'Feladat kiemelés időtartama', 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Mennyi ideig tekintendő egy feladat "mostanában" módosítottnak (másodpercben) (0: funkció letiltva, alapértelmezés szerint 2 nap)', - 'Frequency in second (60 seconds by default)' => 'Infó másodpercben (alapértelmezett 60 másodperc)', - 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Infó másodpercben (0 funkció letiltva, alapértelmezés szerint 10 másodperc)', + 'Frequency in second (60 seconds by default)' => 'Gyakoriság másodpercben (alapértelmezetten 60 másodperc)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Gyakoriság másodpercben (0 funkció letiltva, alapértelmezetten 10 másodperc)', 'Application URL' => 'Alkalmazás URL', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Példa: http://example.kanboard.net/ (e-mail értesítőben)', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Példa: http://example.kanboard.net/ (e-mail értesítőben használt)', 'Token regenerated.' => 'Token újragenerálva.', 'Date format' => 'Dátum formátum', 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formátum mindig elfogadott, pl: "%s" és "%s"', 'New private project' => 'Új privát projekt', 'This project is private' => 'Ez egy privát projekt', 'Type here to create a new sub-task' => 'Ide írva létrehozhat egy új részfeladatot', - 'Add' => 'Hozzáad', + 'Add' => 'Hozzáadás', 'Estimated time: %s hours' => 'Becsült idő: %s óra', 'Time spent: %s hours' => 'Eltöltött idő: %s óra', 'Started on %B %e, %Y' => 'Elkezdve: %Y.%m.%d', @@ -549,9 +547,9 @@ return array( 'There is nothing assigned to you.' => 'Nincs kiosztott feladat.', 'My tasks' => 'Feladataim', 'Activity stream' => 'Legutóbbi tevékenységek', - 'Dashboard' => 'Műszerfal', + 'Dashboard' => 'Vezérlőpult', 'Confirmation' => 'Megerősítés', - 'Allow everybody to access to this project' => 'Engedélyezze a projekt elérését mindenkinek', + 'Allow everybody to access to this project' => 'A projekt elérése mindenkinek engedélyezett', 'Everybody have access to this project.' => 'Mindenki elérheti a projektet', 'Webhooks' => 'Webhook', 'API' => 'API', @@ -560,7 +558,7 @@ return array( 'Help on Github webhooks' => 'Github Webhook súgó', 'Create a comment from an external provider' => 'Megjegyzés létrehozása külső felhasználótól', 'Github issue comment created' => 'Github issue megjegyzés létrehozva', - 'Configure' => 'Konfigurál', + 'Configure' => 'Beállítások', 'Project management' => 'Projekt menedzsment', 'My projects' => 'Projektjeim', 'Columns' => 'Oszlopok', @@ -570,12 +568,12 @@ return array( 'Number of tasks' => 'A feladatok száma', 'Task distribution' => 'Feladatelosztás', 'Reportings' => 'Jelentések', - 'Task repartition for "%s"' => 'Feladat újraosztása "%s" számára', + 'Task repartition for "%s"' => 'Feladat újraosztása: %s', 'Analytics' => 'Analitika', 'Subtask' => 'Részfeladat', 'My subtasks' => 'Részfeladataim', 'User repartition' => 'Felhasználó újrafelosztás', - 'User repartition for "%s"' => 'Felhasználó újrafelosztás "%s" számára', + 'User repartition for "%s"' => 'Felhasználó újrafelosztás: %s', 'Clone this project' => 'Projekt megkettőzése', 'Column removed successfully.' => 'Oszlop sikeresen eltávolítva.', 'Edit Project' => 'Projekt szerkesztése', @@ -594,58 +592,146 @@ return array( 'This value must be numeric' => 'Ez a mező csak szám lehet', 'Unable to create this task.' => 'A feladat nem hozható létre,', 'Cumulative flow diagram' => 'Kumulatív Flow Diagram', - 'Cumulative flow diagram for "%s"' => 'Kumulatív Flow Diagram "%s" számára', + 'Cumulative flow diagram for "%s"' => 'Kumulatív Flow Diagram: %s', 'Daily project summary' => 'Napi projektösszefoglaló', 'Daily project summary export' => 'Napi projektösszefoglaló exportálása', - 'Daily project summary export for "%s"' => 'Napi projektösszefoglaló exportálása "%s" számára', + 'Daily project summary export for "%s"' => 'Napi projektösszefoglaló exportálása: %s', 'Exports' => 'Exportálások', 'This export contains the number of tasks per column grouped per day.' => 'Ez az export tartalmazza a feladatok számát oszloponként összesítve, napokra lebontva.', 'Nothing to preview...' => 'Nincs semmi az előnézetben ...', 'Preview' => 'Előnézet', - 'Write' => 'Írd', - // 'Active swimlanes' => '', - // 'Add a new swimlane' => '', - // 'Change default swimlane' => '', - // 'Default swimlane' => '', - // 'Do you really want to remove this swimlane: "%s"?' => '', - // 'Inactive swimlanes' => '', - // 'Set project manager' => '', - // 'Set project member' => '', - // 'Remove a swimlane' => '', - // 'Rename' => '', - // 'Show default swimlane' => '', - // 'Swimlane modification for the project "%s"' => '', - // 'Swimlane not found.' => '', - // 'Swimlane removed successfully.' => '', - // 'Swimlanes' => '', - // 'Swimlane updated successfully.' => '', - // 'The default swimlane have been updated successfully.' => '', - // 'Unable to create your swimlane.' => '', - // 'Unable to remove this swimlane.' => '', - // 'Unable to update this swimlane.' => '', - // 'Your swimlane have been created successfully.' => '', - // 'Example: "Bug, Feature Request, Improvement"' => '', - // 'Default categories for new projects (Comma-separated)' => '', - // 'Gitlab commit received' => '', - // 'Gitlab issue opened' => '', - // 'Gitlab issue closed' => '', - // 'Gitlab webhooks' => '', - // 'Help on Gitlab webhooks' => '', - // 'Integrations' => '', - // 'Integration with third-party services' => '', - // 'Role for this project' => '', - // 'Project manager' => '', - // 'Project member' => '', - // 'A project manager can change the settings of the project and have more privileges than a standard user.' => '', - // 'Gitlab Issue' => '', - // 'Subtask Id' => '', - // 'Subtasks' => '', - // 'Subtasks Export' => '', - // 'Subtasks exportation for "%s"' => '', - // 'Task Title' => '', - // 'Untitled' => '', - // 'Application default' => '', - // 'Language:' => '', - // 'Timezone:' => '', - // 'Next' => '', + 'Write' => 'Szerkesztés', + 'Active swimlanes' => 'Aktív folyamatok', + 'Add a new swimlane' => 'Új folyamat', + 'Change default swimlane' => 'Alapértelmezett folyamat változtatás', + 'Default swimlane' => 'Alapértelmezett folyamat', + 'Do you really want to remove this swimlane: "%s"?' => 'Valóban törli a folyamatot:%s ?', + 'Inactive swimlanes' => 'Inaktív folyamatok', + 'Set project manager' => 'Beállítás projekt kezelőnek', + 'Set project member' => 'Beállítás projekt felhasználónak', + 'Remove a swimlane' => 'Folyamat törlés', + 'Rename' => 'Átnevezés', + 'Show default swimlane' => 'Alapértelmezett folyamat megjelenítése', + 'Swimlane modification for the project "%s"' => '%s projekt folyamatainak módosítása', + 'Swimlane not found.' => 'Folyamat nem található', + 'Swimlane removed successfully.' => 'Folyamat sikeresen törölve.', + 'Swimlanes' => 'Folyamatok', + 'Swimlane updated successfully.' => 'Folyamat sikeresn frissítve', + 'The default swimlane have been updated successfully.' => 'Az alapértelmezett folyamat sikeresen frissítve.', + 'Unable to create your swimlane.' => 'A folyamat létrehozása sikertelen.', + 'Unable to remove this swimlane.' => 'A folyamat törlése sikertelen.', + 'Unable to update this swimlane.' => 'A folyamat frissítése sikertelen.', + 'Your swimlane have been created successfully.' => 'A folyamat sikeresen létrehozva.', + 'Example: "Bug, Feature Request, Improvement"' => 'Például: Hiba, Új funkció, Fejlesztés', + 'Default categories for new projects (Comma-separated)' => 'Alapértelmezett kategóriák az új projektekben (Vesszővel elválasztva)', + 'Gitlab commit received' => 'Gitlab commit érkezett', + 'Gitlab issue opened' => 'Gitlab issue nyitás', + 'Gitlab issue closed' => 'Gitlab issue zárás', + 'Gitlab webhooks' => 'Gitlab webhooks', + 'Help on Gitlab webhooks' => 'Gitlab webhooks súgó', + 'Integrations' => 'Integráció', + 'Integration with third-party services' => 'Integráció harmadik féllel', + 'Role for this project' => 'Projekt szerepkör', + 'Project manager' => 'Projekt kezelő', + 'Project member' => 'Projekt felhasználó', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'A projekt kezelő képes megváltoztatni a projekt beállításait és több joggal rendelkezik mint az alap felhasználók.', + 'Gitlab Issue' => 'Gitlab issue', + 'Subtask Id' => 'Részfeladat id', + 'Subtasks' => 'Részfeladatok', + 'Subtasks Export' => 'Részfeladat exportálás', + 'Subtasks exportation for "%s"' => 'Részfeladatok exportálása: %s', + 'Task Title' => 'Feladat címe', + 'Untitled' => 'Névtelen', + 'Application default' => 'Alkalmazás alapértelmezett', + 'Language:' => 'Nyelv:', + 'Timezone:' => 'Időzóna:', + 'All columns' => 'Minden oszlop', + 'Calendar for "%s"' => 'Naptár: %s', + 'Filter by column' => 'Szűrés oszlop szerint', + 'Filter by status' => 'Szűrés állapot szerint', + 'Calendar' => 'Naptár', + 'Next' => 'Következő', + '#%d' => '#%d', + 'Filter by color' => 'Szűrés szín szerint', + 'Filter by swimlane' => 'Szűrés folyamat szerint', + 'All swimlanes' => 'Minden folyamat', + 'All colors' => 'Minden szín', + 'All status' => 'Minden állapot', + 'Add a comment logging moving the task between columns' => 'Feladat oszlopok közötti mozgatását megjegyzésben feltüntetni', + 'Moved to column %s' => '%s oszlopba áthelyezve', + 'Change description' => 'Leírás szerkesztés', + 'User dashboard' => 'Felhasználói vezérlőpult', + 'Allow only one subtask in progress at the same time for a user' => 'Egyszerre csak egy folyamatban levő részfeladat engedélyezése a felhasználóknak', + 'Edit column "%s"' => 'Oszlop szerkesztés: %s', + 'Enable time tracking for subtasks' => 'Idő követés engedélyezése a részfeladatokhoz', + 'Select the new status of the subtask: "%s"' => 'Részfeladat állapot változtatás: %s', + 'Subtask timesheet' => 'Részfeladat idővonal', + 'There is nothing to show.' => 'Nincs megjelenítendő adat.', + 'Time Tracking' => 'Idő követés', + 'You already have one subtask in progress' => 'Már van egy folyamatban levő részfeladata', + 'Which parts of the project do you want to duplicate?' => 'A projekt mely részeit szeretné duplikálni?', + 'Change dashboard view' => 'Vezérlőpult megjelenítés változtatás', + 'Show/hide activities' => 'Tevékenységek megjelenítése/elrejtése', + 'Show/hide projects' => 'Projektek megjelenítése/elrejtése', + 'Show/hide subtasks' => 'Részfeladatok megjelenítése/elrejtése', + 'Show/hide tasks' => 'Feladatok megjelenítése/elrejtése', + 'Disable login form' => 'Bejelentkező képernyő tiltása', + 'Show/hide calendar' => 'Naptár megjelenítés/elrejtés', + 'User calendar' => 'Naptár', + 'Bitbucket commit received' => 'Bitbucket commit érkezett', + 'Bitbucket webhooks' => 'Bitbucket webhooks', + 'Help on Bitbucket webhooks' => 'Bitbucket webhooks súgó', + 'Start' => 'Kezdet', + 'End' => 'Vég', + 'Task age in days' => 'Feladat életkora napokban', + 'Days in this column' => 'Napok ebben az oszlopban', + '%dd' => '%dd', + 'Add a link' => 'Hivatkozás hozzáadása', + 'Add a new link' => 'Új hivatkozás hozzáadása', + 'Do you really want to remove this link: "%s"?' => 'Biztos törölni akarja a hivatkozást: "%s"?', + 'Do you really want to remove this link with task #%d?' => 'Biztos törölni akarja a(z) #%s. feladatra mutató hivatkozást?', + 'Field required' => 'Kötelező mező', + 'Link added successfully.' => 'Hivatkozás sikeresen létrehozva.', + 'Link updated successfully.' => 'Hivatkozás sikeresen frissítve.', + 'Link removed successfully.' => 'Hivatkozás sikeresen törölve.', + 'Link labels' => 'Hivatkozás címkék', + 'Link modification' => 'Hivatkozás módosítás', + 'Links' => 'Hivatkozások', + 'Link settings' => 'Hivatkozás beállítasok', + 'Opposite label' => 'Ellenekező címke', + 'Remove a link' => 'Hivatkozás törlése', + 'Task\'s links' => 'Feladat hivatkozások', + 'The labels must be different' => 'A címkék nem lehetnek azonosak', + 'There is no link.' => 'Nincs hivatkozás.', + 'This label must be unique' => 'A címkének egyedinek kell lennie.', + 'Unable to create your link.' => 'Hivatkozás létrehozása sikertelen.', + 'Unable to update your link.' => 'Hivatkozás frissítése sikertelen.', + 'Unable to remove this link.' => 'Hivatkozás törlése sikertelen.', + 'relates to' => 'hozzá tartozik:', + 'blocks' => 'letiltva:', + 'is blocked by' => 'letitoltta:', + 'duplicates' => 'eredeti:', + 'is duplicated by' => 'másolat:', + 'is a child of' => 'szülője:', + 'is a parent of' => 'gyermeke:', + 'targets milestone' => 'megcélzott mérföldkő:', + 'is a milestone of' => 'ehhez a mérföldkőhöz tartozik:', + 'fixes' => 'javítás:', + 'is fixed by' => 'javította:', + 'This task' => 'Ez a feladat', + '<1h' => '<1ó', + '%dh' => '%dó', + '%b %e' => '%b %e', + 'Expand tasks' => 'Feladatok lenyitása', + 'Collapse tasks' => 'Feladatok összecsukása', + 'Expand/collapse tasks' => 'Feladatok lenyitása/összecsukása', + 'Close dialog box' => 'Ablak bezárása', + 'Submit a form' => 'Űrlap beküldése', + 'Board view' => 'Tábla nézet', + 'Keyboard shortcuts' => 'Billentyű kombináció', + 'Open board switcher' => 'Tábla választó lenyitása', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', ); diff --git a/sources/app/Locale/it_IT/translations.php b/sources/app/Locale/it_IT/translations.php index 0777e17..525c828 100644 --- a/sources/app/Locale/it_IT/translations.php +++ b/sources/app/Locale/it_IT/translations.php @@ -408,15 +408,13 @@ return array( 'Comment updated' => 'Commento aggiornato', 'New comment posted by %s' => 'Nuovo commento aggiunto da « %s »', 'List of due tasks for the project "%s"' => 'Lista dei compiti scaduti per il progetto « %s »', - '[%s][New attachment] %s (#%d)' => '[%s][Nuovo allegato] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][Nuovo commento] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][Commento aggiornato] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][Nuovo sotto-compito] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][Sotto-compito aggiornato] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][Nuovo compito] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][Compito aggiornato] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][Compito chiuso] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][Compito aperto] %s (#%d)', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + // 'Task closed' => '', + // 'Task opened' => '', '[%s][Due tasks]' => '[%s][Compiti scaduti]', '[Kanboard] Notification' => '[Kanboard] Notifica', 'I want to receive notifications only for those projects:' => 'Vorrei ricevere le notifiche solo da questi progetti:', @@ -500,9 +498,9 @@ return array( // 'Task assignee change' => '', // '%s change the assignee of the task #%d to %s' => '', // '%s changed the assignee of the task %s to %s' => '', - // '[%s][Column Change] %s (#%d)' => '', - // '[%s][Position Change] %s (#%d)' => '', - // '[%s][Assignee Change] %s (#%d)' => '', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', // 'New password for the user "%s"' => '', // 'Choose an event' => '', // 'Github commit received' => '', @@ -647,5 +645,93 @@ return array( // 'Application default' => '', // 'Language:' => '', // 'Timezone:' => '', + // 'All columns' => '', + // 'Calendar for "%s"' => '', + // 'Filter by column' => '', + // 'Filter by status' => '', + // 'Calendar' => '', // 'Next' => '', + // '#%d' => '', + // 'Filter by color' => '', + // 'Filter by swimlane' => '', + // 'All swimlanes' => '', + // 'All colors' => '', + // 'All status' => '', + // 'Add a comment logging moving the task between columns' => '', + // 'Moved to column %s' => '', + // 'Change description' => '', + // 'User dashboard' => '', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Enable time tracking for subtasks' => '', + // 'Select the new status of the subtask: "%s"' => '', + // 'Subtask timesheet' => '', + // 'There is nothing to show.' => '', + // 'Time Tracking' => '', + // 'You already have one subtask in progress' => '', + // 'Which parts of the project do you want to duplicate?' => '', + // 'Change dashboard view' => '', + // 'Show/hide activities' => '', + // 'Show/hide projects' => '', + // 'Show/hide subtasks' => '', + // 'Show/hide tasks' => '', + // 'Disable login form' => '', + // 'Show/hide calendar' => '', + // 'User calendar' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + // 'Task age in days' => '', + // 'Days in this column' => '', + // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + // 'Expand tasks' => '', + // 'Collapse tasks' => '', + // 'Expand/collapse tasks' => '', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', ); diff --git a/sources/app/Locale/ja_JP/translations.php b/sources/app/Locale/ja_JP/translations.php index f4f5215..90af6a0 100644 --- a/sources/app/Locale/ja_JP/translations.php +++ b/sources/app/Locale/ja_JP/translations.php @@ -408,15 +408,13 @@ return array( 'Comment updated' => 'コメントが更新されました', 'New comment posted by %s' => '「%s」の新しいコメントが追加されました', 'List of due tasks for the project "%s"' => 'プロジェクト「%s」の期限切れのタスク', - '[%s][New attachment] %s (#%d)' => '[%s][新規添付ファイル] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][新規コメント] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][コメント更新] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][新規サブタスク] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][サブタスク更新] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][新規タスク] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][タスク更新] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][タスククローズ] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][タスクオープン] %s (#%d)', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + // 'Task closed' => '', + // 'Task opened' => '', '[%s][Due tasks]' => '[%s][タスク期限切れ]', '[Kanboard] Notification' => '[Kanboard] 通知', 'I want to receive notifications only for those projects:' => '以下のプロジェクトにのみ通知を受け取る:', @@ -500,9 +498,9 @@ return array( 'Task assignee change' => '担当者の変更', '%s change the assignee of the task #%d to %s' => '%s がタスク #%d の担当を %s に変更しました', '%s changed the assignee of the task %s to %s' => '%s がタスク %s の担当を %s に変更しました', - '[%s][Column Change] %s (#%d)' => '[%s][カラムの変更] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][位置の変更] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][担当者変更] %s (#%d)', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', 'New password for the user "%s"' => 'ユーザ「%s」の新しいパスワード', 'Choose an event' => 'イベントの選択', 'Github commit received' => 'Github のコミットを受け取った', @@ -647,5 +645,93 @@ return array( // 'Application default' => '', // 'Language:' => '', // 'Timezone:' => '', + // 'All columns' => '', + // 'Calendar for "%s"' => '', + // 'Filter by column' => '', + // 'Filter by status' => '', + // 'Calendar' => '', // 'Next' => '', + // '#%d' => '', + // 'Filter by color' => '', + // 'Filter by swimlane' => '', + // 'All swimlanes' => '', + // 'All colors' => '', + // 'All status' => '', + // 'Add a comment logging moving the task between columns' => '', + // 'Moved to column %s' => '', + // 'Change description' => '', + // 'User dashboard' => '', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Enable time tracking for subtasks' => '', + // 'Select the new status of the subtask: "%s"' => '', + // 'Subtask timesheet' => '', + // 'There is nothing to show.' => '', + // 'Time Tracking' => '', + // 'You already have one subtask in progress' => '', + // 'Which parts of the project do you want to duplicate?' => '', + // 'Change dashboard view' => '', + // 'Show/hide activities' => '', + // 'Show/hide projects' => '', + // 'Show/hide subtasks' => '', + // 'Show/hide tasks' => '', + // 'Disable login form' => '', + // 'Show/hide calendar' => '', + // 'User calendar' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + // 'Task age in days' => '', + // 'Days in this column' => '', + // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + // 'Expand tasks' => '', + // 'Collapse tasks' => '', + // 'Expand/collapse tasks' => '', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', ); diff --git a/sources/app/Locale/pl_PL/translations.php b/sources/app/Locale/pl_PL/translations.php index 408828a..c6becf4 100644 --- a/sources/app/Locale/pl_PL/translations.php +++ b/sources/app/Locale/pl_PL/translations.php @@ -84,7 +84,7 @@ return array( 'Application settings' => 'Ustawienia aplikacji', 'Language' => 'Język', 'Webhook token:' => 'Token :', - // 'API token:' => '', + 'API token:' => 'Token dla API', 'More information' => 'Więcej informacji', 'Database size:' => 'Rozmiar bazy danych :', 'Download the database' => 'Pobierz bazę danych', @@ -170,7 +170,7 @@ return array( '%B %e, %Y at %k:%M %p' => '%e %B %Y o %k:%M', 'Date created' => 'Data utworzenia', 'Date completed' => 'Data zakończenia', - 'Id' => 'Ident', + 'Id' => 'Id', 'No task' => 'Brak zadań', 'Completed tasks' => 'Ukończone zadania', 'List of projects' => 'Lista projektów', @@ -187,7 +187,7 @@ return array( 'Complexity' => 'Poziom trudności', 'limit' => 'limit', 'Task limit' => 'Limit zadań', - // 'Task count' => '', + 'Task count' => 'Liczba zadań', 'This value must be greater than %d' => 'Wartość musi być większa niż %d', 'Edit project access list' => 'Edycja list dostępu dla projektu', 'Edit users access' => 'Edytuj dostęp', @@ -197,14 +197,14 @@ return array( 'Revoke' => 'Odbierz dostęp', 'List of authorized users' => 'Lista użytkowników mających dostęp', 'User' => 'Użytkownik', - // 'Nobody have access to this project.' => '', + 'Nobody have access to this project.' => 'Żaden użytkownik nie ma dostępu do tego projektu', 'You are not allowed to access to this project.' => 'Nie masz dostępu do tego projektu.', 'Comments' => 'Komentarze', 'Post comment' => 'Dodaj komentarz', 'Write your text in Markdown' => 'Możesz użyć Markdown', 'Leave a comment' => 'Zostaw komentarz', 'Comment is required' => 'Komentarz jest wymagany', - // 'Leave a description' => '', + 'Leave a description' => 'Dodaj opis', 'Comment added successfully.' => 'Komentarz dodany', 'Unable to create your comment.' => 'Nie udało się dodać komentarza', 'The description is required' => 'Opis jest wymagany', @@ -277,375 +277,461 @@ return array( 'Expiration date' => 'Data zakończenia', 'Remember Me' => 'Pamiętaj mnie', 'Creation date' => 'Data utworzenia', - // 'Filter by user' => '', - // 'Filter by due date' => '', - // 'Everybody' => '', - // 'Open' => '', - // 'Closed' => '', - // 'Search' => '', - // 'Nothing found.' => '', - // 'Search in the project "%s"' => '', - // 'Due date' => '', - // 'Others formats accepted: %s and %s' => '', + 'Filter by user' => 'Filtruj według użytkowników', + 'Filter by due date' => 'Filtruj według terminów', + 'Everybody' => 'Wszyscy', + 'Open' => 'Otwarto', + 'Closed' => 'Zamknięto', + 'Search' => 'Szukaj', + 'Nothing found.' => 'Nic nie znaleziono', + 'Search in the project "%s"' => 'Szukaj w projekcie "%s"', + 'Due date' => 'Termin', + 'Others formats accepted: %s and %s' => 'Inne akceptowane formaty: %s and %s', 'Description' => 'Opis', - // '%d comments' => '', - // '%d comment' => '', - // 'Email address invalid' => '', - // 'Your Google Account is not linked anymore to your profile.' => '', - // 'Unable to unlink your Google Account.' => '', - // 'Google authentication failed' => '', - // 'Unable to link your Google Account.' => '', - // 'Your Google Account is linked to your profile successfully.' => '', - // 'Email' => '', - // 'Link my Google Account' => '', - // 'Unlink my Google Account' => '', - // 'Login with my Google Account' => '', - // 'Project not found.' => '', - // 'Task #%d' => '', - // 'Task removed successfully.' => '', - // 'Unable to remove this task.' => '', - // 'Remove a task' => '', - // 'Do you really want to remove this task: "%s"?' => '', - // 'Assign automatically a color based on a category' => '', - // 'Assign automatically a category based on a color' => '', - // 'Task creation or modification' => '', - // 'Category' => '', - // 'Category:' => '', - // 'Categories' => '', - // 'Category not found.' => '', - // 'Your category have been created successfully.' => '', - // 'Unable to create your category.' => '', - // 'Your category have been updated successfully.' => '', - // 'Unable to update your category.' => '', - // 'Remove a category' => '', - // 'Category removed successfully.' => '', - // 'Unable to remove this category.' => '', - // 'Category modification for the project "%s"' => '', - // 'Category Name' => '', - // 'Categories for the project "%s"' => '', - // 'Add a new category' => '', - // 'Do you really want to remove this category: "%s"?' => '', - // 'Filter by category' => '', - // 'All categories' => '', - // 'No category' => '', - // 'The name is required' => '', - // 'Remove a file' => '', - // 'Unable to remove this file.' => '', - // 'File removed successfully.' => '', - // 'Attach a document' => '', - // 'Do you really want to remove this file: "%s"?' => '', - // 'open' => '', - // 'Attachments' => '', - // 'Edit the task' => '', - // 'Edit the description' => '', - // 'Add a comment' => '', - // 'Edit a comment' => '', - // 'Summary' => '', - // 'Time tracking' => '', - // 'Estimate:' => '', - // 'Spent:' => '', - // 'Do you really want to remove this sub-task?' => '', - // 'Remaining:' => '', - // 'hours' => '', - // 'spent' => '', - // 'estimated' => '', - // 'Sub-Tasks' => '', - // 'Add a sub-task' => '', - // 'Original estimate' => '', - // 'Create another sub-task' => '', - // 'Time spent' => '', - // 'Edit a sub-task' => '', - // 'Remove a sub-task' => '', - // 'The time must be a numeric value' => '', - // 'Todo' => '', - // 'In progress' => '', - // 'Sub-task removed successfully.' => '', - // 'Unable to remove this sub-task.' => '', - // 'Sub-task updated successfully.' => '', - // 'Unable to update your sub-task.' => '', - // 'Unable to create your sub-task.' => '', - // 'Sub-task added successfully.' => '', - // 'Maximum size: ' => '', - // 'Unable to upload the file.' => '', - // 'Display another project' => '', - // 'Your GitHub account was successfully linked to your profile.' => '', - // 'Unable to link your GitHub Account.' => '', - // 'GitHub authentication failed' => '', - // 'Your GitHub account is no longer linked to your profile.' => '', - // 'Unable to unlink your GitHub Account.' => '', - // 'Login with my GitHub Account' => '', - // 'Link my GitHub Account' => '', - // 'Unlink my GitHub Account' => '', - // 'Created by %s' => '', - // 'Last modified on %B %e, %Y at %k:%M %p' => '', - // 'Tasks Export' => '', - // 'Tasks exportation for "%s"' => '', - // 'Start Date' => '', - // 'End Date' => '', - // 'Execute' => '', - // 'Task Id' => '', - // 'Creator' => '', - // 'Modification date' => '', - // 'Completion date' => '', - // 'Webhook URL for task creation' => '', - // 'Webhook URL for task modification' => '', - // 'Clone' => '', - // 'Clone Project' => '', - // 'Project cloned successfully.' => '', - // 'Unable to clone this project.' => '', - // 'Email notifications' => '', - // 'Enable email notifications' => '', - // 'Task position:' => '', - // 'The task #%d have been opened.' => '', - // 'The task #%d have been closed.' => '', - // 'Sub-task updated' => '', - // 'Title:' => '', + '%d comments' => '%d Komentarzy', + '%d comment' => '%d Komentarz', + 'Email address invalid' => 'Błędny adres email', + 'Your Google Account is not linked anymore to your profile.' => 'Twoje konto Google nie jest już połączone', + 'Unable to unlink your Google Account.' => 'Nie można odłączyć konta Google', + 'Google authentication failed' => 'Autentykacja Google nieudana', + 'Unable to link your Google Account.' => 'Nie można podłączyć konta Google', + 'Your Google Account is linked to your profile successfully.' => 'Podłączanie konta Google ukończone pomyślnie', + 'Email' => 'Email', + 'Link my Google Account' => 'Połącz z kontem Google', + 'Unlink my Google Account' => 'Rozłącz z kontem Google', + 'Login with my Google Account' => 'Zaloguj przy pomocy konta Google', + 'Project not found.' => 'Projek nieznaleziony.', + 'Task #%d' => 'Zadanie #%d', + 'Task removed successfully.' => 'Zadanie usunięto pomyślnie.', + 'Unable to remove this task.' => 'Nie można usunąć tego zadania.', + 'Remove a task' => 'Usuń zadanie', + 'Do you really want to remove this task: "%s"?' => 'Czy na pewno chcesz usunąć zadanie "%s"?', + 'Assign automatically a color based on a category' => 'Przypisz kolor automatycznie na podstawie kategori', + 'Assign automatically a category based on a color' => 'Przypisz kategorię automatycznie na podstawie koloru', + 'Task creation or modification' => 'Tworzenie lub usuwanie zadania', + 'Category' => 'Kategoria', + 'Category:' => 'Kategoria:', + 'Categories' => 'Kategorie', + 'Category not found.' => 'Kategoria nie istnieje', + 'Your category have been created successfully.' => 'Pomyślnie utworzono kategorię.', + 'Unable to create your category.' => 'Nie można tworzyć kategorii.', + 'Your category have been updated successfully.' => 'Pomyślnie zaktualizowano kategorię', + 'Unable to update your category.' => 'Nie można zaktualizować kategorii', + 'Remove a category' => 'Usuń kategorię', + 'Category removed successfully.' => 'Pomyślnie usunięto kategorię.', + 'Unable to remove this category.' => 'Nie można usunąć tej kategorii.', + 'Category modification for the project "%s"' => 'Zmiania kategorii projektu "%s"', + 'Category Name' => 'Nazwa kategorii', + 'Categories for the project "%s"' => 'Kategorie projektu', + 'Add a new category' => 'Utwórz nową kategorię', + 'Do you really want to remove this category: "%s"?' => 'Czy na pewno chcesz usunąć kategorię: "%s"?', + 'Filter by category' => 'Filtruj według kategorii', + 'All categories' => 'Wszystkie kategorie', + 'No category' => 'Brak kategorii', + 'The name is required' => 'Nazwa jest wymagana', + 'Remove a file' => 'Usuń plik', + 'Unable to remove this file.' => 'Nie można usunąć tego pliku.', + 'File removed successfully.' => 'Plik Usunięty pomyślnie.', + 'Attach a document' => 'Dołącz plik', + 'Do you really want to remove this file: "%s"?' => 'Czy na pewno chcesz usunąć plik: "%s"?', + 'open' => 'otwórz', + 'Attachments' => 'Załączniki', + 'Edit the task' => 'Edytuj Zadanie', + 'Edit the description' => 'Edytuj opis', + 'Add a comment' => 'Dodaj komentarz', + 'Edit a comment' => 'Edytuj komentarz', + 'Summary' => 'Podsumowanie', + 'Time tracking' => 'Śledzenie czasu', + 'Estimate:' => 'Szacowany:', + 'Spent:' => 'Przeznaczony:', + 'Do you really want to remove this sub-task?' => 'Czy na pewno chcesz usunąć to pod-zadanie?', + 'Remaining:' => 'Pozostało:', + 'hours' => 'godzin', + 'spent' => 'przeznaczono', + 'estimated' => 'szacowany', + 'Sub-Tasks' => 'Pod-zadanie', + 'Add a sub-task' => 'Dodaj pod-zadanie', + 'Original estimate' => 'Szacowanie początkowe', + 'Create another sub-task' => 'Dodaj kolejne pod-zadanie', + 'Time spent' => 'Przeznaczony czas', + 'Edit a sub-task' => 'Edytuj pod-zadanie', + 'Remove a sub-task' => 'Usuń pod-zadanie', + 'The time must be a numeric value' => 'Czas musi być wartością liczbową', + 'Todo' => 'Do zrobienia', + 'In progress' => 'W trakcie', + 'Sub-task removed successfully.' => 'Pod-zadanie usunięte pomyślnie.', + 'Unable to remove this sub-task.' => 'Nie można usunąć tego pod-zadania.', + 'Sub-task updated successfully.' => 'Pod-zadanie zaktualizowane pomyślnie.', + 'Unable to update your sub-task.' => 'Nie można zaktalizować tego pod-zadania.', + 'Unable to create your sub-task.' => 'Nie można utworzyć tego pod-zadania.', + 'Sub-task added successfully.' => 'Pod-zadanie utworzone pomyślnie', + 'Maximum size: ' => 'Maksymalny rozmiar: ', + 'Unable to upload the file.' => 'Nie można wczytać pliku.', + 'Display another project' => 'Wyświetl inny projekt', + 'Your GitHub account was successfully linked to your profile.' => 'Konto Github podłączone pomyślnie.', + 'Unable to link your GitHub Account.' => 'Nie można połączyć z kontem Github.', + 'GitHub authentication failed' => 'Autentykacja Github nieudana', + 'Your GitHub account is no longer linked to your profile.' => 'Konto Github nie jest już podłączone do twojego profilu.', + 'Unable to unlink your GitHub Account.' => 'Nie można odłączyć konta Github.', + 'Login with my GitHub Account' => 'Zaloguj przy użyciu konta Github', + 'Link my GitHub Account' => 'Podłącz konto Github', + 'Unlink my GitHub Account' => 'Odłącz konto Github', + 'Created by %s' => 'Utworzone przez %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Ostatnio zmienione %e %B %Y o %k:%M', + 'Tasks Export' => 'Eksport zadań', + 'Tasks exportation for "%s"' => 'Eksport zadań dla "%s"', + 'Start Date' => 'Data początkowa', + 'End Date' => 'Data Końcowa', + 'Execute' => 'Wykonaj', + 'Task Id' => 'Identyfikator Zadania', + 'Creator' => 'Autor', + 'Modification date' => 'Data modyfyfikacji', + 'Completion date' => 'Data ukończenia', + 'Webhook URL for task creation' => 'Webhook URL do tworzenia zadań', + 'Webhook URL for task modification' => 'Webhook URL do modyfikacji zadań', + 'Clone' => 'Sklonuj', + 'Clone Project' => 'Sklonuj projekt', + 'Project cloned successfully.' => 'Projekt sklonowany pomyślnie.', + 'Unable to clone this project.' => 'Nie można sklonować projektu.', + 'Email notifications' => 'Powiadomienia email', + 'Enable email notifications' => 'Włącz powiadomienia email', + 'Task position:' => 'Pozycja zadania:', + 'The task #%d have been opened.' => 'Zadania #%d zostały otwarte.', + 'The task #%d have been closed.' => 'Zadania #$d zostały zamknięte.', + 'Sub-task updated' => 'Pod-zadanie zaktualizowane', + 'Title:' => 'Tytuł:', // 'Status:' => '', - // 'Assignee:' => '', - // 'Time tracking:' => '', - // 'New sub-task' => '', - // 'New attachment added "%s"' => '', - // 'Comment updated' => '', - // 'New comment posted by %s' => '', - // 'List of due tasks for the project "%s"' => '', - // '[%s][New attachment] %s (#%d)' => '', - // '[%s][New comment] %s (#%d)' => '', - // '[%s][Comment updated] %s (#%d)' => '', - // '[%s][New subtask] %s (#%d)' => '', - // '[%s][Subtask updated] %s (#%d)' => '', - // '[%s][New task] %s (#%d)' => '', - // '[%s][Task updated] %s (#%d)' => '', - // '[%s][Task closed] %s (#%d)' => '', - // '[%s][Task opened] %s (#%d)' => '', - // '[%s][Due tasks]' => '', - // '[Kanboard] Notification' => '', - // 'I want to receive notifications only for those projects:' => '', - // 'view the task on Kanboard' => '', - // 'Public access' => '', - // 'Category management' => '', - // 'User management' => '', - // 'Active tasks' => '', - // 'Disable public access' => '', - // 'Enable public access' => '', - // 'Active projects' => '', - // 'Inactive projects' => '', - // 'Public access disabled' => '', - // 'Do you really want to disable this project: "%s"?' => '', - // 'Do you really want to duplicate this project: "%s"?' => '', - // 'Do you really want to enable this project: "%s"?' => '', - // 'Project activation' => '', - // 'Move the task to another project' => '', - // 'Move to another project' => '', - // 'Do you really want to duplicate this task?' => '', - // 'Duplicate a task' => '', - // 'External accounts' => '', - // 'Account type' => '', - // 'Local' => '', - // 'Remote' => '', - // 'Enabled' => '', - // 'Disabled' => '', - // 'Google account linked' => '', - // 'Github account linked' => '', - // 'Username:' => '', - // 'Name:' => '', - // 'Email:' => '', - // 'Default project:' => '', - // 'Notifications:' => '', - // 'Notifications' => '', - // 'Group:' => '', - // 'Regular user' => '', - // 'Account type:' => '', - // 'Edit profile' => '', - // 'Change password' => '', - // 'Password modification' => '', - // 'External authentications' => '', - // 'Google Account' => '', - // 'Github Account' => '', - // 'Never connected.' => '', - // 'No account linked.' => '', - // 'Account linked.' => '', - // 'No external authentication enabled.' => '', - // 'Password modified successfully.' => '', - // 'Unable to change the password.' => '', - // 'Change category for the task "%s"' => '', - // 'Change category' => '', - // '%s updated the task %s' => '', - // '%s opened the task %s' => '', - // '%s moved the task %s to the position #%d in the column "%s"' => '', - // '%s moved the task %s to the column "%s"' => '', - // '%s created the task %s' => '', - // '%s closed the task %s' => '', - // '%s created a subtask for the task %s' => '', - // '%s updated a subtask for the task %s' => '', - // 'Assigned to %s with an estimate of %s/%sh' => '', - // 'Not assigned, estimate of %sh' => '', - // '%s updated a comment on the task %s' => '', - // '%s commented the task %s' => '', - // '%s\'s activity' => '', - // 'No activity.' => '', - // 'RSS feed' => '', - // '%s updated a comment on the task #%d' => '', - // '%s commented on the task #%d' => '', - // '%s updated a subtask for the task #%d' => '', - // '%s created a subtask for the task #%d' => '', - // '%s updated the task #%d' => '', - // '%s created the task #%d' => '', - // '%s closed the task #%d' => '', - // '%s open the task #%d' => '', - // '%s moved the task #%d to the column "%s"' => '', - // '%s moved the task #%d to the position %d in the column "%s"' => '', - // 'Activity' => '', - // 'Default values are "%s"' => '', - // 'Default columns for new projects (Comma-separated)' => '', - // 'Task assignee change' => '', - // '%s change the assignee of the task #%d to %s' => '', - // '%s changed the assignee of the task %s to %s' => '', - // '[%s][Column Change] %s (#%d)' => '', - // '[%s][Position Change] %s (#%d)' => '', - // '[%s][Assignee Change] %s (#%d)' => '', - // 'New password for the user "%s"' => '', - // 'Choose an event' => '', + 'Assignee:' => 'Przypisano do:', + 'Time tracking:' => 'Śledzenie czasu: ', + 'New sub-task' => 'Nowe Pod-zadanie', + 'New attachment added "%s"' => 'Nowy załącznik dodany "%s"', + 'Comment updated' => 'Komentarz zaktualizowany', + 'New comment posted by %s' => 'Nowy komentarz dodany przez %s', + 'List of due tasks for the project "%s"' => 'Lista zadań oczekujących projektu "%s"', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + // 'Task closed' => '', + // 'Task opened' => '', + '[%s][Due tasks]' => '[%s][Zadania oczekujące]', + '[Kanboard] Notification' => '[Kanboard] Powiadomienie', + 'I want to receive notifications only for those projects:' => 'Chcę otrzymywaćpowiadiomienia tylko dla tych projektów:', + 'view the task on Kanboard' => 'Zobacz zadanie', + 'Public access' => 'Dostęp publiczny', + 'Category management' => 'Zarządzanie kategoriami', + 'User management' => 'Zarządzanie użytkownikami', + 'Active tasks' => 'Aktywne zadania', + 'Disable public access' => 'Zablokuj dostęp publiczny', + 'Enable public access' => 'Odblokuj dostęp publiczny', + 'Active projects' => 'Aktywne projety', + 'Inactive projects' => 'Nieaktywne projekty', + 'Public access disabled' => 'Dostęp publiczny zablokowany', + 'Do you really want to disable this project: "%s"?' => 'Czy napewno chcesz zablokować projekt: "%s"?', + 'Do you really want to duplicate this project: "%s"?' => 'Czy napewno chcesz zduplikować projekt: "%s"?', + 'Do you really want to enable this project: "%s"?' => 'Czy napewno chcesz odblokować projekt: "%s"?', + 'Project activation' => 'Aktywacja projekt', + 'Move the task to another project' => 'Przenieś zadanie do innego projektu', + 'Move to another project' => 'Przenieś do innego projektu', + 'Do you really want to duplicate this task?' => 'Czy napewno chcesz zduplikować to zadanie: "%s"?', + 'Duplicate a task' => 'Zduplikuj zadanie', + 'External accounts' => 'Konta zewnętrzne', + 'Account type' => 'Typ konta', + 'Local' => 'Lokalne', + 'Remote' => 'Zdalne', + 'Enabled' => 'Odblokowane', + 'Disabled' => 'Zablokowane', + 'Google account linked' => 'Połączone konto Google', + 'Github account linked' => 'Połączone konto Github', + 'Username:' => 'Nazwa Użytkownika:', + 'Name:' => 'Imię i Nazwisko', + 'Email:' => 'Email: ', + 'Default project:' => 'Projekt domyślny:', + 'Notifications:' => 'Powiadomienia: ', + 'Notifications' => 'Powiadomienia', + 'Group:' => 'Grupa:', + 'Regular user' => 'Zwykły użytkownik', + 'Account type:' => 'Typ konta:', + 'Edit profile' => 'Edytuj profil', + 'Change password' => 'Zmień hasło', + 'Password modification' => 'Zmiania hasła', + 'External authentications' => 'Autentykacja zewnętrzna', + 'Google Account' => 'Konto Google', + 'Github Account' => 'Konto Github', + 'Never connected.' => 'Nigdy nie połączone.', + 'No account linked.' => 'Brak połączonych kont.', + 'Account linked.' => 'Konto połączone.', + 'No external authentication enabled.' => 'Brak autentykacji zewnętrznych.', + 'Password modified successfully.' => 'Hasło zmienione pomyślne.', + 'Unable to change the password.' => 'Nie można zmienić hasła.', + 'Change category for the task "%s"' => 'Zmień kategorię dla zadania "%s"', + 'Change category' => 'Zmień kategorię', + '%s updated the task %s' => '%s zaktualizował zadanie %s', + '%s opened the task %s' => '%s otworzył zadanie %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s przeniósł zadanie %s na pozycję #%d w kolumnie "%s"', + '%s moved the task %s to the column "%s"' => '%s przeniósł zadanie %s do kolumny "%s"', + '%s created the task %s' => '%s utworzył zadanie %s', + '%s closed the task %s' => '%s zamknął zadanie %s', + '%s created a subtask for the task %s' => '%s utworzył pod-zadanie dla zadania %s', + '%s updated a subtask for the task %s' => '%s zaktualizował pod-zadanie dla zadania %s', + 'Assigned to %s with an estimate of %s/%sh' => 'Przypisano do %s z szacowanym czasem wykonania %s/%sh', + 'Not assigned, estimate of %sh' => 'Nie przypisane, szacowany czas wykonania %sh', + '%s updated a comment on the task %s' => '%s zaktualizował komentarz do zadania %s', + '%s commented the task %s' => '%s skomentował zadanie %s', + '%s\'s activity' => 'Aktywność %s', + 'No activity.' => 'Brak aktywności.', + 'RSS feed' => 'Kanał RSS', + '%s updated a comment on the task #%d' => '%s zaktualizował komentarz do zadania #%d', + '%s commented on the task #%d' => '%s skomentował zadanie #%d', + '%s updated a subtask for the task #%d' => '%s zaktualizował pod-zadanie dla zadania #%d', + '%s created a subtask for the task #%d' => '%s utworzył pod-zadanie dla zadania #%d', + '%s updated the task #%d' => '%s zaktualizował zadanie #%d', + '%s created the task #%d' => '%s utworzył zadanie #%d', + '%s closed the task #%d' => '%s zamknął zadanie #%d', + '%s open the task #%d' => '%s otworzył zadanie #%d', + '%s moved the task #%d to the column "%s"' => '%s przeniósł zadanie #%d do kolumny "%s"', + '%s moved the task #%d to the position %d in the column "%s"' => '%s przeniósł zadanie #%d na pozycję %d w kolmnie "%s"', + 'Activity' => 'Aktywność', + 'Default values are "%s"' => 'Domyślne wartości: "%s"', + 'Default columns for new projects (Comma-separated)' => 'Domyślne kolmny dla nowych projektów (oddzielone przecinkiem)', + 'Task assignee change' => 'Zmień osobę odpowiedzialną', + '%s change the assignee of the task #%d to %s' => '%s zmienił osobę odpowiedzialną za zadanie #%d na %s', + '%s changed the assignee of the task %s to %s' => '%s zmienił osobę odpowiedzialną za zadanie %s na %s', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', + 'New password for the user "%s"' => 'Nowe hasło użytkownika "%s"', + 'Choose an event' => 'Wybierz zdarzenie', // 'Github commit received' => '', // 'Github issue opened' => '', // 'Github issue closed' => '', // 'Github issue reopened' => '', // 'Github issue assignee change' => '', // 'Github issue label change' => '', - // 'Create a task from an external provider' => '', - // 'Change the assignee based on an external username' => '', - // 'Change the category based on an external label' => '', + 'Create a task from an external provider' => 'Utwórz zadanie z dostawcy zewnętrznego', + 'Change the assignee based on an external username' => 'Zmień osobę odpowiedzialną na podstawie zewnętrznej nazwy użytkownika', + 'Change the category based on an external label' => 'Zmień kategorię na podstawie zewnętrzenj etykiety', // 'Reference' => '', // 'Reference: %s' => '', - // 'Label' => '', - // 'Database' => '', - // 'About' => '', - // 'Database driver:' => '', - // 'Board settings' => '', - // 'URL and token' => '', + 'Label' => 'Etykieta', + 'Database' => 'Baza danych', + 'About' => 'Informacje', + 'Database driver:' => 'Silnik bazy danych:', + 'Board settings' => 'Ustawienia tablicy', + 'URL and token' => 'URL i token', // 'Webhook settings' => '', - // 'URL for task creation:' => '', - // 'Reset token' => '', + 'URL for task creation:' => 'URL do tworzenia zadań', + 'Reset token' => 'Resetuj token', // 'API endpoint:' => '', - // 'Refresh interval for private board' => '', - // 'Refresh interval for public board' => '', - // 'Task highlight period' => '', - // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '', - // 'Frequency in second (60 seconds by default)' => '', - // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '', - // 'Application URL' => '', - // 'Example: http://example.kanboard.net/ (used by email notifications)' => '', - // 'Token regenerated.' => '', - // 'Date format' => '', - // 'ISO format is always accepted, example: "%s" and "%s"' => '', - // 'New private project' => '', - // 'This project is private' => '', - // 'Type here to create a new sub-task' => '', - // 'Add' => '', - // 'Estimated time: %s hours' => '', - // 'Time spent: %s hours' => '', - // 'Started on %B %e, %Y' => '', - // 'Start date' => '', - // 'Time estimated' => '', - // 'There is nothing assigned to you.' => '', - // 'My tasks' => '', - // 'Activity stream' => '', - // 'Dashboard' => '', - // 'Confirmation' => '', - // 'Allow everybody to access to this project' => '', - // 'Everybody have access to this project.' => '', + 'Refresh interval for private board' => 'Częstotliwość odświerzania dla tablicy prywatnej', + 'Refresh interval for public board' => 'Częstotliwość odświerzania dla tablicy publicznej', + 'Task highlight period' => 'Okres wyróżniania zadań', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Okres (w sekundach) wymagany do uznania projektu za niedawno zmieniony (0 ab zablokować, domyślnie 2 dni)', + 'Frequency in second (60 seconds by default)' => 'Częstotliwosć w sekundach (domyślnie 60 sekund)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Częstotliwość w sekundach (0 aby zablokować, domyślnie 10 sekund)', + 'Application URL' => 'Adres URL aplikacji', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Przykład: http://example.kanboard.net/ (Używane przez powiadomienia email)', + 'Token regenerated.' => 'Token wygenerowany ponownie.', + 'Date format' => 'Format daty', + 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO jest zawsze akceptowany, przykłady: "%s", "%s"', + 'New private project' => 'Nowy projekt prywatny', + 'This project is private' => 'Ten projekt jest prywatny', + 'Type here to create a new sub-task' => 'Wpisz tutaj aby utworzyć pod-zadanie', + 'Add' => 'Dodaj', + 'Estimated time: %s hours' => 'Szacowany czas: %s godzin', + 'Time spent: %s hours' => 'Przeznaczony czas: %s godzin', + 'Started on %B %e, %Y' => 'Rozpoczęto %e %B %Y', + 'Start date' => 'Data rozpoczęcia', + 'Time estimated' => 'Szacowany czas', + 'There is nothing assigned to you.' => 'Nie ma przypisanych zadań', + 'My tasks' => 'Moje zadania', + 'Activity stream' => 'Dziennik aktywności', + 'Dashboard' => 'Panel', + 'Confirmation' => 'Potwierdzenie', + 'Allow everybody to access to this project' => 'Udostepnij ten projekt wszystkim', + 'Everybody have access to this project.' => 'Wszyscy mają dostęp do tego projektu.', // 'Webhooks' => '', // 'API' => '', - // 'Integration' => '', + 'Integration' => 'Integracja', // 'Github webhooks' => '', // 'Help on Github webhooks' => '', - // 'Create a comment from an external provider' => '', + 'Create a comment from an external provider' => 'Utwórz komentarz od zewnętrznego dostawcy', // 'Github issue comment created' => '', - // 'Configure' => '', - // 'Project management' => '', - // 'My projects' => '', - // 'Columns' => '', - // 'Task' => '', - // 'Your are not member of any project.' => '', - // 'Percentage' => '', - // 'Number of tasks' => '', - // 'Task distribution' => '', - // 'Reportings' => '', - // 'Task repartition for "%s"' => '', - // 'Analytics' => '', - // 'Subtask' => '', - // 'My subtasks' => '', - // 'User repartition' => '', - // 'User repartition for "%s"' => '', - // 'Clone this project' => '', - // 'Column removed successfully.' => '', - // 'Edit Project' => '', + 'Configure' => 'Konfiguruj', + 'Project management' => 'Menadżer projektu', + 'My projects' => 'Moje projekty', + 'Columns' => 'Kolumny', + 'Task' => 'zadania', + 'Your are not member of any project.' => 'Nie bierzesz udziału w żadnym projekcie', + 'Percentage' => 'Procent', + 'Number of tasks' => 'Liczba zadań', + 'Task distribution' => 'Rozmieszczenie zadań', + 'Reportings' => 'Raporty', + 'Task repartition for "%s"' => 'Przydział zadań dla "%s"', + 'Analytics' => 'Analizy', + 'Subtask' => 'Pod-zadanie', + 'My subtasks' => 'Moje pod-zadania', + 'User repartition' => 'Przydział użytkownika', + 'User repartition for "%s"' => 'Przydział użytkownika dla "%s"', + 'Clone this project' => 'Sklonuj ten projekt', + 'Column removed successfully.' => 'Kolumna usunięta pomyslnie.', + 'Edit Project' => 'Edytuj projekt', // 'Github Issue' => '', - // 'Not enough data to show the graph.' => '', - // 'Previous' => '', - // 'The id must be an integer' => '', - // 'The project id must be an integer' => '', - // 'The status must be an integer' => '', - // 'The subtask id is required' => '', - // 'The subtask id must be an integer' => '', - // 'The task id is required' => '', - // 'The task id must be an integer' => '', - // 'The user id must be an integer' => '', - // 'This value is required' => '', - // 'This value must be numeric' => '', - // 'Unable to create this task.' => '', - // 'Cumulative flow diagram' => '', - // 'Cumulative flow diagram for "%s"' => '', - // 'Daily project summary' => '', - // 'Daily project summary export' => '', - // 'Daily project summary export for "%s"' => '', - // 'Exports' => '', - // 'This export contains the number of tasks per column grouped per day.' => '', - // 'Nothing to preview...' => '', - // 'Preview' => '', - // 'Write' => '', - // 'Active swimlanes' => '', - // 'Add a new swimlane' => '', - // 'Change default swimlane' => '', - // 'Default swimlane' => '', - // 'Do you really want to remove this swimlane: "%s"?' => '', - // 'Inactive swimlanes' => '', - // 'Set project manager' => '', - // 'Set project member' => '', - // 'Remove a swimlane' => '', - // 'Rename' => '', - // 'Show default swimlane' => '', - // 'Swimlane modification for the project "%s"' => '', - // 'Swimlane not found.' => '', - // 'Swimlane removed successfully.' => '', - // 'Swimlanes' => '', - // 'Swimlane updated successfully.' => '', - // 'The default swimlane have been updated successfully.' => '', - // 'Unable to create your swimlane.' => '', - // 'Unable to remove this swimlane.' => '', - // 'Unable to update this swimlane.' => '', - // 'Your swimlane have been created successfully.' => '', - // 'Example: "Bug, Feature Request, Improvement"' => '', - // 'Default categories for new projects (Comma-separated)' => '', + 'Not enough data to show the graph.' => 'Za mało danych do utworzenia wykresu.', + 'Previous' => 'Poprzedni', + 'The id must be an integer' => 'ID musi być liczbą całkowitą', + 'The project id must be an integer' => 'ID projektu musi być liczbą całkowitą', + 'The status must be an integer' => 'Status musi być liczbą całkowitą', + 'The subtask id is required' => 'ID pod-zadanie jest wymagane', + 'The subtask id must be an integer' => 'ID pod-zadania musi być liczbą całkowitą', + 'The task id is required' => 'ID zadania jest wymagane', + 'The task id must be an integer' => 'ID zadania musi być liczbą całkowitą', + 'The user id must be an integer' => 'ID użytkownika musi być liczbą całkowitą', + 'This value is required' => 'Wymagana wartość', + 'This value must be numeric' => 'Wartość musi być liczbą', + 'Unable to create this task.' => 'Nie można tworzyć zadania.', + 'Cumulative flow diagram' => 'Zbiorowy diagram przepływu', + 'Cumulative flow diagram for "%s"' => 'Zbiorowy diagram przepływu dla "%s"', + 'Daily project summary' => 'Dzienne podsumowanie projektu', + 'Daily project summary export' => 'Eksport dziennego podsumowania projektu', + 'Daily project summary export for "%s"' => 'Eksport dziennego podsumowania projektu dla "%s"', + 'Exports' => 'Eksporty', + 'This export contains the number of tasks per column grouped per day.' => 'Ten eksport zawiera ilość zadań zgrupowanych w kolumnach na dzień', + 'Nothing to preview...' => 'Nic do podejrzenia...', + 'Preview' => 'Podgląd', + 'Write' => 'Edycja', + 'Active swimlanes' => 'Aktywne procesy', + 'Add a new swimlane' => 'Dodaj proces', + 'Change default swimlane' => 'Zmień domyślny proces', + 'Default swimlane' => 'Domyślny proces', + 'Do you really want to remove this swimlane: "%s"?' => 'Czy na pewno chcesz usunąć proces: "%s"?', + 'Inactive swimlanes' => 'Nieaktywne procesy', + 'Set project manager' => 'Ustaw menadżera projektu', + 'Set project member' => 'Ustaw członka projektu', + 'Remove a swimlane' => 'Usuń proces', + 'Rename' => 'Zmień nazwe', + 'Show default swimlane' => 'Pokaż domyślny proces', + 'Swimlane modification for the project "%s"' => 'Edycja procesów dla projektu "%s"', + 'Swimlane not found.' => 'Nie znaleziono procesu.', + 'Swimlane removed successfully.' => 'Proces usunięty pomyslnie.', + 'Swimlanes' => 'Procesy', + 'Swimlane updated successfully.' => 'Proces zaktualizowany pomyślnie.', + 'The default swimlane have been updated successfully.' => 'Domyślny proces zaktualizowany pomyślnie.', + 'Unable to create your swimlane.' => 'Nie można utworzyć procesu.', + 'Unable to remove this swimlane.' => 'Nie można usunąć procesu.', + 'Unable to update this swimlane.' => 'Nie można zaktualizować procesu.', + 'Your swimlane have been created successfully.' => 'Proces tworzony pomyślnie.', + 'Example: "Bug, Feature Request, Improvement"' => 'Przykład: "Błąd, Żądanie Funkcjonalnośći, Udoskonalenia"', + 'Default categories for new projects (Comma-separated)' => 'Domyślne kategorie dla nowych projektów (oddzielone przecinkiem)', // 'Gitlab commit received' => '', // 'Gitlab issue opened' => '', // 'Gitlab issue closed' => '', // 'Gitlab webhooks' => '', // 'Help on Gitlab webhooks' => '', - // 'Integrations' => '', - // 'Integration with third-party services' => '', - // 'Role for this project' => '', - // 'Project manager' => '', - // 'Project member' => '', - // 'A project manager can change the settings of the project and have more privileges than a standard user.' => '', + 'Integrations' => 'Integracje', + 'Integration with third-party services' => 'Integracja z usługami firm trzecich', + 'Role for this project' => 'Rola w tym projekcie', + 'Project manager' => 'Manadżer projektu', + 'Project member' => 'Członek projektu', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Menadżer projektu może zmieniać ustawienia projektu i posiada większe uprawnienia od zwykłego użytkownika', // 'Gitlab Issue' => '', - // 'Subtask Id' => '', - // 'Subtasks' => '', - // 'Subtasks Export' => '', - // 'Subtasks exportation for "%s"' => '', - // 'Task Title' => '', - // 'Untitled' => '', - // 'Application default' => '', - // 'Language:' => '', - // 'Timezone:' => '', - // 'Next' => '', + 'Subtask Id' => 'ID pod-zadania', + 'Subtasks' => 'Pod-zadania', + 'Subtasks Export' => 'Eksport pod-zadań', + 'Subtasks exportation for "%s"' => 'Eksporty pod-zadań dla "%s"', + 'Task Title' => 'Tytuł zadania', + 'Untitled' => 'Bez tytułu', + 'Application default' => 'Domyślne dla aplikacji', + 'Language:' => 'Język:', + 'Timezone:' => 'Strefa czasowa:', + 'All columns' => 'Wszystkie kolumny', + 'Calendar for "%s"' => 'Kalendarz dla "%s"', + 'Filter by column' => 'Filtrj według kolumn', + 'Filter by status' => 'Filtruj według statusu', + 'Calendar' => 'Kalendarz', + 'Next' => 'Następny', + // '#%d' => '', + 'Filter by color' => 'Filtruj według koloru', + 'Filter by swimlane' => 'Filtruj według procesu', + 'All swimlanes' => 'Wszystkie procesy', + 'All colors' => 'Wszystkie kolory', + 'All status' => 'Wszystkie statusy', + 'Add a comment logging moving the task between columns' => 'Dodaj komentarz dokumentujący przeniesienie zadania pomiędzy kolumnami', + 'Moved to column %s' => 'Przeniosiono do kolumny %s', + // 'Change description' => '', + // 'User dashboard' => '', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Enable time tracking for subtasks' => '', + // 'Select the new status of the subtask: "%s"' => '', + // 'Subtask timesheet' => '', + // 'There is nothing to show.' => '', + // 'Time Tracking' => '', + // 'You already have one subtask in progress' => '', + // 'Which parts of the project do you want to duplicate?' => '', + // 'Change dashboard view' => '', + // 'Show/hide activities' => '', + // 'Show/hide projects' => '', + // 'Show/hide subtasks' => '', + // 'Show/hide tasks' => '', + // 'Disable login form' => '', + // 'Show/hide calendar' => '', + // 'User calendar' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + // 'Task age in days' => '', + // 'Days in this column' => '', + // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + // 'Expand tasks' => '', + // 'Collapse tasks' => '', + // 'Expand/collapse tasks' => '', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', ); diff --git a/sources/app/Locale/pt_BR/translations.php b/sources/app/Locale/pt_BR/translations.php index f155b04..89f7e29 100644 --- a/sources/app/Locale/pt_BR/translations.php +++ b/sources/app/Locale/pt_BR/translations.php @@ -408,15 +408,13 @@ return array( 'Comment updated' => 'Comentário atualizado', 'New comment posted by %s' => 'Novo comentário postado por %s', 'List of due tasks for the project "%s"' => 'Lista de tarefas pendentes para o projeto "%s"', - '[%s][New attachment] %s (#%d)' => '[%s][Novo anexo] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][Novo comentário] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][Comentário atualizado] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][Nova subtarefa] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][Subtarefa atualizada] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][Nova tarefa] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][Tarefa atualizada] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][Tarefa finalizada] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][Tarefa aberta] %s (#%d)', + 'New attachment' => 'Novo anexo', + 'New comment' => 'Novo comentário', + 'New subtask' => 'Nova subtarefa', + 'Subtask updated' => 'Subtarefa alterada', + 'Task updated' => 'Tarefa alterada', + 'Task closed' => 'Tarefa finalizada', + 'Task opened' => 'Tarefa aberta', '[%s][Due tasks]' => '[%s][Tarefas pendentes]', '[Kanboard] Notification' => '[Kanboard] Notificação', 'I want to receive notifications only for those projects:' => 'Quero receber notificações apenas destes projetos:', @@ -500,9 +498,9 @@ return array( 'Task assignee change' => 'Mudar designação da tarefa', '%s change the assignee of the task #%d to %s' => '%s mudou a designação da tarefa #%d para %s', '%s changed the assignee of the task %s to %s' => '%s mudou a designação da tarefa %s para %s', - '[%s][Column Change] %s (#%d)' => '[%s][Modificou Coluna] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][Modificou Posição] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][Modificou Designação] %s (#%d)', + 'Column Change' => 'Mudança de coluna', + 'Position Change' => 'Mudança de posição', + 'Assignee Change' => 'Mudança de designado', 'New password for the user "%s"' => 'Nova senha para o usuário "%s"', 'Choose an event' => 'Escolher um evento', 'Github commit received' => 'Github commit received', @@ -647,5 +645,93 @@ return array( 'Application default' => 'Aplicação padrão', 'Language:' => 'Idioma', 'Timezone:' => 'Fuso horário', + 'All columns' => 'Todas as colunas', + 'Calendar for "%s"' => 'Calendário para "%s"', + 'Filter by column' => 'Filtrar por coluna', + 'Filter by status' => 'Filtrar por status', + 'Calendar' => 'Calendário', 'Next' => 'Próximo', + // '#%d' => '', + 'Filter by color' => 'Filtrar por cor', + 'Filter by swimlane' => 'Filtrar por swimlane', + 'All swimlanes' => 'Todos os swimlane', + 'All colors' => 'Todas as cores', + 'All status' => 'Todos os status', + 'Add a comment logging moving the task between columns' => 'Adicionar un comentário de log ao mover uma tarefa em outra coluna', + 'Moved to column %s' => 'Mover para a coluna %s', + 'Change description' => 'Modificar a descrição', + 'User dashboard' => 'Painel de Controle do usuário', + 'Allow only one subtask in progress at the same time for a user' => 'Permitir apenas uma subtarefa em andamento ao mesmo tempo para um usuário', + 'Edit column "%s"' => 'Editar a coluna "%s"', + 'Enable time tracking for subtasks' => 'Ativar a gestão de tempo par a subtarefa', + 'Select the new status of the subtask: "%s"' => 'Selecionar um novo status para a subtarefa', + 'Subtask timesheet' => 'Gestão de tempo das subtarefas', + 'There is nothing to show.' => 'Não há nada para mostrar', + 'Time Tracking' => 'Gestão de tempo', + 'You already have one subtask in progress' => 'Você já tem um subtarefa em andamento', + 'Which parts of the project do you want to duplicate?' => 'Quais as partes do projeto você deseja duplicar?', + 'Change dashboard view' => 'Alterar a vista do Painel de Controle', + 'Show/hide activities' => 'Mostrar / ocultar as atividades', + 'Show/hide projects' => 'Mostrar / ocultar os projetos', + 'Show/hide subtasks' => 'Mostrar / ocultar as subtarefas', + 'Show/hide tasks' => 'Mostrar / ocultar as tarefas', + 'Disable login form' => 'Desativar o formulário de login', + 'Show/hide calendar' => 'Mostrar / ocultar calendário', + 'User calendar' => 'Calendário do usuário', + 'Bitbucket commit received' => '"Commit" recebido via Bitbucket', + 'Bitbucket webhooks' => 'Webhook Bitbucket', + 'Help on Bitbucket webhooks' => 'Ajuda sobre os webhooks Bitbucket', + 'Start' => 'Inicio', + 'End' => 'Fim', + 'Task age in days' => 'Idade da tarefa em dias', + 'Days in this column' => 'Dias nesta coluna', + // '%dd' => '', + 'Add a link' => 'Adicionar uma associação', + 'Add a new link' => 'Adicionar uma nova associação', + 'Do you really want to remove this link: "%s"?' => 'Você realmente deseja remover esta associação: "%s"?', + 'Do you really want to remove this link with task #%d?' => 'Você realmente deseja remover esta associação com a tarefa n°%d?', + // 'Field required' => '', + 'Link added successfully.' => 'Associação criada com sucesso.', + 'Link updated successfully.' => 'Associação atualizada com sucesso.', + 'Link removed successfully.' => 'Associação removida com sucesso.', + 'Link labels' => 'Etiquetas das associações', + 'Link modification' => 'Modificação de uma associação', + 'Links' => 'Associações', + 'Link settings' => 'Configuração das associações', + 'Opposite label' => 'Nome da etiqueta oposta', + 'Remove a link' => 'Remover uma associação', + 'Task\'s links' => 'Associações das tarefas', + 'The labels must be different' => 'As etiquetas devem ser diferentes', + 'There is no link.' => 'Não há nenhuma associação.', + 'This label must be unique' => 'Esta etiqueta deve ser unica', + 'Unable to create your link.' => 'Impossível de adicionar sua associação.', + 'Unable to update your link.' => 'Impossível de atualizar sua associação.', + 'Unable to remove this link.' => 'Impossível de remover sua associação.', + 'relates to' => 'é associado com', + 'blocks' => 'blocos', + 'is blocked by' => 'esta bloqueado por', + 'duplicates' => 'duplica', + 'is duplicated by' => 'é duplicado por', + 'is a child of' => 'é um filho de', + 'is a parent of' => 'é um parente do', + 'targets milestone' => 'visa um milestone', + 'is a milestone of' => 'é um milestone de', + 'fixes' => 'corrige', + 'is fixed by' => 'foi corrigido por', + 'This task' => 'Esta tarefa', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + 'Expand tasks' => 'Expandir tarefas', + 'Collapse tasks' => 'Contrair tarefas', + 'Expand/collapse tasks' => 'Expandir/Contrair tarefas', + // 'Close dialog box' => '', + 'Submit a form' => 'Envia o formulário', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + 'Application' => 'Aplicação', + 'Filter recently updated' => 'Filtro recentemente atualizado', + // 'since %B %e, %Y at %k:%M %p' => '', + 'More filters' => 'Mais filtros', ); diff --git a/sources/app/Locale/ru_RU/translations.php b/sources/app/Locale/ru_RU/translations.php index a857de3..02a66b5 100644 --- a/sources/app/Locale/ru_RU/translations.php +++ b/sources/app/Locale/ru_RU/translations.php @@ -408,15 +408,13 @@ return array( 'Comment updated' => 'Комментарий обновлен', 'New comment posted by %s' => 'Новый комментарий написан « %s »', 'List of due tasks for the project "%s"' => 'Список сроков к проекту « %s »', - '[%s][New attachment] %s (#%d)' => '[%s][Новых вложений] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][Новых комментариев] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][Обновленых коментариев] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][Новых подзадач] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][Обновленных подзадач] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][Новых задач] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][Обновленных задач] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][Закрытых задач] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][Открытых задач] %s (#%d)', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + // 'Task closed' => '', + // 'Task opened' => '', '[%s][Due tasks]' => '[%s][Текущие задачи]', '[Kanboard] Notification' => '[Kanboard] Оповещение', 'I want to receive notifications only for those projects:' => 'Я хочу получать уведомления только по этим проектам :', @@ -500,9 +498,9 @@ return array( 'Task assignee change' => 'Изменен назначенный', '%s change the assignee of the task #%d to %s' => '%s сменил назначенного для задачи #%d на %s', '%s changed the assignee of the task %s to %s' => '%s сменил назначенного для задачи %s на %s', - '[%s][Column Change] %s (#%d)' => '[%s][Изменение колонки] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][Изменение позиции] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][Изменение назначеного] %s (#%d)', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', 'New password for the user "%s"' => 'Новый пароль для пользователя %s"', 'Choose an event' => 'Выберите событие', 'Github commit received' => 'Github: коммит получен', @@ -647,5 +645,93 @@ return array( // 'Application default' => '', // 'Language:' => '', // 'Timezone:' => '', + // 'All columns' => '', + // 'Calendar for "%s"' => '', + // 'Filter by column' => '', + // 'Filter by status' => '', + // 'Calendar' => '', // 'Next' => '', + // '#%d' => '', + // 'Filter by color' => '', + // 'Filter by swimlane' => '', + // 'All swimlanes' => '', + // 'All colors' => '', + // 'All status' => '', + // 'Add a comment logging moving the task between columns' => '', + // 'Moved to column %s' => '', + // 'Change description' => '', + // 'User dashboard' => '', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Enable time tracking for subtasks' => '', + // 'Select the new status of the subtask: "%s"' => '', + // 'Subtask timesheet' => '', + // 'There is nothing to show.' => '', + // 'Time Tracking' => '', + // 'You already have one subtask in progress' => '', + // 'Which parts of the project do you want to duplicate?' => '', + // 'Change dashboard view' => '', + // 'Show/hide activities' => '', + // 'Show/hide projects' => '', + // 'Show/hide subtasks' => '', + // 'Show/hide tasks' => '', + // 'Disable login form' => '', + // 'Show/hide calendar' => '', + // 'User calendar' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + // 'Task age in days' => '', + // 'Days in this column' => '', + // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + // 'Expand tasks' => '', + // 'Collapse tasks' => '', + // 'Expand/collapse tasks' => '', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', ); diff --git a/sources/app/Locale/sv_SE/translations.php b/sources/app/Locale/sv_SE/translations.php index ae805d4..2fcc3ce 100644 --- a/sources/app/Locale/sv_SE/translations.php +++ b/sources/app/Locale/sv_SE/translations.php @@ -408,15 +408,13 @@ return array( 'Comment updated' => 'Kommentaren har uppdaterats', 'New comment posted by %s' => 'Ny kommentar postad av %s', 'List of due tasks for the project "%s"' => 'Lista med uppgifter för projektet "%s"', - '[%s][New attachment] %s (#%d)' => '[%s][Ny bifogning] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][Ny kommentar] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][Uppdaterad kommentar] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][Ny deluppgift] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][Deluppgiften uppdaterad] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][Ny uppgift] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][Uppgiften uppdaterad] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][Uppgiften stängd] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][Uppgiften öppnad] %s (#%d)', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + // 'Task closed' => '', + // 'Task opened' => '', '[%s][Due tasks]' => '[%s][Förfallen uppgift]', '[Kanboard] Notification' => '[Kanboard] Notis', 'I want to receive notifications only for those projects:' => 'Jag vill endast få notiser för dessa projekt:', @@ -500,9 +498,9 @@ return array( 'Task assignee change' => 'Ändra tilldelning av uppgiften', '%s change the assignee of the task #%d to %s' => '%s byt tilldelning av uppgiften #%d till %s', '%s changed the assignee of the task %s to %s' => '%s byt tilldelning av uppgiften %s till %s', - '[%s][Column Change] %s (#%d)' => '[%s][Byt kolumn] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][Byt position] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][Byt tilldelning] %s (#%d)', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', 'New password for the user "%s"' => 'Nytt lösenord för användaren "%s"', 'Choose an event' => 'Välj en händelse', 'Github commit received' => 'Github-bidrag mottaget', @@ -647,5 +645,93 @@ return array( 'Application default' => 'Applikationsstandard', 'Language:' => 'Språk', 'Timezone:' => 'Tidszon', + // 'All columns' => '', + // 'Calendar for "%s"' => '', + // 'Filter by column' => '', + // 'Filter by status' => '', + // 'Calendar' => '', 'Next' => 'Nästa', + // '#%d' => '', + // 'Filter by color' => '', + // 'Filter by swimlane' => '', + // 'All swimlanes' => '', + // 'All colors' => '', + // 'All status' => '', + // 'Add a comment logging moving the task between columns' => '', + // 'Moved to column %s' => '', + // 'Change description' => '', + // 'User dashboard' => '', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Enable time tracking for subtasks' => '', + // 'Select the new status of the subtask: "%s"' => '', + // 'Subtask timesheet' => '', + // 'There is nothing to show.' => '', + // 'Time Tracking' => '', + // 'You already have one subtask in progress' => '', + // 'Which parts of the project do you want to duplicate?' => '', + // 'Change dashboard view' => '', + // 'Show/hide activities' => '', + // 'Show/hide projects' => '', + // 'Show/hide subtasks' => '', + // 'Show/hide tasks' => '', + // 'Disable login form' => '', + // 'Show/hide calendar' => '', + // 'User calendar' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + // 'Task age in days' => '', + // 'Days in this column' => '', + // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + // 'Expand tasks' => '', + // 'Collapse tasks' => '', + // 'Expand/collapse tasks' => '', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', ); diff --git a/sources/app/Locale/th_TH/translations.php b/sources/app/Locale/th_TH/translations.php index f091020..1945f04 100644 --- a/sources/app/Locale/th_TH/translations.php +++ b/sources/app/Locale/th_TH/translations.php @@ -408,15 +408,13 @@ return array( 'Comment updated' => 'ปรับปรุงความคิดเห็น', 'New comment posted by %s' => 'ความคิดเห็นใหม่จาก %s', 'List of due tasks for the project "%s"' => 'รายการงานสำหรับโปรเจค "%s"', - '[%s][New attachment] %s (#%d)' => '[%s][แนบใหม่] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][ความคิดเห็นใหม่] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][ปรับปรุงความคิดเห็น] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][งานย่อยใหม่] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][ปรับปรุงงานย่อย] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][งานใหม่] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][ปรับปรุุงงาน] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][งานที่ปิด] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][งานที่เปิด] %s (#%d)', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + // 'Task closed' => '', + // 'Task opened' => '', '[%s][Due tasks]' => '[%s][งานปัจจุบัน]', '[Kanboard] Notification' => '[Kanboard] แจ้งเตือน', 'I want to receive notifications only for those projects:' => 'ฉันต้องการรับการแจ้งเตือนสำหรับโปรเจค:', @@ -500,9 +498,9 @@ return array( 'Task assignee change' => 'เปลี่ยนการกำหนดบุคคลของงาน', // '%s change the assignee of the task #%d to %s' => '', // '%s changed the assignee of the task %s to %s' => '', - // '[%s][Column Change] %s (#%d)' => '', - // '[%s][Position Change] %s (#%d)' => '', - // '[%s][Assignee Change] %s (#%d)' => '', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', 'New password for the user "%s"' => 'รหัสผ่านใหม่สำหรับผู้ใช้ "%s"', // 'Choose an event' => '', // 'Github commit received' => '', @@ -647,5 +645,93 @@ return array( // 'Application default' => '', // 'Language:' => '', // 'Timezone:' => '', + // 'All columns' => '', + // 'Calendar for "%s"' => '', + // 'Filter by column' => '', + // 'Filter by status' => '', + // 'Calendar' => '', // 'Next' => '', + // '#%d' => '', + // 'Filter by color' => '', + // 'Filter by swimlane' => '', + // 'All swimlanes' => '', + // 'All colors' => '', + // 'All status' => '', + // 'Add a comment logging moving the task between columns' => '', + // 'Moved to column %s' => '', + // 'Change description' => '', + // 'User dashboard' => '', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Enable time tracking for subtasks' => '', + // 'Select the new status of the subtask: "%s"' => '', + // 'Subtask timesheet' => '', + // 'There is nothing to show.' => '', + // 'Time Tracking' => '', + // 'You already have one subtask in progress' => '', + // 'Which parts of the project do you want to duplicate?' => '', + // 'Change dashboard view' => '', + // 'Show/hide activities' => '', + // 'Show/hide projects' => '', + // 'Show/hide subtasks' => '', + // 'Show/hide tasks' => '', + // 'Disable login form' => '', + // 'Show/hide calendar' => '', + // 'User calendar' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + // 'Task age in days' => '', + // 'Days in this column' => '', + // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + // 'Expand tasks' => '', + // 'Collapse tasks' => '', + // 'Expand/collapse tasks' => '', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', ); diff --git a/sources/app/Locale/zh_CN/translations.php b/sources/app/Locale/zh_CN/translations.php index cd423d0..cd1e2eb 100644 --- a/sources/app/Locale/zh_CN/translations.php +++ b/sources/app/Locale/zh_CN/translations.php @@ -408,15 +408,13 @@ return array( 'Comment updated' => '更新了评论', 'New comment posted by %s' => '%s 的新评论', 'List of due tasks for the project "%s"' => '项目"%s"的到期任务列表', - '[%s][New attachment] %s (#%d)' => '[%s][新附件] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][新评论] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][评论更新] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][新子任务] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][子任务更新] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][新任务] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][任务更新] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][任务关闭] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][任务开启] %s (#%d)', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + // 'Task closed' => '', + // 'Task opened' => '', '[%s][Due tasks]' => '[%s][到期任务]', '[Kanboard] Notification' => '[Kanboard] 通知', 'I want to receive notifications only for those projects:' => '我仅需要收到下面项目的通知:', @@ -500,9 +498,9 @@ return array( 'Task assignee change' => '任务分配变更', '%s change the assignee of the task #%d to %s' => '%s 将任务 #%d 分配给了 %s', '%s changed the assignee of the task %s to %s' => '%s 将任务 %s 分配给 %s', - '[%s][Column Change] %s (#%d)' => '[%s][栏目变更] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][位置变更] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][任务分配变更] %s (#%d)', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', 'New password for the user "%s"' => '用户"%s"的新密码', 'Choose an event' => '选择一个事件', 'Github commit received' => '收到了Github提交', @@ -647,5 +645,93 @@ return array( // 'Application default' => '', // 'Language:' => '', // 'Timezone:' => '', + // 'All columns' => '', + // 'Calendar for "%s"' => '', + // 'Filter by column' => '', + // 'Filter by status' => '', + // 'Calendar' => '', // 'Next' => '', + // '#%d' => '', + // 'Filter by color' => '', + // 'Filter by swimlane' => '', + // 'All swimlanes' => '', + // 'All colors' => '', + // 'All status' => '', + // 'Add a comment logging moving the task between columns' => '', + // 'Moved to column %s' => '', + // 'Change description' => '', + // 'User dashboard' => '', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Enable time tracking for subtasks' => '', + // 'Select the new status of the subtask: "%s"' => '', + // 'Subtask timesheet' => '', + // 'There is nothing to show.' => '', + // 'Time Tracking' => '', + // 'You already have one subtask in progress' => '', + // 'Which parts of the project do you want to duplicate?' => '', + // 'Change dashboard view' => '', + // 'Show/hide activities' => '', + // 'Show/hide projects' => '', + // 'Show/hide subtasks' => '', + // 'Show/hide tasks' => '', + // 'Disable login form' => '', + // 'Show/hide calendar' => '', + // 'User calendar' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + // 'Task age in days' => '', + // 'Days in this column' => '', + // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + // 'Expand tasks' => '', + // 'Collapse tasks' => '', + // 'Expand/collapse tasks' => '', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', ); diff --git a/sources/app/Model/Acl.php b/sources/app/Model/Acl.php index 599ff05..9fc8174 100644 --- a/sources/app/Model/Acl.php +++ b/sources/app/Model/Acl.php @@ -22,6 +22,7 @@ class Acl extends Base 'board' => array('readonly'), 'project' => array('feed'), 'webhook' => '*', + 'app' => array('colors'), ); /** @@ -37,6 +38,8 @@ class Acl extends Base 'project' => array('show', 'tasks', 'search', 'activity'), 'subtask' => '*', 'task' => '*', + 'tasklink' => '*', + 'calendar' => array('show', 'project'), ); /** @@ -48,7 +51,7 @@ class Acl extends Base private $manager_acl = array( 'action' => '*', 'analytic' => '*', - 'board' => array('movecolumn', 'edit', 'update', 'add', 'remove'), + 'board' => array('movecolumn', 'edit', 'editcolumn', 'updatecolumn', 'add', 'remove'), 'category' => '*', 'export' => array('tasks', 'subtasks', 'summary'), 'project' => array('edit', 'update', 'share', 'integration', 'users', 'alloweverybody', 'allow', 'setowner', 'revoke', 'duplicate', 'disable', 'enable'), @@ -62,8 +65,10 @@ class Acl extends Base * @var array */ private $admin_acl = array( + 'app' => array('dashboard'), 'user' => array('index', 'create', 'save', 'remove'), 'config' => '*', + 'link' => '*', 'project' => array('remove'), ); diff --git a/sources/app/Model/Action.php b/sources/app/Model/Action.php index 2204ad3..6fb2a2f 100644 --- a/sources/app/Model/Action.php +++ b/sources/app/Model/Action.php @@ -4,6 +4,7 @@ namespace Model; use Integration\GitlabWebhook; use Integration\GithubWebhook; +use Integration\BitbucketWebhook; use SimpleValidator\Validator; use SimpleValidator\Validators; @@ -49,6 +50,7 @@ class Action extends Base 'TaskAssignCategoryColor' => t('Assign automatically a category based on a color'), 'CommentCreation' => t('Create a comment from an external provider'), 'TaskCreation' => t('Create a task from an external provider'), + 'TaskLogMoveAnotherColumn' => t('Add a comment logging moving the task between columns'), 'TaskAssignUser' => t('Change the assignee based on an external username'), 'TaskAssignCategoryLabel' => t('Change the category based on an external label'), ); @@ -84,6 +86,7 @@ class Action extends Base GitlabWebhook::EVENT_COMMIT => t('Gitlab commit received'), GitlabWebhook::EVENT_ISSUE_OPENED => t('Gitlab issue opened'), GitlabWebhook::EVENT_ISSUE_CLOSED => t('Gitlab issue closed'), + BitbucketWebhook::EVENT_COMMIT => t('Bitbucket commit received'), ); asort($values); diff --git a/sources/app/Model/Authentication.php b/sources/app/Model/Authentication.php index 92898cd..86c1c43 100644 --- a/sources/app/Model/Authentication.php +++ b/sources/app/Model/Authentication.php @@ -42,6 +42,13 @@ class Authentication extends Base // If the user is already logged it's ok if ($this->userSession->isLogged()) { + // Check if the user session match an existing user + if (! $this->user->exists($this->userSession->getId())) { + $this->backend('rememberMe')->destroy($this->userSession->getId()); + $this->session->close(); + return false; + } + // We update each time the RememberMe cookie tokens if ($this->backend('rememberMe')->hasCookie()) { $this->backend('rememberMe')->refresh(); diff --git a/sources/app/Model/Base.php b/sources/app/Model/Base.php index 3f847c2..f836231 100644 --- a/sources/app/Model/Base.php +++ b/sources/app/Model/Base.php @@ -10,37 +10,43 @@ use Pimple\Container; * @package model * @author Frederic Guillot * - * @property \Core\Session $session - * @property \Core\Template $template - * @property \Model\Acl $acl - * @property \Model\Action $action - * @property \Model\Authentication $authentication - * @property \Model\Board $board - * @property \Model\Category $category - * @property \Model\Comment $comment - * @property \Model\CommentHistory $commentHistory - * @property \Model\Color $color - * @property \Model\Config $config - * @property \Model\DateParser $dateParser - * @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\SubtaskHistory $subtaskHistory - * @property \Model\Swimlane $swimlane - * @property \Model\Task $task - * @property \Model\TaskCreation $taskCreation - * @property \Model\TaskExport $taskExport - * @property \Model\TaskFinder $taskFinder - * @property \Model\TaskHistory $taskHistory - * @property \Model\TaskPosition $taskPosition - * @property \Model\TaskValidator $taskValidator - * @property \Model\TimeTracking $timeTracking - * @property \Model\User $user - * @property \Model\UserSession $userSession - * @property \Model\Webhook $webhook + * @property \Core\Session $session + * @property \Core\Template $template + * @property \Model\Acl $acl + * @property \Model\Action $action + * @property \Model\Authentication $authentication + * @property \Model\Board $board + * @property \Model\Category $category + * @property \Model\Comment $comment + * @property \Model\CommentHistory $commentHistory + * @property \Model\Color $color + * @property \Model\Config $config + * @property \Model\DateParser $dateParser + * @property \Model\File $file + * @property \Model\Helper $helper + * @property \Model\LastLogin $lastLogin + * @property \Model\Link $link + * @property \Model\Notification $notification + * @property \Model\Project $project + * @property \Model\ProjectDuplication $projectDuplication + * @property \Model\ProjectPermission $projectPermission + * @property \Model\Subtask $subtask + * @property \Model\SubtaskHistory $subtaskHistory + * @property \Model\Swimlane $swimlane + * @property \Model\Task $task + * @property \Model\TaskCreation $taskCreation + * @property \Model\TaskDuplication $taskDuplication + * @property \Model\TaskExport $taskExport + * @property \Model\TaskFinder $taskFinder + * @property \Model\TaskHistory $taskHistory + * @property \Model\TaskLink $taskLink + * @property \Model\TaskPosition $taskPosition + * @property \Model\TaskValidator $taskValidator + * @property \Model\TimeTracking $timeTracking + * @property \Model\SubtaskTimeTracking $subtaskTimeTracking + * @property \Model\User $user + * @property \Model\UserSession $userSession + * @property \Model\Webhook $webhook */ abstract class Base { diff --git a/sources/app/Model/Board.php b/sources/app/Model/Board.php index 550009f..8a344f7 100644 --- a/sources/app/Model/Board.php +++ b/sources/app/Model/Board.php @@ -47,7 +47,7 @@ class Board extends Base $column_name = trim($column_name); if (! empty($column_name)) { - $columns[] = array('title' => $column_name, 'task_limit' => 0); + $columns[] = array('title' => $column_name, 'task_limit' => 0, 'description' => ''); } } @@ -73,6 +73,7 @@ class Board extends Base 'position' => ++$position, 'project_id' => $project_id, 'task_limit' => $column['task_limit'], + 'description' => $column['description'], ); if (! $this->db->table(self::TABLE)->save($values)) { @@ -94,7 +95,7 @@ class Board extends Base public function duplicate($project_from, $project_to) { $columns = $this->db->table(Board::TABLE) - ->columns('title', 'task_limit') + ->columns('title', 'task_limit', 'description') ->eq('project_id', $project_from) ->asc('position') ->findAll(); @@ -109,48 +110,22 @@ class Board extends Base * @param integer $project_id Project id * @param string $title Column title * @param integer $task_limit Task limit + * @param string $description Column description * @return boolean|integer */ - public function addColumn($project_id, $title, $task_limit = 0) + public function addColumn($project_id, $title, $task_limit = 0, $description = '') { $values = array( 'project_id' => $project_id, 'title' => $title, - 'task_limit' => $task_limit, + 'task_limit' => intval($task_limit), 'position' => $this->getLastColumnPosition($project_id) + 1, + 'description' => $description, ); return $this->persist(self::TABLE, $values); } - /** - * Update columns - * - * @access public - * @param array $values Form values - * @return boolean - */ - public function update(array $values) - { - $columns = array(); - - foreach (array('title', 'task_limit') as $field) { - foreach ($values[$field] as $column_id => $value) { - $columns[$column_id][$field] = $value; - } - } - - $this->db->startTransaction(); - - foreach ($columns as $column_id => $values) { - $this->updateColumn($column_id, $values['title'], (int) $values['task_limit']); - } - - $this->db->closeTransaction(); - - return true; - } - /** * Update a column * @@ -158,13 +133,15 @@ class Board extends Base * @param integer $column_id Column id * @param string $title Column title * @param integer $task_limit Task limit + * @param string $description Optional description * @return boolean */ - public function updateColumn($column_id, $title, $task_limit = 0) + public function updateColumn($column_id, $title, $task_limit = 0, $description = '') { return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array( 'title' => $title, - 'task_limit' => $task_limit, + 'task_limit' => intval($task_limit), + 'description' => $description, )); } @@ -178,7 +155,7 @@ class Board extends Base */ public function moveDown($project_id, $column_id) { - $columns = $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'position'); + $columns = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'position'); $positions = array_flip($columns); if (isset($columns[$column_id]) && $columns[$column_id] < count($columns)) { @@ -207,7 +184,7 @@ class Board extends Base */ public function moveUp($project_id, $column_id) { - $columns = $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'position'); + $columns = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'position'); $positions = array_flip($columns); if (isset($columns[$column_id]) && $columns[$column_id] > 1) { @@ -243,10 +220,12 @@ class Board extends Base $swimlanes[$i]['columns'] = $columns; $swimlanes[$i]['nb_columns'] = $nb_columns; + $swimlanes[$i]['nb_tasks'] = 0; for ($j = 0; $j < $nb_columns; $j++) { $swimlanes[$i]['columns'][$j]['tasks'] = $this->taskFinder->getTasksByColumnAndSwimlane($project_id, $columns[$j]['id'], $swimlanes[$i]['id']); $swimlanes[$i]['columns'][$j]['nb_tasks'] = count($swimlanes[$i]['columns'][$j]['tasks']); + $swimlanes[$i]['nb_tasks'] += $swimlanes[$i]['columns'][$j]['nb_tasks']; } } @@ -258,16 +237,19 @@ class Board extends Base * * @access public * @param integer $project_id + * @param boolean $prepend Prepend default value * @return array */ - public function getColumnStats($project_id) + public function getColumnStats($project_id, $prepend = false) { - return $this->db - ->table(Task::TABLE) - ->eq('project_id', $project_id) - ->eq('is_active', 1) - ->groupBy('column_id') - ->listing('column_id', 'COUNT(*) AS total'); + $listing = $this->db + ->hashtable(Task::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', 1) + ->groupBy('column_id') + ->getAll('column_id', 'COUNT(*) AS total'); + + return $prepend ? array(-1 => t('All columns')) + $listing : $listing; } /** @@ -287,11 +269,13 @@ class Board extends Base * * @access public * @param integer $project_id Project id + * @param boolean $prepend Prepend a default value * @return array */ - public function getColumnsList($project_id) + public function getColumnsList($project_id, $prepend = false) { - return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'title'); + $listing = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'title'); + return $prepend ? array(-1 => t('All columns')) + $listing : $listing; } /** @@ -362,22 +346,16 @@ class Board extends Base * Validate column modification * * @access public - * @param array $columns Original columns List * @param array $values Required parameters to update a column * @return array $valid, $errors [0] = Success or not, [1] = List of errors */ - public function validateModification(array $columns, array $values) + public function validateModification(array $values) { - $rules = array(); - - foreach ($columns as $column_id => $column_title) { - $rules[] = new Validators\Integer('task_limit['.$column_id.']', t('This value must be an integer')); - $rules[] = new Validators\GreaterThan('task_limit['.$column_id.']', t('This value must be greater than %d', 0), 0); - $rules[] = new Validators\Required('title['.$column_id.']', t('The title is required')); - $rules[] = new Validators\MaxLength('title['.$column_id.']', t('The maximum length is %d characters', 50), 50); - } - - $v = new Validator($values, $rules); + $v = new Validator($values, array( + new Validators\Integer('task_limit', t('This value must be an integer')), + new Validators\Required('title', t('The title is required')), + new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50), + )); return array( $v->execute(), diff --git a/sources/app/Model/Category.php b/sources/app/Model/Category.php index cd60e7f..1c8ba96 100644 --- a/sources/app/Model/Category.php +++ b/sources/app/Model/Category.php @@ -84,10 +84,10 @@ class Category extends Base */ public function getList($project_id, $prepend_none = true, $prepend_all = false) { - $listing = $this->db->table(self::TABLE) + $listing = $this->db->hashtable(self::TABLE) ->eq('project_id', $project_id) ->asc('name') - ->listing('id', 'name'); + ->getAll('id', 'name'); $prepend = array(); diff --git a/sources/app/Model/Color.php b/sources/app/Model/Color.php index 8668cf0..241a97c 100644 --- a/sources/app/Model/Color.php +++ b/sources/app/Model/Color.php @@ -3,22 +3,68 @@ namespace Model; /** - * Color model (TODO: model for the future color picker) + * Color model * * @package model * @author Frederic Guillot */ class Color extends Base { + /** + * Default colors + * + * @access private + * @var array + */ + private $default_colors = array( + 'yellow' => array( + 'name' => 'Yellow', + 'background' => 'rgb(245, 247, 196)', + 'border' => 'rgb(223, 227, 45)', + ), + 'blue' => array( + 'name' => 'Blue', + 'background' => 'rgb(219, 235, 255)', + 'border' => 'rgb(168, 207, 255)', + ), + 'green' => array( + 'name' => 'Green', + 'background' => 'rgb(189, 244, 203)', + 'border' => 'rgb(74, 227, 113)', + ), + 'purple' => array( + 'name' => 'Purple', + 'background' => 'rgb(223, 176, 255)', + 'border' => 'rgb(205, 133, 254)', + ), + 'red' => array( + 'name' => 'Red', + 'background' => 'rgb(255, 187, 187)', + 'border' => 'rgb(255, 151, 151)', + ), + 'orange' => array( + 'name' => 'Orange', + 'background' => 'rgb(255, 215, 179)', + 'border' => 'rgb(255, 172, 98)', + ), + 'grey' => array( + 'name' => 'Grey', + 'background' => 'rgb(238, 238, 238)', + 'border' => 'rgb(204, 204, 204)', + ), + ); + /** * Get available colors * * @access public * @return array */ - public function getList() + public function getList($prepend = false) { - return array( + $listing = $prepend ? array('' => t('All colors')) : array(); + + return $listing + array( 'yellow' => t('Yellow'), 'blue' => t('Blue'), 'green' => t('Green'), @@ -39,4 +85,57 @@ class Color extends Base { return 'yellow'; // TODO: make this parameter configurable } + + /** + * Get Bordercolor from string + * + * @access public + * @param string $color_id Color id + * @return string + */ + public function getBorderColor($color_id) + { + if (isset($this->default_colors[$color_id])) { + return $this->default_colors[$color_id]['border']; + } + + return $this->default_colors[$this->getDefaultColor()]['border']; + } + + /** + * Get background color from the color_id + * + * @access public + * @param string $color_id Color id + * @return string + */ + public function getBackgroundColor($color_id) + { + if (isset($this->default_colors[$color_id])) { + return $this->default_colors[$color_id]['background']; + } + + return $this->default_colors[$this->getDefaultColor()]['background']; + } + + /** + * Get CSS stylesheet of all colors + * + * @access public + * @return string + */ + public function getCss() + { + $buffer = ''; + + foreach ($this->default_colors as $color => $values) { + $buffer .= 'td.color-'.$color.','; + $buffer .= 'div.color-'.$color.' {'; + $buffer .= 'background-color: '.$values['background'].';'; + $buffer .= 'border-color: '.$values['border']; + $buffer .= '}'; + } + + return $buffer; + } } diff --git a/sources/app/Model/Config.php b/sources/app/Model/Config.php index e6d6673..48640f4 100644 --- a/sources/app/Model/Config.php +++ b/sources/app/Model/Config.php @@ -75,6 +75,52 @@ class Config extends Base return $languages; } + /** + * Get javascript language code + * + * @access public + * @return string + */ + public function getJsLanguageCode() + { + $languages = array( + 'da_DK' => 'da', + 'de_DE' => 'de', + 'en_US' => 'en', + 'es_ES' => 'es', + 'fr_FR' => 'fr', + 'it_IT' => 'it', + 'hu_HU' => 'hu', + 'pl_PL' => 'pl', + 'pt_BR' => 'pt-br', + 'ru_RU' => 'ru', + 'fi_FI' => 'fi', + 'sv_SE' => 'sv', + 'zh_CN' => 'zh-cn', + 'ja_JP' => 'ja', + 'th_TH' => 'th', + ); + + $lang = $this->getCurrentLanguage(); + + return isset($languages[$lang]) ? $languages[$lang] : 'en'; + } + + /** + * Get current language + * + * @access public + * @return string + */ + public function getCurrentLanguage() + { + if ($this->userSession->isLogged() && ! empty($this->session['user']['language'])) { + return $this->session['user']['language']; + } + + return $this->get('application_language', 'en_US'); + } + /** * Get a config variable from the session or the database * @@ -110,7 +156,7 @@ class Config extends Base */ public function getAll() { - return $this->db->table(self::TABLE)->listing('option', 'value'); + return $this->db->hashtable(self::TABLE)->getAll('option', 'value'); } /** @@ -152,12 +198,22 @@ class Config extends Base */ public function setupTranslations() { - if ($this->userSession->isLogged() && ! empty($this->session['user']['language'])) { - Translator::load($this->session['user']['language']); - } - else { - Translator::load($this->get('application_language', 'en_US')); + Translator::load($this->getCurrentLanguage()); + } + + /** + * Get current timezone + * + * @access public + * @return string + */ + public function getCurrentTimezone() + { + if ($this->userSession->isLogged() && ! empty($this->session['user']['timezone'])) { + return $this->session['user']['timezone']; } + + return $this->get('application_timezone', 'UTC'); } /** @@ -167,12 +223,7 @@ class Config extends Base */ public function setupTimezone() { - if ($this->userSession->isLogged() && ! empty($this->session['user']['timezone'])) { - date_default_timezone_set($this->session['user']['timezone']); - } - else { - date_default_timezone_set($this->get('application_timezone', 'UTC')); - } + date_default_timezone_set($this->getCurrentTimezone()); } /** diff --git a/sources/app/Model/DateParser.php b/sources/app/Model/DateParser.php index 518a4f3..8a4d3ed 100644 --- a/sources/app/Model/DateParser.php +++ b/sources/app/Model/DateParser.php @@ -87,17 +87,29 @@ class DateParser extends Base } /** - * For a given timestamp, reset the date to midnight + * Remove the time from a timestamp * * @access public * @param integer $timestamp Timestamp * @return integer */ - public function resetDateToMidnight($timestamp) + public function removeTimeFromTimestamp($timestamp) { return mktime(0, 0, 0, date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp)); } + /** + * Get a timetstamp from an ISO date format + * + * @access public + * @param string $date Date format + * @return integer + */ + public function getTimestampFromIsoFormat($date) + { + return $this->removeTimeFromTimestamp(strtotime($date)); + } + /** * Format date (form display) * @@ -135,7 +147,7 @@ class DateParser extends Base foreach ($fields as $field) { if (! empty($values[$field]) && ! is_numeric($values[$field])) { - $values[$field] = $this->getTimestamp($values[$field]); + $values[$field] = $this->removeTimeFromTimestamp($this->getTimestamp($values[$field])); } } } diff --git a/sources/app/Model/File.php b/sources/app/Model/File.php index 20fba9b..1b9351d 100644 --- a/sources/app/Model/File.php +++ b/sources/app/Model/File.php @@ -19,13 +19,6 @@ class File extends Base */ const TABLE = 'task_has_files'; - /** - * Directory where are stored files - * - * @var string - */ - const BASE_PATH = 'data/files/'; - /** * Events * @@ -56,7 +49,7 @@ class File extends Base { $file = $this->getbyId($file_id); - if (! empty($file) && @unlink(self::BASE_PATH.$file['path'])) { + if (! empty($file) && @unlink(FILES_DIR.$file['path'])) { return $this->db->table(self::TABLE)->eq('id', $file_id)->remove(); } @@ -152,14 +145,14 @@ class File extends Base */ public function setup() { - if (! is_dir(self::BASE_PATH)) { - if (! mkdir(self::BASE_PATH, 0755, true)) { - die('Unable to create the upload directory: "'.self::BASE_PATH.'"'); + if (! is_dir(FILES_DIR)) { + if (! mkdir(FILES_DIR, 0755, true)) { + die('Unable to create the upload directory: "'.FILES_DIR.'"'); } } - if (! is_writable(self::BASE_PATH)) { - die('The directory "'.self::BASE_PATH.'" must be writeable by your webserver user'); + if (! is_writable(FILES_DIR)) { + die('The directory "'.FILES_DIR.'" must be writeable by your webserver user'); } } @@ -187,15 +180,15 @@ class File extends Base $uploaded_filename = $_FILES[$form_name]['tmp_name'][$key]; $destination_filename = $this->generatePath($project_id, $task_id, $original_filename); - @mkdir(self::BASE_PATH.dirname($destination_filename), 0755, true); + @mkdir(FILES_DIR.dirname($destination_filename), 0755, true); - if (@move_uploaded_file($uploaded_filename, self::BASE_PATH.$destination_filename)) { + if (@move_uploaded_file($uploaded_filename, FILES_DIR.$destination_filename)) { $result[] = $this->create( $task_id, $original_filename, $destination_filename, - $this->isImage(self::BASE_PATH.$destination_filename) + $this->isImage(FILES_DIR.$destination_filename) ); } } diff --git a/sources/app/Model/Link.php b/sources/app/Model/Link.php new file mode 100644 index 0000000..87ba49c --- /dev/null +++ b/sources/app/Model/Link.php @@ -0,0 +1,234 @@ +db->table(self::TABLE)->eq('id', $link_id)->findOne(); + } + + /** + * Get a link by name + * + * @access public + * @param string $label + * @return array + */ + public function getByLabel($label) + { + return $this->db->table(self::TABLE)->eq('label', $label)->findOne(); + } + + /** + * Get the opposite link id + * + * @access public + * @param integer $link_id Link id + * @return integer + */ + public function getOppositeLinkId($link_id) + { + $link = $this->getById($link_id); + return $link['opposite_id'] ?: $link_id; + } + + /** + * Get all links + * + * @access public + * @return array + */ + public function getAll() + { + return $this->db->table(self::TABLE)->findAll(); + } + + /** + * Get merged links + * + * @access public + * @return array + */ + public function getMergedList() + { + return $this->db + ->execute(' + SELECT + links.id, links.label, opposite.label as opposite_label + FROM links + LEFT JOIN links AS opposite ON opposite.id=links.opposite_id + ') + ->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Get label list + * + * @access public + * @param integer $exclude_id Exclude this link + * @param booelan $prepend Prepend default value + * @return array + */ + public function getList($exclude_id = 0, $prepend = true) + { + $labels = $this->db->hashtable(self::TABLE)->neq('id', $exclude_id)->asc('id')->getAll('id', 'label'); + + foreach ($labels as &$value) { + $value = t($value); + } + + return $prepend ? array('') + $labels : $labels; + } + + /** + * Create a new link label + * + * @access public + * @param string $label + * @param string $opposite_label + * @return boolean + */ + public function create($label, $opposite_label = '') + { + $this->db->startTransaction(); + + if (! $this->db->table(self::TABLE)->insert(array('label' => $label))) { + $this->db->cancelTransaction(); + return false; + } + + if ($opposite_label !== '') { + $this->createOpposite($opposite_label); + } + + $this->db->closeTransaction(); + + return true; + } + + /** + * Create the opposite label (executed inside create() method) + * + * @access private + * @param string $label + */ + private function createOpposite($label) + { + $label_id = $this->db->getConnection()->getLastId(); + + $this->db + ->table(self::TABLE) + ->insert(array( + 'label' => $label, + 'opposite_id' => $label_id, + )); + + $this->db + ->table(self::TABLE) + ->eq('id', $label_id) + ->update(array( + 'opposite_id' => $this->db->getConnection()->getLastId() + )); + } + + /** + * Update a link + * + * @access public + * @param array $values + * @return boolean + */ + public function update(array $values) + { + return $this->db + ->table(self::TABLE) + ->eq('id', $values['id']) + ->update(array( + 'label' => $values['label'], + 'opposite_id' => $values['opposite_id'], + )); + } + + /** + * Remove a link a the relation to its opposite + * + * @access public + * @param integer $link_id + * @return boolean + */ + public function remove($link_id) + { + $this->db->table(self::TABLE)->eq('opposite_id', $link_id)->update(array('opposite_id' => 0)); + return $this->db->table(self::TABLE)->eq('id', $link_id)->remove(); + } + + /** + * Validate creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('label', t('Field required')), + new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE), + new Validators\NotEquals('label', 'opposite_label', t('The labels must be different')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $v = new Validator($values, array( + new Validators\Required('id', t('Field required')), + new Validators\Required('opposite_id', t('Field required')), + new Validators\Required('label', t('Field required')), + new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/sources/app/Model/Notification.php b/sources/app/Model/Notification.php index 95306e8..2b6e6a7 100644 --- a/sources/app/Model/Notification.php +++ b/sources/app/Model/Notification.php @@ -118,6 +118,18 @@ class Notification extends Base } } + /** + * Get the mail subject for a given label + * + * @access private + * @param string $label Label + * @param array $data Template data + */ + private function getStandardMailSubject($label, array $data) + { + return sprintf('[%s][%s] %s (#%d)', $data['task']['project_name'], $label, $data['task']['title'], $data['task']['id']); + } + /** * Get the mail subject for a given template name * @@ -129,40 +141,40 @@ class Notification extends Base { switch ($template) { case 'file_creation': - $subject = e('[%s][New attachment] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + $subject = $this->getStandardMailSubject(t('New attachment'), $data); break; case 'comment_creation': - $subject = e('[%s][New comment] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + $subject = $this->getStandardMailSubject(t('New comment'), $data); break; case 'comment_update': - $subject = e('[%s][Comment updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + $subject = $this->getStandardMailSubject(t('Comment updated'), $data); break; case 'subtask_creation': - $subject = e('[%s][New subtask] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + $subject = $this->getStandardMailSubject(t('New subtask'), $data); break; case 'subtask_update': - $subject = e('[%s][Subtask updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + $subject = $this->getStandardMailSubject(t('Subtask updated'), $data); break; case 'task_creation': - $subject = e('[%s][New task] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + $subject = $this->getStandardMailSubject(t('New task'), $data); break; case 'task_update': - $subject = e('[%s][Task updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + $subject = $this->getStandardMailSubject(t('Task updated'), $data); break; case 'task_close': - $subject = e('[%s][Task closed] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + $subject = $this->getStandardMailSubject(t('Task closed'), $data); break; case 'task_open': - $subject = e('[%s][Task opened] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + $subject = $this->getStandardMailSubject(t('Task opened'), $data); break; case 'task_move_column': - $subject = e('[%s][Column Change] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + $subject = $this->getStandardMailSubject(t('Column Change'), $data); break; case 'task_move_position': - $subject = e('[%s][Position Change] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + $subject = $this->getStandardMailSubject(t('Position Change'), $data); break; case 'task_assignee_change': - $subject = e('[%s][Assignee Change] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + $subject = $this->getStandardMailSubject(t('Assignee Change'), $data); break; case 'task_due': $subject = e('[%s][Due tasks]', $data['project']); diff --git a/sources/app/Model/Project.php b/sources/app/Model/Project.php index f9c5c39..dbb9db1 100644 --- a/sources/app/Model/Project.php +++ b/sources/app/Model/Project.php @@ -95,27 +95,25 @@ class Project extends Base } /** - * Get all projects, optionaly fetch stats for each project and can check users permissions + * Get all projects * * @access public - * @param bool $filter_permissions If true, remove projects not allowed for the current user * @return array */ - public function getAll($filter_permissions = false) + public function getAll() { - $projects = $this->db->table(self::TABLE)->asc('name')->findAll(); + return $this->db->table(self::TABLE)->asc('name')->findAll(); + } - if ($filter_permissions) { - - foreach ($projects as $key => $project) { - - if (! $this->projectPermission->isUserAllowed($project['id'], $this->userSession->getId())) { - unset($projects[$key]); - } - } - } - - return $projects; + /** + * Get all project ids + * + * @access public + * @return array + */ + public function getAllIds() + { + return $this->db->table(self::TABLE)->asc('name')->findAllByColumn('id'); } /** @@ -128,10 +126,10 @@ class Project extends Base public function getList($prepend = true) { if ($prepend) { - return array(t('None')) + $this->db->table(self::TABLE)->asc('name')->listing('id', 'name'); + return array(t('None')) + $this->db->hashtable(self::TABLE)->asc('name')->getAll('id', 'name'); } - return $this->db->table(self::TABLE)->asc('name')->listing('id', 'name'); + return $this->db->hashtable(self::TABLE)->asc('name')->getAll('id', 'name'); } /** @@ -160,10 +158,10 @@ class Project extends Base public function getListByStatus($status) { return $this->db - ->table(self::TABLE) + ->hashtable(self::TABLE) ->asc('name') ->eq('is_active', $status) - ->listing('id', 'name'); + ->getAll('id', 'name'); } /** @@ -188,7 +186,7 @@ class Project extends Base * @param integer $project_id Project id * @return array */ - public function getStats($project_id) + public function getTaskStats($project_id) { $stats = array(); $stats['nb_active_tasks'] = 0; @@ -208,62 +206,57 @@ class Project extends Base } /** - * Create a project from another one. + * Get stats for each column of a project * - * @author Antonio Rabelo - * @param integer $project_id Project Id - * @return integer Cloned Project Id + * @access public + * @param array $project + * @return array */ - public function createProjectFromAnotherProject($project_id) + public function getColumnStats(array &$project) { - $project = $this->getById($project_id); + $project['columns'] = $this->board->getColumns($project['id']); + $stats = $this->board->getColumnStats($project['id']); - $values = array( - 'name' => $project['name'].' ('.t('Clone').')', - 'is_active' => true, - 'last_modified' => 0, - 'token' => '', - 'is_public' => 0, - 'is_private' => empty($project['is_private']) ? 0 : 1, - ); - - if (! $this->db->table(self::TABLE)->save($values)) { - return 0; + foreach ($project['columns'] as &$column) { + $column['nb_tasks'] = isset($stats[$column['id']]) ? $stats[$column['id']] : 0; } - return $this->db->getConnection()->getLastId(); + return $project; } /** - * Clone a project + * Apply column stats to a collection of projects (filter callback) * - * @author Antonio Rabelo - * @param integer $project_id Project Id - * @return integer Cloned Project Id + * @access public + * @param array $projects + * @return array */ - public function duplicate($project_id) + public function applyColumnStats(array $projects) { - $this->db->startTransaction(); - - // Get the cloned project Id - $clone_project_id = $this->createProjectFromAnotherProject($project_id); - - if (! $clone_project_id) { - $this->db->cancelTransaction(); - return false; + foreach ($projects as &$project) { + $this->getColumnStats($project); } - foreach (array('board', 'category', 'projectPermission', 'action') as $model) { + return $projects; + } - if (! $this->$model->duplicate($project_id, $clone_project_id)) { - $this->db->cancelTransaction(); - return false; - } + /** + * Get project summary for a list of project + * + * @access public + * @param array $project_ids List of project id + * @return \PicoDb\Table + */ + public function getQueryColumnStats(array $project_ids) + { + if (empty($project_ids)) { + return $this->db->table(Project::TABLE)->limit(0); } - $this->db->closeTransaction(); - - return (int) $clone_project_id; + return $this->db + ->table(Project::TABLE) + ->in('id', $project_ids) + ->filter(array($this, 'applyColumnStats')); } /** diff --git a/sources/app/Model/ProjectActivity.php b/sources/app/Model/ProjectActivity.php index bbcb7f5..652cc84 100644 --- a/sources/app/Model/ProjectActivity.php +++ b/sources/app/Model/ProjectActivity.php @@ -56,11 +56,13 @@ class ProjectActivity extends Base * @access public * @param integer $project_id Project id * @param integer $limit Maximum events number + * @param integer $start Timestamp of earliest activity + * @param integer $end Timestamp of latest activity * @return array */ - public function getProject($project_id, $limit = 50) + public function getProject($project_id, $limit = 50, $start = null, $end = null) { - return $this->getProjects(array($project_id), $limit); + return $this->getProjects(array($project_id), $limit, $start, $end); } /** @@ -69,15 +71,17 @@ class ProjectActivity extends Base * @access public * @param integer[] $project_ids Projects id * @param integer $limit Maximum events number + * @param integer $start Timestamp of earliest activity + * @param integer $end Timestamp of latest activity * @return array */ - public function getProjects(array $project_ids, $limit = 50) + public function getProjects(array $project_ids, $limit = 50, $start = null, $end = null) { if (empty($project_ids)) { return array(); } - $events = $this->db->table(self::TABLE) + $query = $this->db->table(self::TABLE) ->columns( self::TABLE.'.*', User::TABLE.'.username AS author_username', @@ -85,9 +89,18 @@ class ProjectActivity extends Base ) ->in('project_id', $project_ids) ->join(User::TABLE, 'id', 'creator_id') - ->desc('id') - ->limit($limit) - ->findAll(); + ->desc(self::TABLE.'.id') + ->limit($limit); + + if(!is_null($start)){ + $query->gte('date_creation', $start); + } + + if(!is_null($end)){ + $query->lte('date_creation', $end); + } + + $events = $query->findAll(); foreach ($events as &$event) { @@ -162,9 +175,9 @@ class ProjectActivity extends Base return t('%s moved the task #%d to the column "%s"', $event['author'], $event['task']['id'], $event['task']['column_title']); case Task::EVENT_MOVE_POSITION: return t('%s moved the task #%d to the position %d in the column "%s"', $event['author'], $event['task']['id'], $event['task']['position'], $event['task']['column_title']); - case SubTask::EVENT_UPDATE: + case Subtask::EVENT_UPDATE: return t('%s updated a subtask for the task #%d', $event['author'], $event['task']['id']); - case SubTask::EVENT_CREATE: + case Subtask::EVENT_CREATE: return t('%s created a subtask for the task #%d', $event['author'], $event['task']['id']); case Comment::EVENT_UPDATE: return t('%s updated a comment on the task #%d', $event['author'], $event['task']['id']); diff --git a/sources/app/Model/ProjectDuplication.php b/sources/app/Model/ProjectDuplication.php new file mode 100644 index 0000000..7e3407b --- /dev/null +++ b/sources/app/Model/ProjectDuplication.php @@ -0,0 +1,108 @@ + $max_length) { + $name = substr($name, 0, $max_length - strlen($suffix)); + } + + return $name.$suffix; + } + + /** + * Create a project from another one + * + * @param integer $project_id Project Id + * @return integer Cloned Project Id + */ + public function copy($project_id) + { + $project = $this->project->getById($project_id); + + $values = array( + 'name' => $this->getClonedProjectName($project['name']), + 'is_active' => true, + 'last_modified' => 0, + 'token' => '', + 'is_public' => 0, + 'is_private' => empty($project['is_private']) ? 0 : 1, + ); + + if (! $this->db->table(Project::TABLE)->save($values)) { + return 0; + } + + return $this->db->getConnection()->getLastId(); + } + + /** + * Clone a project with all settings + * + * @param integer $project_id Project Id + * @param array $part_selection Selection of optional project parts to duplicate. Possible options: 'swimlane', 'action', 'category', 'task' + * @return integer Cloned Project Id + */ + public function duplicate($project_id, $part_selection = array('category', 'action')) + { + $this->db->startTransaction(); + + // Get the cloned project Id + $clone_project_id = $this->copy($project_id); + + if (! $clone_project_id) { + $this->db->cancelTransaction(); + return false; + } + + // Clone Columns, Categories, Permissions and Actions + $optional_parts = array('swimlane', 'action', 'category'); + foreach (array('board', 'category', 'projectPermission', 'action', 'swimlane') as $model) { + + // Skip if optional part has not been selected + if (in_array($model, $optional_parts) && ! in_array($model, $part_selection)) { + continue; + } + + if (! $this->$model->duplicate($project_id, $clone_project_id)) { + $this->db->cancelTransaction(); + return false; + } + } + + $this->db->closeTransaction(); + + // Clone Tasks if in $part_selection + if (in_array('task', $part_selection)) { + $tasks = $this->taskFinder->getAll($project_id); + + foreach ($tasks as $task) { + if (! $this->taskDuplication->duplicateToProject($task['id'], $clone_project_id)) { + return false; + } + } + } + + return (int) $clone_project_id; + } +} diff --git a/sources/app/Model/ProjectPaginator.php b/sources/app/Model/ProjectPaginator.php deleted file mode 100644 index 68b216b..0000000 --- a/sources/app/Model/ProjectPaginator.php +++ /dev/null @@ -1,50 +0,0 @@ -db - ->table(Project::TABLE) - ->in('id', $project_ids) - ->offset($offset) - ->limit($limit) - ->orderBy($column, $direction) - ->findAll(); - - foreach ($projects as &$project) { - - $project['columns'] = $this->board->getColumns($project['id']); - $stats = $this->board->getColumnStats($project['id']); - - foreach ($project['columns'] as &$column) { - $column['nb_tasks'] = isset($stats[$column['id']]) ? $stats[$column['id']] : 0; - } - } - - return $projects; - } -} diff --git a/sources/app/Model/ProjectPermission.php b/sources/app/Model/ProjectPermission.php index 02f3b42..12bd930 100644 --- a/sources/app/Model/ProjectPermission.php +++ b/sources/app/Model/ProjectPermission.php @@ -313,12 +313,62 @@ class ProjectPermission extends Base * @return array */ public function getMemberProjects($user_id) + { + return $this->db + ->hashtable(Project::TABLE) + ->eq('user_id', $user_id) + ->join(self::TABLE, 'project_id', 'id') + ->getAll('projects.id', 'name'); + } + + /** + * Return a list of project ids where the user is member + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getMemberProjectIds($user_id) { return $this->db ->table(Project::TABLE) ->eq('user_id', $user_id) ->join(self::TABLE, 'project_id', 'id') - ->listing('projects.id', 'name'); + ->findAllByColumn('projects.id'); + } + + /** + * Return a list of active project ids where the user is member + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getActiveMemberProjectIds($user_id) + { + return $this->db + ->table(Project::TABLE) + ->eq('user_id', $user_id) + ->eq(Project::TABLE.'.is_active', Project::ACTIVE) + ->join(self::TABLE, 'project_id', 'id') + ->findAllByColumn('projects.id'); + } + + /** + * Return a list of active projects where the user is member + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getActiveMemberProjects($user_id) + { + return $this->db + ->hashtable(Project::TABLE) + ->eq('user_id', $user_id) + ->eq(Project::TABLE.'.is_active', Project::ACTIVE) + ->join(self::TABLE, 'project_id', 'id') + ->getAll('projects.id', 'name'); } /** diff --git a/sources/app/Model/SubTask.php b/sources/app/Model/SubTask.php index 1c5d1bf..048594b 100644 --- a/sources/app/Model/SubTask.php +++ b/sources/app/Model/SubTask.php @@ -12,7 +12,7 @@ use SimpleValidator\Validators; * @package model * @author Frederic Guillot */ -class SubTask extends Base +class Subtask extends Base { /** * SQL table name @@ -65,6 +65,49 @@ class SubTask extends Base ); } + /** + * Add subtask status status to the resultset + * + * @access public + * @param array $subtasks Subtasks + * @return array + */ + public function addStatusName(array $subtasks) + { + $status = $this->getStatusList(); + + foreach ($subtasks as &$subtask) { + $subtask['status_name'] = $status[$subtask['status']]; + } + + return $subtasks; + } + + /** + * Get the query to fetch subtasks assigned to a user + * + * @access public + * @param integer $user_id User id + * @param array $status List of status + * @return \PicoDb\Table + */ + public function getUserQuery($user_id, array $status) + { + return $this->db->table(Subtask::TABLE) + ->columns( + Subtask::TABLE.'.*', + Task::TABLE.'.project_id', + Task::TABLE.'.color_id', + Project::TABLE.'.name AS project_name' + ) + ->eq('user_id', $user_id) + ->eq(Project::TABLE.'.is_active', Project::ACTIVE) + ->in(Subtask::TABLE.'.status', $status) + ->join(Task::TABLE, 'id', 'task_id') + ->join(Project::TABLE, 'id', 'project_id', Task::TABLE) + ->filter(array($this, 'addStatusName')); + } + /** * Get all subtasks for a given task * @@ -74,19 +117,14 @@ class SubTask extends Base */ public function getAll($task_id) { - $status = $this->getStatusList(); - $subtasks = $this->db->table(self::TABLE) - ->eq('task_id', $task_id) - ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name') - ->join(User::TABLE, 'id', 'user_id') - ->asc(self::TABLE.'.id') - ->findAll(); - - foreach ($subtasks as &$subtask) { - $subtask['status_name'] = $status[$subtask['status']]; - } - - return $subtasks; + return $this->db + ->table(self::TABLE) + ->eq('task_id', $task_id) + ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name') + ->join(User::TABLE, 'id', 'user_id') + ->asc(self::TABLE.'.id') + ->filter(array($this, 'addStatusName')) + ->findAll(); } /** @@ -101,18 +139,13 @@ class SubTask extends Base { if ($more) { - $subtask = $this->db->table(self::TABLE) - ->eq(self::TABLE.'.id', $subtask_id) - ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name') - ->join(User::TABLE, 'id', 'user_id') - ->findOne(); - - if ($subtask) { - $status = $this->getStatusList(); - $subtask['status_name'] = $status[$subtask['status']]; - } - - return $subtask; + return $this->db + ->table(self::TABLE) + ->eq(self::TABLE.'.id', $subtask_id) + ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name') + ->join(User::TABLE, 'id', 'user_id') + ->filter(array($this, 'addStatusName')) + ->findOne(); } return $this->db->table(self::TABLE)->eq('id', $subtask_id)->findOne(); @@ -165,6 +198,7 @@ class SubTask extends Base $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); if ($result) { + $this->container['dispatcher']->dispatch( self::EVENT_UPDATE, new SubtaskEvent($values) @@ -196,6 +230,37 @@ class SubTask extends Base return $this->update($values); } + /** + * Get the subtask in progress for this user + * + * @access public + * @param integer $user_id + * @return array + */ + public function getSubtaskInProgress($user_id) + { + return $this->db->table(self::TABLE) + ->eq('status', self::STATUS_INPROGRESS) + ->eq('user_id', $user_id) + ->findOne(); + } + + /** + * Return true if the user have a subtask in progress + * + * @access public + * @param integer $user_id + * @return boolean + */ + public function hasSubtaskInProgress($user_id) + { + return $this->config->get('subtask_restriction') == 1 && + $this->db->table(self::TABLE) + ->eq('status', self::STATUS_INPROGRESS) + ->eq('user_id', $user_id) + ->count() === 1; + } + /** * Remove * @@ -220,7 +285,7 @@ class SubTask extends Base { return $this->db->transaction(function ($db) use ($src_task_id, $dst_task_id) { - $subtasks = $db->table(SubTask::TABLE) + $subtasks = $db->table(Subtask::TABLE) ->columns('title', 'time_estimated') ->eq('task_id', $src_task_id) ->asc('id') // Explicit sorting for postgresql @@ -230,7 +295,7 @@ class SubTask extends Base $subtask['task_id'] = $dst_task_id; - if (! $db->table(SubTask::TABLE)->save($subtask)) { + if (! $db->table(Subtask::TABLE)->save($subtask)) { return false; } } diff --git a/sources/app/Model/SubtaskExport.php b/sources/app/Model/SubtaskExport.php index 50b028e..23dcc01 100644 --- a/sources/app/Model/SubtaskExport.php +++ b/sources/app/Model/SubtaskExport.php @@ -29,7 +29,7 @@ class SubtaskExport extends Base */ public function export($project_id, $from, $to) { - $this->subtask_status = $this->subTask->getStatusList(); + $this->subtask_status = $this->subtask->getStatusList(); $subtasks = $this->getSubtasks($project_id, $from, $to); $results = array($this->getColumns()); @@ -86,25 +86,25 @@ class SubtaskExport extends Base * Get all subtasks for a given project * * @access public - * @param integer $task_id Task id - * @param mixed $from Start date (timestamp or user formatted date) - * @param mixed $to End date (timestamp or user formatted date) + * @param integer $project_id Project id + * @param mixed $from Start date (timestamp or user formatted date) + * @param mixed $to End date (timestamp or user formatted date) * @return array */ public function getSubtasks($project_id, $from, $to) { if (! is_numeric($from)) { - $from = $this->dateParser->resetDateToMidnight($this->dateParser->getTimestamp($from)); + $from = $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($from)); } if (! is_numeric($to)) { - $to = $this->dateParser->resetDateToMidnight(strtotime('+1 day', $this->dateParser->getTimestamp($to))); + $to = $this->dateParser->removeTimeFromTimestamp(strtotime('+1 day', $this->dateParser->getTimestamp($to))); } - return $this->db->table(SubTask::TABLE) + return $this->db->table(Subtask::TABLE) ->eq('project_id', $project_id) ->columns( - SubTask::TABLE.'.*', + Subtask::TABLE.'.*', User::TABLE.'.username AS assignee_username', User::TABLE.'.name AS assignee_name', Task::TABLE.'.title AS task_title' @@ -113,7 +113,7 @@ class SubtaskExport extends Base ->lte('date_creation', $to) ->join(Task::TABLE, 'id', 'task_id') ->join(User::TABLE, 'id', 'user_id') - ->asc(SubTask::TABLE.'.id') + ->asc(Subtask::TABLE.'.id') ->findAll(); } } diff --git a/sources/app/Model/SubtaskPaginator.php b/sources/app/Model/SubtaskPaginator.php deleted file mode 100644 index 8ccbd69..0000000 --- a/sources/app/Model/SubtaskPaginator.php +++ /dev/null @@ -1,68 +0,0 @@ -subTask->getStatusList(); - - $subtasks = $this->db->table(SubTask::TABLE) - ->columns( - SubTask::TABLE.'.*', - Task::TABLE.'.project_id', - Task::TABLE.'.color_id', - Project::TABLE.'.name AS project_name' - ) - ->eq('user_id', $user_id) - ->in(SubTask::TABLE.'.status', $status) - ->join(Task::TABLE, 'id', 'task_id') - ->join(Project::TABLE, 'id', 'project_id', Task::TABLE) - ->offset($offset) - ->limit($limit) - ->orderBy($column, $direction) - ->findAll(); - - foreach ($subtasks as &$subtask) { - $subtask['status_name'] = $status_list[$subtask['status']]; - } - - return $subtasks; - } - - /** - * Count all subtasks assigned to the user - * - * @access public - * @param integer $user_id User id - * @param array $status List of status - * @return integer - */ - public function countUserSubtasks($user_id, array $status) - { - return $this->db - ->table(SubTask::TABLE) - ->eq('user_id', $user_id) - ->in('status', $status) - ->count(); - } -} diff --git a/sources/app/Model/SubtaskTimeTracking.php b/sources/app/Model/SubtaskTimeTracking.php new file mode 100644 index 0000000..8b197c4 --- /dev/null +++ b/sources/app/Model/SubtaskTimeTracking.php @@ -0,0 +1,319 @@ +db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.subtask_id', + self::TABLE.'.end', + self::TABLE.'.start', + Subtask::TABLE.'.task_id', + Subtask::TABLE.'.title AS subtask_title', + Task::TABLE.'.title AS task_title', + Task::TABLE.'.project_id', + Task::TABLE.'.color_id' + ) + ->join(Subtask::TABLE, 'id', 'subtask_id') + ->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE) + ->eq(self::TABLE.'.user_id', $user_id); + } + + /** + * Get query for task timesheet (pagination) + * + * @access public + * @param integer $task_id Task id + * @return \PicoDb\Table + */ + public function getTaskQuery($task_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.subtask_id', + self::TABLE.'.end', + self::TABLE.'.start', + self::TABLE.'.user_id', + Subtask::TABLE.'.task_id', + Subtask::TABLE.'.title AS subtask_title', + Task::TABLE.'.project_id', + User::TABLE.'.username', + User::TABLE.'.name AS user_fullname' + ) + ->join(Subtask::TABLE, 'id', 'subtask_id') + ->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE) + ->join(User::TABLE, 'id', 'user_id', self::TABLE) + ->eq(Task::TABLE.'.id', $task_id); + } + + /** + * Get query for project timesheet (pagination) + * + * @access public + * @param integer $project_id Project id + * @return \PicoDb\Table + */ + public function getProjectQuery($project_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.subtask_id', + self::TABLE.'.end', + self::TABLE.'.start', + self::TABLE.'.user_id', + Subtask::TABLE.'.task_id', + Subtask::TABLE.'.title AS subtask_title', + Task::TABLE.'.project_id', + Task::TABLE.'.color_id', + User::TABLE.'.username', + User::TABLE.'.name AS user_fullname' + ) + ->join(Subtask::TABLE, 'id', 'subtask_id') + ->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE) + ->join(User::TABLE, 'id', 'user_id', self::TABLE) + ->eq(Task::TABLE.'.project_id', $project_id); + } + + /** + * Get all recorded time slots for a given user + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getUserTimesheet($user_id) + { + return $this->db + ->table(self::TABLE) + ->eq('user_id', $user_id) + ->findAll(); + } + + /** + * Get user calendar events + * + * @access public + * @param integer $user_id + * @param integer $start + * @param integer $end + * @return array + */ + public function getUserCalendarEvents($user_id, $start, $end) + { + $result = $this->getUserQuery($user_id) + ->addCondition($this->getCalendarCondition($start, $end)) + ->findAll(); + + return $this->toCalendarEvents($result); + } + + /** + * Get project calendar events + * + * @access public + * @param integer $project_id + * @param integer $start + * @param integer $end + * @return array + */ + public function getProjectCalendarEvents($project_id, $start, $end) + { + $result = $this->getProjectQuery($project_id) + ->addCondition($this->getCalendarCondition($start, $end)) + ->findAll(); + + return $this->toCalendarEvents($result); + } + + /** + * Get time slots that should be displayed in the calendar time range + * + * @access private + * @param string $start ISO8601 start date + * @param string $end ISO8601 end date + * @return string + */ + private function getCalendarCondition($start, $end) + { + $start_time = $this->dateParser->getTimestampFromIsoFormat($start); + $end_time = $this->dateParser->getTimestampFromIsoFormat($end); + $start_column = $this->db->escapeIdentifier('start'); + $end_column = $this->db->escapeIdentifier('end'); + + $conditions = array( + "($start_column >= '$start_time' AND $start_column <= '$end_time')", + "($start_column <= '$start_time' AND $end_column >= '$start_time')", + "($start_column <= '$start_time' AND $end_column = '0')", + ); + + return '('.implode(' OR ', $conditions).')'; + } + + /** + * Convert a record set to calendar events + * + * @access private + * @param array $rows + * @return array + */ + private function toCalendarEvents(array $rows) + { + $events = array(); + + foreach ($rows as $row) { + + $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : ''; + + $events[] = array( + 'id' => $row['id'], + 'subtask_id' => $row['subtask_id'], + 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user, + 'start' => date('Y-m-d\TH:i:s', $row['start']), + 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()), + 'backgroundColor' => $this->color->getBackgroundColor($row['color_id']), + 'borderColor' => $this->color->getBorderColor($row['color_id']), + 'textColor' => 'black', + 'url' => $this->helper->url('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])), + 'editable' => false, + ); + } + + return $events; + } + + /** + * Log start time + * + * @access public + * @param integer $subtask_id + * @param integer $user_id + * @return boolean + */ + public function logStartTime($subtask_id, $user_id) + { + return $this->db + ->table(self::TABLE) + ->insert(array('subtask_id' => $subtask_id, 'user_id' => $user_id, 'start' => time())); + } + + /** + * Log end time + * + * @access public + * @param integer $subtask_id + * @param integer $user_id + * @return boolean + */ + public function logEndTime($subtask_id, $user_id) + { + $this->updateSubtaskTimeSpent($subtask_id, $user_id); + + return $this->db + ->table(self::TABLE) + ->eq('subtask_id', $subtask_id) + ->eq('user_id', $user_id) + ->eq('end', 0) + ->update(array( + 'end' => time() + )); + } + + /** + * Update task time tracking based on subtasks time tracking + * + * @access public + * @param integer $task_id Task id + * @return bool + */ + public function updateTaskTimeTracking($task_id) + { + $result = $this->calculateSubtaskTime($task_id); + + if (empty($result['total_spent']) && empty($result['total_estimated'])) { + return true; + } + + return $this->db + ->table(Task::TABLE) + ->eq('id', $task_id) + ->update(array( + 'time_spent' => $result['total_spent'], + 'time_estimated' => $result['total_estimated'], + )); + } + + /** + * Sum time spent and time estimated for all subtasks + * + * @access public + * @param integer $task_id Task id + * @return array + */ + public function calculateSubtaskTime($task_id) + { + return $this->db + ->table(Subtask::TABLE) + ->eq('task_id', $task_id) + ->columns( + 'SUM(time_spent) AS total_spent', + 'SUM(time_estimated) AS total_estimated' + ) + ->findOne(); + } + + /** + * Update subtask time spent based on the punch clock table + * + * @access public + * @param integer $subtask_id + * @param integer $user_id + * @return bool + */ + public function updateSubtaskTimeSpent($subtask_id, $user_id) + { + $start_time = $this->db + ->table(self::TABLE) + ->eq('subtask_id', $subtask_id) + ->eq('user_id', $user_id) + ->eq('end', 0) + ->findOneColumn('start'); + + $subtask = $this->subtask->getById($subtask_id); + + return $start_time && + $this->subtask->update(array( // Fire the event subtask.update + 'id' => $subtask['id'], + 'time_spent' => $subtask['time_spent'] + round((time() - $start_time) / 3600, 1), + 'task_id' => $subtask['task_id'], + )); + } +} diff --git a/sources/app/Model/Swimlane.php b/sources/app/Model/Swimlane.php index 069f14b..c9bc43e 100644 --- a/sources/app/Model/Swimlane.php +++ b/sources/app/Model/Swimlane.php @@ -161,20 +161,20 @@ class Swimlane extends Base * * @access public * @param integer $project_id Project id + * @param boolean $prepend Prepend default value * @return array */ - public function getSwimlanesList($project_id) + public function getList($project_id, $prepend = false) { - $swimlanes = $this->db->table(self::TABLE) - ->eq('project_id', $project_id) - ->orderBy('position', 'asc') - ->listing('id', 'name'); + $swimlanes = array(); + $swimlanes[] = $this->db->table(Project::TABLE)->eq('id', $project_id)->findOneColumn('default_swimlane'); - $swimlanes[0] = $this->db->table(Project::TABLE) - ->eq('id', $project_id) - ->findOneColumn('default_swimlane'); + $swimlanes = array_merge( + $swimlanes, + $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->orderBy('name', 'asc')->getAll('id', 'name') + ); - return $swimlanes; + return $prepend ? array(-1 => t('All swimlanes')) + $swimlanes : $swimlanes; } /** @@ -183,7 +183,7 @@ class Swimlane extends Base * @access public * @param integer $project_id * @param string $name - * @return bool + * @return integer|boolean */ public function create($project_id, $name) { @@ -354,11 +354,11 @@ class Swimlane extends Base */ public function moveDown($project_id, $swimlane_id) { - $swimlanes = $this->db->table(self::TABLE) + $swimlanes = $this->db->hashtable(self::TABLE) ->eq('project_id', $project_id) ->eq('is_active', self::ACTIVE) ->asc('position') - ->listing('id', 'position'); + ->getAll('id', 'position'); $positions = array_flip($swimlanes); @@ -388,11 +388,11 @@ class Swimlane extends Base */ public function moveUp($project_id, $swimlane_id) { - $swimlanes = $this->db->table(self::TABLE) + $swimlanes = $this->db->hashtable(self::TABLE) ->eq('project_id', $project_id) ->eq('is_active', self::ACTIVE) ->asc('position') - ->listing('id', 'position'); + ->getAll('id', 'position'); $positions = array_flip($swimlanes); @@ -412,6 +412,37 @@ class Swimlane extends Base return false; } + /** + * Duplicate Swimlane to project + * + * @access public + * @param integer $project_from Project Template + * @param integer $project_to Project that receives the copy + * @return integer|boolean + */ + + public function duplicate($project_from, $project_to) + { + $swimlanes = $this->getAll($project_from); + + foreach ($swimlanes as $swimlane) { + + unset($swimlane['id']); + $swimlane['project_id'] = $project_to; + + if (! $this->db->table(self::TABLE)->save($swimlane)) { + return false; + } + } + + $default_swimlane = $this->getDefault($project_from); + $default_swimlane['id'] = $project_to; + + $this->updateDefault($default_swimlane); + + return true; + } + /** * Validate creation * diff --git a/sources/app/Model/TaskCreation.php b/sources/app/Model/TaskCreation.php index 17e5ff7..893cbc4 100644 --- a/sources/app/Model/TaskCreation.php +++ b/sources/app/Model/TaskCreation.php @@ -62,6 +62,7 @@ class TaskCreation extends Base $values['swimlane_id'] = empty($values['swimlane_id']) ? 0 : $values['swimlane_id']; $values['date_creation'] = time(); $values['date_modification'] = $values['date_creation']; + $values['date_moved'] = $values['date_creation']; $values['position'] = $this->taskFinder->countByColumnAndSwimlaneId($values['project_id'], $values['column_id'], $values['swimlane_id']) + 1; } diff --git a/sources/app/Model/TaskDuplication.php b/sources/app/Model/TaskDuplication.php index 172edb9..bd593dc 100644 --- a/sources/app/Model/TaskDuplication.php +++ b/sources/app/Model/TaskDuplication.php @@ -158,7 +158,7 @@ class TaskDuplication extends Base $new_task_id = $this->taskCreation->create($values); if ($new_task_id) { - $this->subTask->duplicate($task_id, $new_task_id); + $this->subtask->duplicate($task_id, $new_task_id); } return $new_task_id; diff --git a/sources/app/Model/TaskExport.php b/sources/app/Model/TaskExport.php index 1592deb..90aa196 100644 --- a/sources/app/Model/TaskExport.php +++ b/sources/app/Model/TaskExport.php @@ -24,7 +24,7 @@ class TaskExport extends Base public function export($project_id, $from, $to) { $tasks = $this->getTasks($project_id, $from, $to); - $swimlanes = $this->swimlane->getSwimlanesList($project_id); + $swimlanes = $this->swimlane->getList($project_id); $results = array($this->getColumns()); foreach ($tasks as &$task) { @@ -77,11 +77,11 @@ class TaskExport extends Base '; if (! is_numeric($from)) { - $from = $this->dateParser->resetDateToMidnight($this->dateParser->getTimestamp($from)); + $from = $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($from)); } if (! is_numeric($to)) { - $to = $this->dateParser->resetDateToMidnight(strtotime('+1 day', $this->dateParser->getTimestamp($to))); + $to = $this->dateParser->removeTimeFromTimestamp(strtotime('+1 day', $this->dateParser->getTimestamp($to))); } $rq = $this->db->execute($sql, array($from, $to, $project_id)); diff --git a/sources/app/Model/TaskFilter.php b/sources/app/Model/TaskFilter.php new file mode 100644 index 0000000..31de279 --- /dev/null +++ b/sources/app/Model/TaskFilter.php @@ -0,0 +1,150 @@ +query = $this->db->table(Task::TABLE); + return $this; + } + + public function excludeTasks(array $task_ids) + { + $this->query->notin('id', $task_ids); + return $this; + } + + public function filterByTitle($title) + { + $this->query->ilike('title', '%'.$title.'%'); + return $this; + } + + public function filterByProjects(array $project_ids) + { + $this->query->in('project_id', $project_ids); + return $this; + } + + public function filterByProject($project_id) + { + if ($project_id > 0) { + $this->query->eq('project_id', $project_id); + } + + return $this; + } + + public function filterByCategory($category_id) + { + if ($category_id >= 0) { + $this->query->eq('category_id', $category_id); + } + + return $this; + } + + public function filterByOwner($owner_id) + { + if ($owner_id >= 0) { + $this->query->eq('owner_id', $owner_id); + } + + return $this; + } + + public function filterByColor($color_id) + { + if ($color_id !== '') { + $this->query->eq('color_id', $color_id); + } + + return $this; + } + + public function filterByColumn($column_id) + { + if ($column_id >= 0) { + $this->query->eq('column_id', $column_id); + } + + return $this; + } + + public function filterBySwimlane($swimlane_id) + { + if ($swimlane_id >= 0) { + $this->query->eq('swimlane_id', $swimlane_id); + } + + return $this; + } + + public function filterByStatus($is_active) + { + if ($is_active >= 0) { + $this->query->eq('is_active', $is_active); + } + + return $this; + } + + public function filterByDueDateRange($start, $end) + { + $this->query->gte('date_due', $this->dateParser->getTimestampFromIsoFormat($start)); + $this->query->lte('date_due', $this->dateParser->getTimestampFromIsoFormat($end)); + + return $this; + } + + public function findAll() + { + return $this->query->findAll(); + } + + public function toAutoCompletion() + { + return $this->query->columns('id', 'title')->filter(function(array $results) { + + foreach ($results as &$result) { + $result['value'] = $result['title']; + $result['label'] = '#'.$result['id'].' - '.$result['title']; + } + + return $results; + + })->findAll(); + } + + public function toCalendarEvents() + { + $events = array(); + + foreach ($this->query->findAll() as $task) { + $events[] = array( + 'timezoneParam' => $this->config->getCurrentTimezone(), + 'id' => $task['id'], + 'title' => t('#%d', $task['id']).' '.$task['title'], + 'start' => date('Y-m-d', $task['date_due']), + 'end' => date('Y-m-d', $task['date_due']), + 'allday' => true, + 'backgroundColor' => $this->color->getBackgroundColor($task['color_id']), + 'borderColor' => $this->color->getBorderColor($task['color_id']), + 'textColor' => 'black', + 'url' => $this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), + ); + } + + return $events; + } +} diff --git a/sources/app/Model/TaskFinder.php b/sources/app/Model/TaskFinder.php index eb86fe3..98ece4e 100644 --- a/sources/app/Model/TaskFinder.php +++ b/sources/app/Model/TaskFinder.php @@ -13,12 +13,69 @@ use PDO; class TaskFinder extends Base { /** - * Common request to fetch a list of tasks + * Get query for closed tasks + * + * @access public + * @param integer $project_id Project id + * @return \PicoDb\Table + */ + public function getClosedTaskQuery($project_id) + { + return $this->getExtendedQuery() + ->eq('project_id', $project_id) + ->eq('is_active', Task::STATUS_CLOSED); + } + + /** + * Get query for task search + * + * @access public + * @param integer $project_id Project id + * @param string $search Search terms + * @return \PicoDb\Table + */ + public function getSearchQuery($project_id, $search) + { + return $this->getExtendedQuery() + ->eq('project_id', $project_id) + ->ilike('title', '%'.$search.'%'); + } + + /** + * Get query for assigned user tasks + * + * @access public + * @param integer $user_id User id + * @return \PicoDb\Table + */ + public function getUserQuery($user_id) + { + return $this->db + ->table(Task::TABLE) + ->columns( + 'tasks.id', + 'tasks.title', + 'tasks.date_due', + 'tasks.date_creation', + 'tasks.project_id', + 'tasks.color_id', + 'tasks.time_spent', + 'tasks.time_estimated', + 'projects.name AS project_name' + ) + ->join(Project::TABLE, 'id', 'project_id') + ->eq(Task::TABLE.'.owner_id', $user_id) + ->eq(Task::TABLE.'.is_active', Task::STATUS_OPEN) + ->eq(Project::TABLE.'.is_active', Project::ACTIVE); + } + + /** + * Extended query * * @access public * @return \PicoDb\Table */ - public function getQuery() + public function getExtendedQuery() { return $this->db ->table(Task::TABLE) @@ -27,6 +84,7 @@ class TaskFinder extends Base '(SELECT count(*) FROM task_has_files WHERE task_id=tasks.id) AS nb_files', '(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id) AS nb_subtasks', '(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id AND status=2) AS nb_completed_subtasks', + '(SELECT count(*) FROM ' . TaskLink::TABLE . ' WHERE ' . TaskLink::TABLE . '.task_id = tasks.id) AS nb_links', 'tasks.id', 'tasks.reference', 'tasks.title', @@ -45,6 +103,7 @@ class TaskFinder extends Base 'tasks.is_active', 'tasks.score', 'tasks.category_id', + 'tasks.date_moved', 'users.username AS assignee_username', 'users.name AS assignee_name' ) @@ -62,7 +121,7 @@ class TaskFinder extends Base */ public function getTasksByColumnAndSwimlane($project_id, $column_id, $swimlane_id = 0) { - return $this->getQuery() + return $this->getExtendedQuery() ->eq('project_id', $project_id) ->eq('column_id', $column_id) ->eq('swimlane_id', $swimlane_id) @@ -117,6 +176,18 @@ class TaskFinder extends Base return $tasks; } + /** + * Get project id for a given task + * + * @access public + * @param integer $task_id Task id + * @return integer + */ + public function getProjectId($task_id) + { + return (int) $this->db->table(Task::TABLE)->eq('id', $task_id)->findOneColumn('project_id') ?: 0; + } + /** * Fetch a task by the id * @@ -173,6 +244,7 @@ class TaskFinder extends Base tasks.score, tasks.category_id, tasks.swimlane_id, + tasks.date_moved, project_has_categories.name AS category_name, projects.name AS project_name, columns.title AS column_title, diff --git a/sources/app/Model/TaskLink.php b/sources/app/Model/TaskLink.php new file mode 100644 index 0000000..f8e9f99 --- /dev/null +++ b/sources/app/Model/TaskLink.php @@ -0,0 +1,144 @@ +db->table(self::TABLE)->eq('id', $task_link_id)->findOne(); + } + + /** + * Get all links attached to a task + * + * @access public + * @param integer $task_id Task id + * @return array + */ + public function getLinks($task_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.opposite_task_id AS task_id', + Link::TABLE.'.label', + Task::TABLE.'.title', + Task::TABLE.'.is_active', + Task::TABLE.'.project_id', + Board::TABLE.'.title AS column_title' + ) + ->eq(self::TABLE.'.task_id', $task_id) + ->join(Link::TABLE, 'id', 'link_id') + ->join(Task::TABLE, 'id', 'opposite_task_id') + ->join(Board::TABLE, 'id', 'column_id', Task::TABLE) + ->findAll(); + } + + /** + * Create a new link + * + * @access public + * @param integer $task_id Task id + * @param integer $opposite_task_id Opposite task id + * @param integer $link_id Link id + * @return boolean + */ + public function create($task_id, $opposite_task_id, $link_id) + { + $this->db->startTransaction(); + + // Create the original link + $this->db->table(self::TABLE)->insert(array( + 'task_id' => $task_id, + 'opposite_task_id' => $opposite_task_id, + 'link_id' => $link_id, + )); + + $link_id = $this->link->getOppositeLinkId($link_id); + + // Create the opposite link + $this->db->table(self::TABLE)->insert(array( + 'task_id' => $opposite_task_id, + 'opposite_task_id' => $task_id, + 'link_id' => $link_id, + )); + + $this->db->closeTransaction(); + + return true; + } + + /** + * Remove a link between two tasks + * + * @access public + * @param integer $task_link_id + * @return boolean + */ + public function remove($task_link_id) + { + $this->db->startTransaction(); + + $link = $this->getById($task_link_id); + $link_id = $this->link->getOppositeLinkId($link['link_id']); + + $this->db->table(self::TABLE)->eq('id', $task_link_id)->remove(); + + $this->db + ->table(self::TABLE) + ->eq('opposite_task_id', $link['task_id']) + ->eq('task_id', $link['opposite_task_id']) + ->eq('link_id', $link_id)->remove(); + + $this->db->closeTransaction(); + + return true; + } + + /** + * Validate creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('task_id', t('Field required')), + new Validators\Required('link_id', t('Field required')), + new Validators\Required('title', t('Field required')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/sources/app/Model/TaskPaginator.php b/sources/app/Model/TaskPaginator.php deleted file mode 100644 index e810922..0000000 --- a/sources/app/Model/TaskPaginator.php +++ /dev/null @@ -1,138 +0,0 @@ -taskFinder->getQuery() - ->eq('project_id', $project_id) - ->ilike('title', '%'.$search.'%') - ->offset($offset) - ->limit($limit) - ->orderBy($column, $direction) - ->findAll(); - } - - /** - * Count the number of tasks for a custom search - * - * @access public - * @param integer $project_id Project id - * @param string $search Search terms - * @return integer - */ - public function countSearchTasks($project_id, $search) - { - return $this->db->table(Task::TABLE) - ->eq('project_id', $project_id) - ->ilike('title', '%'.$search.'%') - ->count(); - } - - /** - * Get all completed tasks with pagination - * - * @access public - * @param integer $project_id Project id - * @param integer $offset Offset - * @param integer $limit Limit - * @param string $column Sorting column - * @param string $direction Sorting direction - * @return array - */ - public function closedTasks($project_id, $offset = 0, $limit = 25, $column = 'tasks.date_completed', $direction = 'DESC') - { - return $this->taskFinder->getQuery() - ->eq('project_id', $project_id) - ->eq('is_active', Task::STATUS_CLOSED) - ->offset($offset) - ->limit($limit) - ->orderBy($column, $direction) - ->findAll(); - } - - /** - * Count all closed tasks - * - * @access public - * @param integer $project_id Project id - * @return integer - */ - public function countClosedTasks($project_id) - { - return $this->db - ->table(Task::TABLE) - ->eq('project_id', $project_id) - ->eq('is_active', Task::STATUS_CLOSED) - ->count(); - } - - /** - * Get all open tasks for a given user - * - * @access public - * @param integer $user_id User id - * @param integer $offset Offset - * @param integer $limit Limit - * @param string $column Sorting column - * @param string $direction Sorting direction - * @return array - */ - public function userTasks($user_id, $offset = 0, $limit = 25, $column = 'tasks.id', $direction = 'ASC') - { - return $this->db - ->table(Task::TABLE) - ->columns( - 'tasks.id', - 'tasks.title', - 'tasks.date_due', - 'tasks.date_creation', - 'tasks.project_id', - 'tasks.color_id', - 'projects.name AS project_name' - ) - ->join(Project::TABLE, 'id', 'project_id') - ->eq('tasks.owner_id', $user_id) - ->eq('tasks.is_active', Task::STATUS_OPEN) - ->offset($offset) - ->limit($limit) - ->orderBy($column, $direction) - ->findAll(); - } - - /** - * Count all tasks assigned to the user - * - * @access public - * @param integer $user_id User id - * @return integer - */ - public function countUserTasks($user_id) - { - return $this->db - ->table(Task::TABLE) - ->eq('owner_id', $user_id) - ->eq('is_active', Task::STATUS_OPEN) - ->count(); - } -} diff --git a/sources/app/Model/TaskPosition.php b/sources/app/Model/TaskPosition.php index 2c271de..1e49543 100644 --- a/sources/app/Model/TaskPosition.php +++ b/sources/app/Model/TaskPosition.php @@ -30,7 +30,7 @@ class TaskPosition extends Base $result = $this->calculateAndSave($project_id, $task_id, $column_id, $position, $swimlane_id); if ($result) { - + if ($original_task['swimlane_id'] != $swimlane_id) { $this->calculateAndSave($project_id, 0, $column_id, 1, $original_task['swimlane_id']); } diff --git a/sources/app/Model/TaskStatus.php b/sources/app/Model/TaskStatus.php index 225b393..30a65e1 100644 --- a/sources/app/Model/TaskStatus.php +++ b/sources/app/Model/TaskStatus.php @@ -12,6 +12,23 @@ use Event\TaskEvent; */ class TaskStatus extends Base { + /** + * Return the list of statuses + * + * @access public + * @param boolean $prepend Prepend default value + * @return array + */ + public function getList($prepend = false) + { + $listing = $prepend ? array(-1 => t('All status')) : array(); + + return $listing + array( + Task::STATUS_OPEN => t('Open'), + Task::STATUS_CLOSED => t('Closed'), + ); + } + /** * Return true if the task is closed * diff --git a/sources/app/Model/TimeTracking.php b/sources/app/Model/TimeTracking.php deleted file mode 100644 index 4ddddf1..0000000 --- a/sources/app/Model/TimeTracking.php +++ /dev/null @@ -1,45 +0,0 @@ - 0, - 'time_estimated' => 0, - 'time_remaining' => 0, - ); - - foreach ($subtasks as &$subtask) { - $timesheet['time_estimated'] += $subtask['time_estimated']; - $timesheet['time_spent'] += $subtask['time_spent']; - } - - if ($timesheet['time_estimated'] == 0 && $timesheet['time_spent'] == 0) { - $timesheet['time_estimated'] = $task['time_estimated']; - $timesheet['time_spent'] = $task['time_spent']; - } - - $timesheet['time_remaining'] = $timesheet['time_estimated'] - $timesheet['time_spent']; - - return $timesheet; - } -} diff --git a/sources/app/Model/User.php b/sources/app/Model/User.php index 1bcc82b..7586f3c 100644 --- a/sources/app/Model/User.php +++ b/sources/app/Model/User.php @@ -28,6 +28,42 @@ class User extends Base */ const EVERYBODY_ID = -1; + /** + * Return true if the user exists + * + * @access public + * @param integer $user_id User id + * @return boolean + */ + public function exists($user_id) + { + return $this->db->table(self::TABLE)->eq('id', $user_id)->count() === 1; + } + + /** + * Get query to fetch all users + * + * @access public + * @return \PicoDb\Table + */ + public function getQuery() + { + return $this->db + ->table(self::TABLE) + ->columns( + 'id', + 'username', + 'name', + 'email', + 'is_admin', + 'default_project_id', + 'is_ldap_user', + 'notifications_enabled', + 'google_id', + 'github_id' + ); + } + /** * Return the full name * @@ -112,54 +148,7 @@ class User extends Base */ public function getAll() { - return $this->db - ->table(self::TABLE) - ->asc('username') - ->columns( - 'id', - 'username', - 'name', - 'email', - 'is_admin', - 'default_project_id', - 'is_ldap_user', - 'notifications_enabled', - 'google_id', - 'github_id' - ) - ->findAll(); - } - - /** - * Get all users with pagination - * - * @access public - * @param integer $offset Offset - * @param integer $limit Limit - * @param string $column Sorting column - * @param string $direction Sorting direction - * @return array - */ - public function paginate($offset = 0, $limit = 25, $column = 'username', $direction = 'ASC') - { - return $this->db - ->table(self::TABLE) - ->columns( - 'id', - 'username', - 'name', - 'email', - 'is_admin', - 'default_project_id', - 'is_ldap_user', - 'notifications_enabled', - 'google_id', - 'github_id' - ) - ->offset($offset) - ->limit($limit) - ->orderBy($column, $direction) - ->findAll(); + return $this->getQuery()->asc('username')->findAll(); } /** diff --git a/sources/app/Schema/Mysql.php b/sources/app/Schema/Mysql.php index 05e8f14..947a62b 100644 --- a/sources/app/Schema/Mysql.php +++ b/sources/app/Schema/Mysql.php @@ -4,8 +4,110 @@ namespace Schema; use PDO; use Core\Security; +use Model\Link; -const VERSION = 41; +const VERSION = 46; + +function version_46($pdo) +{ + $pdo->exec("CREATE TABLE links ( + id INT NOT NULL AUTO_INCREMENT, + label VARCHAR(255) NOT NULL, + opposite_id INT DEFAULT 0, + PRIMARY KEY(id), + UNIQUE(label) + ) ENGINE=InnoDB CHARSET=utf8"); + + $pdo->exec("CREATE TABLE task_has_links ( + id INT NOT NULL AUTO_INCREMENT, + link_id INT NOT NULL, + task_id INT NOT NULL, + opposite_task_id INT NOT NULL, + FOREIGN KEY(link_id) REFERENCES links(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(opposite_task_id) REFERENCES tasks(id) ON DELETE CASCADE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8"); + + $pdo->exec("CREATE INDEX task_has_links_task_index ON task_has_links(task_id)"); + $pdo->exec("CREATE UNIQUE INDEX task_has_links_unique ON task_has_links(link_id, task_id, opposite_task_id)"); + + $rq = $pdo->prepare('INSERT INTO links (label, opposite_id) VALUES (?, ?)'); + $rq->execute(array('relates to', 0)); + $rq->execute(array('blocks', 3)); + $rq->execute(array('is blocked by', 2)); + $rq->execute(array('duplicates', 5)); + $rq->execute(array('is duplicated by', 4)); + $rq->execute(array('is a child of', 7)); + $rq->execute(array('is a parent of', 6)); + $rq->execute(array('targets milestone', 9)); + $rq->execute(array('is a milestone of', 8)); + $rq->execute(array('fixes', 11)); + $rq->execute(array('is fixed by', 10)); +} + +function version_45($pdo) +{ + $pdo->exec('ALTER TABLE tasks ADD COLUMN date_moved INT DEFAULT 0'); + + /* Update tasks.date_moved from project_activities table if tasks.date_moved = null or 0. + * We take max project_activities.date_creation where event_name in task.create','task.move.column + * since creation date is always less than task moves + */ + $pdo->exec("UPDATE tasks + SET date_moved = ( + SELECT md + FROM ( + SELECT task_id, max(date_creation) md + FROM project_activities + WHERE event_name IN ('task.create', 'task.move.column') + GROUP BY task_id + ) src + WHERE id = src.task_id + ) + WHERE (date_moved IS NULL OR date_moved = 0) AND id IN ( + SELECT task_id + FROM ( + SELECT task_id, max(date_creation) md + FROM project_activities + WHERE event_name IN ('task.create', 'task.move.column') + GROUP BY task_id + ) src + )"); + + // If there is no activities for some tasks use the date_creation + $pdo->exec("UPDATE tasks SET date_moved = date_creation WHERE date_moved IS NULL OR date_moved = 0"); +} + +function version_44($pdo) +{ + $pdo->exec('ALTER TABLE users ADD COLUMN disable_login_form TINYINT(1) DEFAULT 0'); +} + +function version_43($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('subtask_restriction', '0')); + $rq->execute(array('subtask_time_tracking', '0')); + + $pdo->exec(" + CREATE TABLE subtask_time_tracking ( + id INT NOT NULL AUTO_INCREMENT, + user_id INT NOT NULL, + subtask_id INT NOT NULL, + start INT DEFAULT 0, + end INT DEFAULT 0, + PRIMARY KEY(id), + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(subtask_id) REFERENCES task_has_subtasks(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8 + "); +} + +function version_42($pdo) +{ + $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT'); +} function version_41($pdo) { @@ -59,7 +161,7 @@ function version_38($pdo) "); $pdo->exec('ALTER TABLE tasks ADD COLUMN swimlane_id INT DEFAULT 0'); - $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT '".t('Default swimlane')."'"); + $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT 'Default swimlane'"); $pdo->exec("ALTER TABLE projects ADD COLUMN show_default_swimlane INT DEFAULT 1"); } diff --git a/sources/app/Schema/Postgres.php b/sources/app/Schema/Postgres.php index 9d5aa7a..027401f 100644 --- a/sources/app/Schema/Postgres.php +++ b/sources/app/Schema/Postgres.php @@ -4,8 +4,107 @@ namespace Schema; use PDO; use Core\Security; +use Model\Link; -const VERSION = 22; +const VERSION = 27; + +function version_27($pdo) +{ + $pdo->exec('CREATE TABLE links ( + "id" SERIAL PRIMARY KEY, + "label" VARCHAR(255) NOT NULL, + "opposite_id" INTEGER DEFAULT 0, + UNIQUE("label") + )'); + + $pdo->exec("CREATE TABLE task_has_links ( + id SERIAL PRIMARY KEY, + link_id INTEGER NOT NULL, + task_id INTEGER NOT NULL, + opposite_task_id INTEGER NOT NULL, + FOREIGN KEY(link_id) REFERENCES links(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(opposite_task_id) REFERENCES tasks(id) ON DELETE CASCADE + )"); + + $pdo->exec("CREATE INDEX task_has_links_task_index ON task_has_links(task_id)"); + $pdo->exec("CREATE UNIQUE INDEX task_has_links_unique ON task_has_links(link_id, task_id, opposite_task_id)"); + + $rq = $pdo->prepare('INSERT INTO links (label, opposite_id) VALUES (?, ?)'); + $rq->execute(array('relates to', 0)); + $rq->execute(array('blocks', 3)); + $rq->execute(array('is blocked by', 2)); + $rq->execute(array('duplicates', 5)); + $rq->execute(array('is duplicated by', 4)); + $rq->execute(array('is a child of', 7)); + $rq->execute(array('is a parent of', 6)); + $rq->execute(array('targets milestone', 9)); + $rq->execute(array('is a milestone of', 8)); + $rq->execute(array('fixes', 11)); + $rq->execute(array('is fixed by', 10)); +} + +function version_26($pdo) +{ + $pdo->exec('ALTER TABLE tasks ADD COLUMN date_moved INT DEFAULT 0'); + + /* Update tasks.date_moved from project_activities table if tasks.date_moved = null or 0. + * We take max project_activities.date_creation where event_name in task.create','task.move.column + * since creation date is always less than task moves + */ + $pdo->exec("UPDATE tasks + SET date_moved = ( + SELECT md + FROM ( + SELECT task_id, max(date_creation) md + FROM project_activities + WHERE event_name IN ('task.create', 'task.move.column') + GROUP BY task_id + ) src + WHERE id = src.task_id + ) + WHERE (date_moved IS NULL OR date_moved = 0) AND id IN ( + SELECT task_id + FROM ( + SELECT task_id, max(date_creation) md + FROM project_activities + WHERE event_name IN ('task.create', 'task.move.column') + GROUP BY task_id + ) src + )"); + + // If there is no activities for some tasks use the date_creation + $pdo->exec("UPDATE tasks SET date_moved = date_creation WHERE date_moved IS NULL OR date_moved = 0"); +} + +function version_25($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN disable_login_form BOOLEAN DEFAULT '0'"); +} + +function version_24($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('subtask_restriction', '0')); + $rq->execute(array('subtask_time_tracking', '0')); + + $pdo->exec(' + CREATE TABLE subtask_time_tracking ( + id SERIAL PRIMARY KEY, + "user_id" INTEGER NOT NULL, + "subtask_id" INTEGER NOT NULL, + "start" INTEGER DEFAULT 0, + "end" INTEGER DEFAULT 0, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(subtask_id) REFERENCES task_has_subtasks(id) ON DELETE CASCADE + ) + '); +} + +function version_23($pdo) +{ + $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT'); +} function version_22($pdo) { @@ -30,7 +129,7 @@ function version_21($pdo) $rq->execute(); $project_ids = $rq->fetchAll(PDO::FETCH_COLUMN, 0); - $rq = $pdo->prepare('UPDATE project_has_users SET is_owner=1 WHERE project_id=?'); + $rq = $pdo->prepare("UPDATE project_has_users SET is_owner='1' WHERE project_id=?"); foreach ($project_ids as $project_id) { $rq->execute(array($project_id)); @@ -58,7 +157,7 @@ function version_19($pdo) "); $pdo->exec('ALTER TABLE tasks ADD COLUMN swimlane_id INTEGER DEFAULT 0'); - $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT '".t('Default swimlane')."'"); + $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT 'Default swimlane'"); $pdo->exec("ALTER TABLE projects ADD COLUMN show_default_swimlane BOOLEAN DEFAULT '1'"); } diff --git a/sources/app/Schema/Sqlite.php b/sources/app/Schema/Sqlite.php index c615606..c6dec33 100644 --- a/sources/app/Schema/Sqlite.php +++ b/sources/app/Schema/Sqlite.php @@ -4,8 +4,107 @@ namespace Schema; use Core\Security; use PDO; +use Model\Link; -const VERSION = 40; +const VERSION = 45; + +function version_45($pdo) +{ + $pdo->exec("CREATE TABLE links ( + id INTEGER PRIMARY KEY, + label TEXT NOT NULL, + opposite_id INTEGER DEFAULT 0, + UNIQUE(label) + )"); + + $pdo->exec("CREATE TABLE task_has_links ( + id INTEGER PRIMARY KEY, + link_id INTEGER NOT NULL, + task_id INTEGER NOT NULL, + opposite_task_id INTEGER NOT NULL, + FOREIGN KEY(link_id) REFERENCES links(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(opposite_task_id) REFERENCES tasks(id) ON DELETE CASCADE + )"); + + $pdo->exec("CREATE INDEX task_has_links_task_index ON task_has_links(task_id)"); + $pdo->exec("CREATE UNIQUE INDEX task_has_links_unique ON task_has_links(link_id, task_id, opposite_task_id)"); + + $rq = $pdo->prepare('INSERT INTO links (label, opposite_id) VALUES (?, ?)'); + $rq->execute(array('relates to', 0)); + $rq->execute(array('blocks', 3)); + $rq->execute(array('is blocked by', 2)); + $rq->execute(array('duplicates', 5)); + $rq->execute(array('is duplicated by', 4)); + $rq->execute(array('is a child of', 7)); + $rq->execute(array('is a parent of', 6)); + $rq->execute(array('targets milestone', 9)); + $rq->execute(array('is a milestone of', 8)); + $rq->execute(array('fixes', 11)); + $rq->execute(array('is fixed by', 10)); +} + +function version_44($pdo) +{ + $pdo->exec('ALTER TABLE tasks ADD COLUMN date_moved INTEGER DEFAULT 0'); + + /* Update tasks.date_moved from project_activities table if tasks.date_moved = null or 0. + * We take max project_activities.date_creation where event_name in task.create','task.move.column + * since creation date is always less than task moves + */ + $pdo->exec("UPDATE tasks + SET date_moved = ( + SELECT md + FROM ( + SELECT task_id, max(date_creation) md + FROM project_activities + WHERE event_name IN ('task.create', 'task.move.column') + GROUP BY task_id + ) src + WHERE id = src.task_id + ) + WHERE (date_moved IS NULL OR date_moved = 0) AND id IN ( + SELECT task_id + FROM ( + SELECT task_id, max(date_creation) md + FROM project_activities + WHERE event_name IN ('task.create', 'task.move.column') + GROUP BY task_id + ) src + )"); + + // If there is no activities for some tasks use the date_creation + $pdo->exec("UPDATE tasks SET date_moved = date_creation WHERE date_moved IS NULL OR date_moved = 0"); +} + +function version_43($pdo) +{ + $pdo->exec('ALTER TABLE users ADD COLUMN disable_login_form INTEGER DEFAULT 0'); +} + +function version_42($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('subtask_restriction', '0')); + $rq->execute(array('subtask_time_tracking', '0')); + + $pdo->exec(" + CREATE TABLE subtask_time_tracking ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + subtask_id INTEGER NOT NULL, + start INTEGER DEFAULT 0, + end INTEGER DEFAULT 0, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(subtask_id) REFERENCES task_has_subtasks(id) ON DELETE CASCADE + ) + "); +} + +function version_41($pdo) +{ + $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT'); +} function version_40($pdo) { @@ -58,7 +157,7 @@ function version_37($pdo) "); $pdo->exec('ALTER TABLE tasks ADD COLUMN swimlane_id INTEGER DEFAULT 0'); - $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane TEXT DEFAULT '".t('Default swimlane')."'"); + $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane TEXT DEFAULT 'Default swimlane'"); $pdo->exec("ALTER TABLE projects ADD COLUMN show_default_swimlane INTEGER DEFAULT 1"); } diff --git a/sources/app/ServiceProvider/ClassProvider.php b/sources/app/ServiceProvider/ClassProvider.php index 3177276..213972e 100644 --- a/sources/app/ServiceProvider/ClassProvider.php +++ b/sources/app/ServiceProvider/ClassProvider.php @@ -2,6 +2,7 @@ namespace ServiceProvider; +use Core\Paginator; use Model\Config; use Model\Project; use Model\Webhook; @@ -23,24 +24,26 @@ class ClassProvider implements ServiceProviderInterface 'DateParser', 'File', 'LastLogin', + 'Link', 'Notification', 'Project', 'ProjectActivity', 'ProjectAnalytic', + 'ProjectDuplication', 'ProjectDailySummary', - 'ProjectPaginator', 'ProjectPermission', - 'SubTask', - 'SubtaskPaginator', + 'Subtask', 'SubtaskExport', + 'SubtaskTimeTracking', 'Swimlane', 'Task', 'TaskCreation', 'TaskDuplication', 'TaskExport', 'TaskFinder', + 'TaskFilter', + 'TaskLink', 'TaskModification', - 'TaskPaginator', 'TaskPermission', 'TaskPosition', 'TaskStatus', @@ -51,14 +54,17 @@ class ClassProvider implements ServiceProviderInterface 'Webhook', ), 'Core' => array( + 'Helper', 'Template', 'Session', 'MemoryCache', 'FileCache', + 'Request', ), 'Integration' => array( 'GitlabWebhook', 'GithubWebhook', + 'BitbucketWebhook', ) ); @@ -75,5 +81,9 @@ class ClassProvider implements ServiceProviderInterface }; } } + + $container['paginator'] = $container->factory(function ($c) { + return new Paginator($c); + }); } } diff --git a/sources/app/ServiceProvider/EventDispatcherProvider.php b/sources/app/ServiceProvider/EventDispatcherProvider.php index fd0f7a8..ec38220 100644 --- a/sources/app/ServiceProvider/EventDispatcherProvider.php +++ b/sources/app/ServiceProvider/EventDispatcherProvider.php @@ -12,6 +12,8 @@ use Subscriber\ProjectActivitySubscriber; use Subscriber\ProjectDailySummarySubscriber; use Subscriber\ProjectModificationDateSubscriber; use Subscriber\WebhookSubscriber; +use Subscriber\SubtaskTimesheetSubscriber; +use Subscriber\TaskMovedDateSubscriber; class EventDispatcherProvider implements ServiceProviderInterface { @@ -25,6 +27,8 @@ class EventDispatcherProvider implements ServiceProviderInterface $container['dispatcher']->addSubscriber(new ProjectModificationDateSubscriber($container)); $container['dispatcher']->addSubscriber(new WebhookSubscriber($container)); $container['dispatcher']->addSubscriber(new NotificationSubscriber($container)); + $container['dispatcher']->addSubscriber(new SubtaskTimesheetSubscriber($container)); + $container['dispatcher']->addSubscriber(new TaskMovedDateSubscriber($container)); // Automatic actions $container['action']->attachEvents(); diff --git a/sources/app/ServiceProvider/LoggingProvider.php b/sources/app/ServiceProvider/LoggingProvider.php index 5b2cf56..76263da 100644 --- a/sources/app/ServiceProvider/LoggingProvider.php +++ b/sources/app/ServiceProvider/LoggingProvider.php @@ -16,7 +16,7 @@ class LoggingProvider implements ServiceProviderInterface $logger->setLogger(new Syslog('kanboard')); if (DEBUG) { - $logger->setLogger(new File(__DIR__.'/../../data/debug.log')); + $logger->setLogger(new File(DEBUG_FILE)); } $container['logger'] = $logger; diff --git a/sources/app/Subscriber/Base.php b/sources/app/Subscriber/Base.php index abc051b..f90d960 100644 --- a/sources/app/Subscriber/Base.php +++ b/sources/app/Subscriber/Base.php @@ -10,16 +10,23 @@ use Pimple\Container; * @package subscriber * @author Frederic Guillot * + * @property \Model\Board $board * @property \Model\Config $config + * @property \Model\Comment $comment + * @property \Model\LastLogin $lastLogin * @property \Model\Notification $notification * @property \Model\Project $project * @property \Model\ProjectPermission $projectPermission + * @property \Model\ProjectActivity $projectActivity * @property \Model\ProjectAnalytic $projectAnalytic * @property \Model\ProjectDailySummary $projectDailySummary + * @property \Model\Subtask $subtask * @property \Model\Task $task * @property \Model\TaskExport $taskExport * @property \Model\TaskFinder $taskFinder + * @property \Model\SubtaskTimeTracking $subtaskTimeTracking * @property \Model\UserSession $userSession + * @property \Model\Webhook $webhook */ abstract class Base { diff --git a/sources/app/Subscriber/NotificationSubscriber.php b/sources/app/Subscriber/NotificationSubscriber.php index 1580f6d..1b7187f 100644 --- a/sources/app/Subscriber/NotificationSubscriber.php +++ b/sources/app/Subscriber/NotificationSubscriber.php @@ -5,7 +5,7 @@ namespace Subscriber; use Event\GenericEvent; use Model\Task; use Model\Comment; -use Model\SubTask; +use Model\Subtask; use Model\File; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -19,8 +19,8 @@ class NotificationSubscriber extends Base implements EventSubscriberInterface Task::EVENT_MOVE_COLUMN => 'task_move_column', Task::EVENT_MOVE_POSITION => 'task_move_position', Task::EVENT_ASSIGNEE_CHANGE => 'task_assignee_change', - SubTask::EVENT_CREATE => 'subtask_creation', - SubTask::EVENT_UPDATE => 'subtask_update', + Subtask::EVENT_CREATE => 'subtask_creation', + Subtask::EVENT_UPDATE => 'subtask_update', Comment::EVENT_CREATE => 'comment_creation', Comment::EVENT_UPDATE => 'comment_update', File::EVENT_CREATE => 'file_creation', @@ -36,8 +36,8 @@ class NotificationSubscriber extends Base implements EventSubscriberInterface Task::EVENT_MOVE_COLUMN => array('execute', 0), Task::EVENT_MOVE_POSITION => array('execute', 0), Task::EVENT_ASSIGNEE_CHANGE => array('execute', 0), - SubTask::EVENT_CREATE => array('execute', 0), - SubTask::EVENT_UPDATE => array('execute', 0), + Subtask::EVENT_CREATE => array('execute', 0), + Subtask::EVENT_UPDATE => array('execute', 0), Comment::EVENT_CREATE => array('execute', 0), Comment::EVENT_UPDATE => array('execute', 0), File::EVENT_CREATE => array('execute', 0), @@ -63,7 +63,7 @@ class NotificationSubscriber extends Base implements EventSubscriberInterface $values['task'] = $this->taskFinder->getDetails($event['task_id']); break; case 'Event\SubtaskEvent': - $values['subtask'] = $this->subTask->getById($event['id'], true); + $values['subtask'] = $this->subtask->getById($event['id'], true); $values['task'] = $this->taskFinder->getDetails($event['task_id']); break; case 'Event\FileEvent': diff --git a/sources/app/Subscriber/ProjectActivitySubscriber.php b/sources/app/Subscriber/ProjectActivitySubscriber.php index aae09ae..00f5b04 100644 --- a/sources/app/Subscriber/ProjectActivitySubscriber.php +++ b/sources/app/Subscriber/ProjectActivitySubscriber.php @@ -5,7 +5,7 @@ namespace Subscriber; use Event\GenericEvent; use Model\Task; use Model\Comment; -use Model\SubTask; +use Model\Subtask; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class ProjectActivitySubscriber extends Base implements EventSubscriberInterface @@ -22,8 +22,8 @@ class ProjectActivitySubscriber extends Base implements EventSubscriberInterface Task::EVENT_MOVE_POSITION => array('execute', 0), Comment::EVENT_UPDATE => array('execute', 0), Comment::EVENT_CREATE => array('execute', 0), - SubTask::EVENT_UPDATE => array('execute', 0), - SubTask::EVENT_CREATE => array('execute', 0), + Subtask::EVENT_UPDATE => array('execute', 0), + Subtask::EVENT_CREATE => array('execute', 0), ); } @@ -51,7 +51,7 @@ class ProjectActivitySubscriber extends Base implements EventSubscriberInterface switch (get_class($event)) { case 'Event\SubtaskEvent': - $values['subtask'] = $this->subTask->getById($event['id'], true); + $values['subtask'] = $this->subtask->getById($event['id'], true); break; case 'Event\CommentEvent': $values['comment'] = $this->comment->getById($event['id']); diff --git a/sources/app/Subscriber/SubtaskTimesheetSubscriber.php b/sources/app/Subscriber/SubtaskTimesheetSubscriber.php new file mode 100644 index 0000000..acae9a4 --- /dev/null +++ b/sources/app/Subscriber/SubtaskTimesheetSubscriber.php @@ -0,0 +1,47 @@ + array('updateTaskTime', 0), + Subtask::EVENT_UPDATE => array( + array('logStartEnd', 10), + array('updateTaskTime', 0), + ) + ); + } + + public function updateTaskTime(SubtaskEvent $event) + { + if (isset($event['task_id'])) { + $this->subtaskTimeTracking->updateTaskTimeTracking($event['task_id']); + } + } + + public function logStartEnd(SubtaskEvent $event) + { + if ($this->config->get('subtask_time_tracking') == 1 && isset($event['status'])) { + + $subtask = $this->subtask->getById($event['id']); + + if (empty($subtask['user_id'])) { + return false; + } + + if ($subtask['status'] == Subtask::STATUS_INPROGRESS) { + return $this->subtaskTimeTracking->logStartTime($subtask['id'], $subtask['user_id']); + } + else { + return $this->subtaskTimeTracking->logEndTime($subtask['id'], $subtask['user_id']); + } + } + } +} diff --git a/sources/app/Subscriber/TaskMovedDateSubscriber.php b/sources/app/Subscriber/TaskMovedDateSubscriber.php new file mode 100644 index 0000000..0e36acf --- /dev/null +++ b/sources/app/Subscriber/TaskMovedDateSubscriber.php @@ -0,0 +1,24 @@ + array('execute', 0), + ); + } + + public function execute(TaskEvent $event) + { + if (isset($event['task_id'])) { + $this->container['db']->table(Task::TABLE)->eq('id', $event['task_id'])->update(array('date_moved' => time())); + } + } +} diff --git a/sources/app/Template/analytic/layout.php b/sources/app/Template/analytic/layout.php index f250691..8c94669 100644 --- a/sources/app/Template/analytic/layout.php +++ b/sources/app/Template/analytic/layout.php @@ -1,5 +1,5 @@ -js('assets/js/d3.v3.4.8.min.js') ?> -js('assets/js/dimple.v2.1.0.min.js') ?> +js('assets/js/vendor/d3.v3.4.8.min.js') ?> +js('assets/js/vendor/dimple.v2.1.0.min.js') ?>
diff --git a/sources/app/Template/board/category.php b/sources/app/Template/board/category.php index 2484214..ee01ec5 100644 --- a/sources/app/Template/board/category.php +++ b/sources/app/Template/board/category.php @@ -14,7 +14,7 @@
- a(t('cancel'), 'board', 'show', array('project_id' => $project['id'])) ?> + a(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
diff --git a/sources/app/Template/board/edit.php b/sources/app/Template/board/edit.php index 65a8fb2..b9b1788 100644 --- a/sources/app/Template/board/edit.php +++ b/sources/app/Template/board/edit.php @@ -1,50 +1,48 @@ -

-
- formCsrf() ?> - - - - - - - - - - - - - - - - -
formLabel('#'.++$i, 'title['.$column['id'].']', array('title="column_id='.$column['id'].'"')) ?>formText('title['.$column['id'].']', $values, $errors, array('required')) ?>formNumber('task_limit['.$column['id'].']', $values, $errors, array('placeholder="'.t('limit').'"')) ?> -
    - -
  • - a(t('Move Up'), 'board', 'moveColumn', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'up'), true) ?> -
  • - - -
  • - a(t('Move Down'), 'board', 'moveColumn', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'down'), true) ?> -
  • - -
  • - a(t('Remove'), 'board', 'remove', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?> -
  • -
-
+ + + + + + + + + + + + + +
e($column['title']) ?> + + + + + + e($column['task_limit']) ?> +
    +
  • + a(t('Edit'), 'board', 'editColumn', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?> +
  • + +
  • + a(t('Move Up'), 'board', 'moveColumn', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'up'), true) ?> +
  • + + +
  • + a(t('Move Down'), 'board', 'moveColumn', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'down'), true) ?> +
  • + +
  • + a(t('Remove'), 'board', 'remove', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?> +
  • +
+
-
- -
-
-

@@ -53,7 +51,30 @@ formHidden('project_id', $values) ?> formLabel(t('Title'), 'title') ?> - formText('title', $values, $errors, array('required')) ?> + formText('title', $values, $errors, array('required', 'maxlength="50"')) ?> + + formLabel(t('Task limit'), 'task_limit') ?> + formNumber('task_limit', $values, $errors) ?> + + formLabel(t('Description'), 'description') ?> + +
+
+ formTextarea('description', $values, $errors) ?> +
+
+
+
+
    +
  • + +
  • +
  • + +
  • +
+
+
diff --git a/sources/app/Template/board/edit_column.php b/sources/app/Template/board/edit_column.php new file mode 100644 index 0000000..ef76b18 --- /dev/null +++ b/sources/app/Template/board/edit_column.php @@ -0,0 +1,42 @@ + + + + + formCsrf() ?> + + formHidden('id', $values) ?> + formHidden('project_id', $values) ?> + + formLabel(t('Title'), 'title') ?> + formText('title', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> + + formLabel(t('Task limit'), 'task_limit') ?> + formNumber('task_limit', $values, $errors) ?> + + formLabel(t('Description'), 'description') ?> + +
+ +
+ formTextarea('description', $values, $errors) ?> +
+
+
+
+
    +
  • + +
  • +
  • + +
  • +
+
+
+ +
+ +
+ \ No newline at end of file diff --git a/sources/app/Template/board/files.php b/sources/app/Template/board/files.php index fee0b63..278b906 100644 --- a/sources/app/Template/board/files.php +++ b/sources/app/Template/board/files.php @@ -6,7 +6,7 @@ $this->e($file['name']), 'file', 'download', - array('file_id' => $file['id'], 'task_id' => $file['task_id']) + array('file_id' => $file['id'], 'task_id' => $file['task_id'], 'project_id' => $task['project_id']) ) ?>
diff --git a/sources/app/Template/board/filters.php b/sources/app/Template/board/filters.php index 3625982..a0de5fd 100644 --- a/sources/app/Template/board/filters.php +++ b/sources/app/Template/board/filters.php @@ -1,35 +1,65 @@ \ No newline at end of file diff --git a/sources/app/Template/board/show.php b/sources/app/Template/board/show.php index 5fbb576..f1607d2 100644 --- a/sources/app/Template/board/show.php +++ b/sources/app/Template/board/show.php @@ -1,28 +1,30 @@ - - - - -
- - - - -

- +
+ +
- render('board/swimlane', array( - 'project' => $project, - 'swimlane' => $swimlane, - 'board_highlight_period' => $board_highlight_period, - 'categories' => $categories, - 'hide_swimlane' => count($swimlanes) === 1, - 'not_editable' => isset($not_editable), - )) ?> +
- -
+ + + +

+ + + render('board/swimlane', array( + 'project' => $project, + 'swimlane' => $swimlane, + 'board_highlight_period' => $board_highlight_period, + 'categories' => $categories, + 'hide_swimlane' => count($swimlanes) === 1, + 'not_editable' => isset($not_editable), + )) ?> + + + +
\ No newline at end of file diff --git a/sources/app/Template/board/subtasks.php b/sources/app/Template/board/subtasks.php index 1cb0549..18f7f9d 100644 --- a/sources/app/Template/board/subtasks.php +++ b/sources/app/Template/board/subtasks.php @@ -1,14 +1,7 @@
- a( - trim($this->render('subtask/icons', array('subtask' => $subtask))) . $this->e($subtask['title']), - 'board', - 'toggleSubtask', - array('task_id' => $subtask['task_id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']) - ) ?> - + toggleSubtaskStatus($subtask, 'board') ?> e(empty($subtask['username']) ? '' : ' ['.$this->getFullname($subtask).']') ?> -
diff --git a/sources/app/Template/board/swimlane.php b/sources/app/Template/board/swimlane.php index 5766141..ec298e2 100644 --- a/sources/app/Template/board/swimlane.php +++ b/sources/app/Template/board/swimlane.php @@ -1,18 +1,38 @@ - + + + 0): ?> + + + + + + + e($swimlane['name']) ?> + + + () + + - +
- a('+', 'task', 'create', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'task-creation-popover', t('Add a new task')) ?> + a('+', 'task', 'create', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'task-board-popover', t('Add a new task')) ?>
e($column['title']) ?> + + + + + + (/e($column['task_limit']) ?>) @@ -25,11 +45,10 @@ - + + - - e($swimlane['name']) ?> - + @@ -46,7 +65,7 @@ - render('board/task', array( + render($not_editable ? 'board/task_public' : 'board/task_private', array( 'project' => $project, 'task' => $task, 'categories' => $categories, diff --git a/sources/app/Template/board/task.php b/sources/app/Template/board/task.php deleted file mode 100644 index 6700b69..0000000 --- a/sources/app/Template/board/task.php +++ /dev/null @@ -1,125 +0,0 @@ - - -
- - a('#'.$task['id'], 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?> - - - - () - - - -  -  - - - - - - - - - - - e($task['score']) ?> - - -
- a($this->e($task['title']), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?> -
- - - -
- - a('#'.$task['id'], 'task', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-edit-popover', t('Edit this task')) ?> - - - - () - - - - - a( - (! empty($task['owner_id']) ? t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) : t('Nobody assigned')), - 'board', - 'changeAssignee', - array('task_id' => $task['id'], 'project_id' => $task['project_id']), - false, - 'assignee-popover', - t('Change assignee') - ) ?> - - - - e($task['score']) ?> - - -
- a($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> -
- - - - - -
- - a( - $this->inList($task['category_id'], $categories), - 'board', - 'changeCategory', - array('task_id' => $task['id'], 'project_id' => $task['project_id']), - false, - 'category-popover', - t('Change category') - ) ?> - -
- - - - - - - -
\ No newline at end of file diff --git a/sources/app/Template/board/task_footer.php b/sources/app/Template/board/task_footer.php new file mode 100644 index 0000000..d413692 --- /dev/null +++ b/sources/app/Template/board/task_footer.php @@ -0,0 +1,45 @@ + +
+ + a( + $this->inList($task['category_id'], $categories), + 'board', + 'changeCategory', + array('task_id' => $task['id'], 'project_id' => $task['project_id']), + false, + 'task-board-popover', + t('Change category') + ) ?> + +
+ + +
+ + +   + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/sources/app/Template/board/task_menu.php b/sources/app/Template/board/task_menu.php new file mode 100644 index 0000000..e7e0f41 --- /dev/null +++ b/sources/app/Template/board/task_menu.php @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/sources/app/Template/board/task_private.php b/sources/app/Template/board/task_private.php new file mode 100644 index 0000000..71d94fb --- /dev/null +++ b/sources/app/Template/board/task_private.php @@ -0,0 +1,49 @@ +
+ + render('board/task_menu', array('task' => $task)) ?> + + + +
+ + + + () + + + + + a( + (! empty($task['owner_id']) ? ($task['assignee_name'] ?: $task['assignee_username']) : t('Nobody assigned')), + 'board', + 'changeAssignee', + array('task_id' => $task['id'], 'project_id' => $task['project_id']), + false, + 'task-board-popover', + t('Change assignee') + ) ?> + + + + e($task['score']) ?> + + +
+ getTaskAge($task['date_creation']) ?> + getTaskAge($task['date_moved']) ?> +
+ +
+ a($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> +
+ + render('board/task_footer', array('task' => $task, 'categories' => $categories)) ?> +
+
diff --git a/sources/app/Template/board/task_public.php b/sources/app/Template/board/task_public.php new file mode 100644 index 0000000..650b956 --- /dev/null +++ b/sources/app/Template/board/task_public.php @@ -0,0 +1,30 @@ +
+ + a('#'.$task['id'], 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?> + + + + () + + + +  -  + + + + + + + + + + + e($task['score']) ?> + + +
+ a($this->e($task['title']), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?> +
+ + render('board/task_footer', array('task' => $task, 'categories' => $categories)) ?> +
\ No newline at end of file diff --git a/sources/app/Template/board/tasklinks.php b/sources/app/Template/board/tasklinks.php new file mode 100644 index 0000000..9c4f52c --- /dev/null +++ b/sources/app/Template/board/tasklinks.php @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/sources/app/Template/calendar/show.php b/sources/app/Template/calendar/show.php new file mode 100644 index 0000000..9e96d29 --- /dev/null +++ b/sources/app/Template/calendar/show.php @@ -0,0 +1,44 @@ +
+ + +
\ No newline at end of file diff --git a/sources/app/Template/calendar/sidebar.php b/sources/app/Template/calendar/sidebar.php new file mode 100644 index 0000000..b074ed9 --- /dev/null +++ b/sources/app/Template/calendar/sidebar.php @@ -0,0 +1,40 @@ + diff --git a/sources/app/Template/category/edit.php b/sources/app/Template/category/edit.php index bec175d..46d4778 100644 --- a/sources/app/Template/category/edit.php +++ b/sources/app/Template/category/edit.php @@ -10,7 +10,7 @@ formHidden('project_id', $values) ?> formLabel(t('Category Name'), 'name') ?> - formText('name', $values, $errors, array('autofocus required')) ?> + formText('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
diff --git a/sources/app/Template/category/index.php b/sources/app/Template/category/index.php index f876275..d5b50af 100644 --- a/sources/app/Template/category/index.php +++ b/sources/app/Template/category/index.php @@ -34,7 +34,7 @@ formHidden('project_id', $values) ?> formLabel(t('Category Name'), 'name') ?> - formText('name', $values, $errors, array('autofocus required')) ?> + formText('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
diff --git a/sources/app/Template/comment/create.php b/sources/app/Template/comment/create.php index 1bdbac4..8d2b05d 100644 --- a/sources/app/Template/comment/create.php +++ b/sources/app/Template/comment/create.php @@ -2,7 +2,7 @@

-
+ formCsrf() ?> formHidden('task_id', $values) ?> formHidden('user_id', $values) ?> @@ -30,7 +30,11 @@ - a(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + + a(t('cancel'), 'board', 'show', array('project_id' => $task['project_id'])) ?> + + a(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> +
diff --git a/sources/app/Template/config/about.php b/sources/app/Template/config/about.php index d96e6bd..f6474e2 100644 --- a/sources/app/Template/config/about.php +++ b/sources/app/Template/config/about.php @@ -1,7 +1,7 @@ -
+
  • @@ -12,12 +12,11 @@
-
- +
-
+
  • @@ -38,4 +37,20 @@
-
\ No newline at end of file + + +
+

+
    +
  • = n
  • +
  • = s
  • +
+

+
    +
  • = b
  • +
  • = ESC
  • +
  • = CTRL+ENTER ⌘+ENTER
  • +
+
\ No newline at end of file diff --git a/sources/app/Template/config/board.php b/sources/app/Template/config/board.php index d7f8ee4..57efcd0 100644 --- a/sources/app/Template/config/board.php +++ b/sources/app/Template/config/board.php @@ -26,6 +26,9 @@ formText('project_categories', $values, $errors) ?>

+ formCheckbox('subtask_restriction', t('Allow only one subtask in progress at the same time for a user'), 1, $values['subtask_restriction'] == 1) ?> + formCheckbox('subtask_time_tracking', t('Enable time tracking for subtasks'), 1, $values['subtask_time_tracking'] == 1) ?> +
diff --git a/sources/app/Template/config/sidebar.php b/sources/app/Template/config/sidebar.php index 8e6fa37..89f2c20 100644 --- a/sources/app/Template/config/sidebar.php +++ b/sources/app/Template/config/sidebar.php @@ -10,6 +10,9 @@
  • a(t('Board settings'), 'config', 'board') ?>
  • +
  • + a(t('Link settings'), 'link', 'index') ?> +
  • a(t('Webhooks'), 'config', 'webhook') ?>
  • diff --git a/sources/app/Template/file/show.php b/sources/app/Template/file/show.php index 179f574..298976f 100644 --- a/sources/app/Template/file/show.php +++ b/sources/app/Template/file/show.php @@ -11,7 +11,7 @@ a($this->e($file['name']), 'file', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> - a(t('open'), 'file', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'file-popover') ?>, + a(t('open'), 'file', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?>, a(t('remove'), 'file', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> diff --git a/sources/app/Template/layout.php b/sources/app/Template/layout.php index 9aa3490..ad4c408 100644 --- a/sources/app/Template/layout.php +++ b/sources/app/Template/layout.php @@ -14,6 +14,7 @@ js('assets/js/app.js') ?> + css($this->u('app', 'colors'), false) ?> css('assets/css/app.css') ?> @@ -24,17 +25,21 @@ <?= isset($title) ? $this->e($title) : 'Kanboard' ?> - + +