diff --git a/sources/app/Action/Base.php b/sources/app/Action/Base.php index a2b07e3..0d8bd56 100644 --- a/sources/app/Action/Base.php +++ b/sources/app/Action/Base.php @@ -2,9 +2,8 @@ namespace Action; +use Event\GenericEvent; use Pimple\Container; -use Core\Listener; -use Core\Tool; /** * Base class for automatic actions @@ -12,7 +11,7 @@ use Core\Tool; * @package action * @author Frederic Guillot * - * @property \Model\Acl $acl + * @property \Model\UserSession $userSession * @property \Model\Comment $comment * @property \Model\Task $task * @property \Model\TaskCreation $taskCreation @@ -21,8 +20,16 @@ use Core\Tool; * @property \Model\TaskFinder $taskFinder * @property \Model\TaskStatus $taskStatus */ -abstract class Base implements Listener +abstract class Base { + /** + * Flag for called listener + * + * @access private + * @var boolean + */ + private $called = false; + /** * Project id * @@ -114,6 +121,7 @@ abstract class Base implements Listener $this->container = $container; $this->project_id = $project_id; $this->event_name = $event_name; + $this->called = false; } /** @@ -136,7 +144,7 @@ abstract class Base implements Listener */ public function __get($name) { - return Tool::loadModel($this->container, $name); + return $this->container[$name]; } /** @@ -183,7 +191,6 @@ abstract class Base implements Listener * Check if the event is compatible with the action * * @access public - * @param array $data Event data dictionary * @return bool */ public function hasCompatibleEvent() @@ -225,12 +232,20 @@ abstract class Base implements Listener * Execute the action * * @access public - * @param array $data Event data dictionary - * @return bool True if the action was executed or false when not executed + * @param \Event\GenericEvent $event Event data dictionary + * @return bool True if the action was executed or false when not executed */ - public function execute(array $data) + public function execute(GenericEvent $event) { + // Avoid infinite loop, a listener instance can be called only one time + if ($this->called) { + return false; + } + + $data = $event->getAll(); + if ($this->isExecutable($data)) { + $this->called = true; return $this->doAction($data); } diff --git a/sources/app/Action/CommentCreation.php b/sources/app/Action/CommentCreation.php index 5dbe32f..54d7be7 100644 --- a/sources/app/Action/CommentCreation.php +++ b/sources/app/Action/CommentCreation.php @@ -2,7 +2,7 @@ namespace Action; -use Model\GithubWebhook; +use Integration\GithubWebhook; /** * Create automatically a comment from a webhook @@ -16,7 +16,7 @@ class CommentCreation extends Base * Get the list of compatible events * * @access public - * @return array + * @return string[] */ public function getCompatibleEvents() { @@ -29,7 +29,7 @@ class CommentCreation extends Base * Get the required parameter for the action (defined by the user) * * @access public - * @return array + * @return string[] */ public function getActionRequiredParameters() { diff --git a/sources/app/Action/TaskAssignCategoryColor.php b/sources/app/Action/TaskAssignCategoryColor.php index 4134b58..ba319a1 100644 --- a/sources/app/Action/TaskAssignCategoryColor.php +++ b/sources/app/Action/TaskAssignCategoryColor.php @@ -67,7 +67,7 @@ class TaskAssignCategoryColor extends Base 'category_id' => $this->getParam('category_id'), ); - return $this->taskModification->update($values, false); + return $this->taskModification->update($values); } /** diff --git a/sources/app/Action/TaskAssignCategoryLabel.php b/sources/app/Action/TaskAssignCategoryLabel.php index da41a31..1383d49 100644 --- a/sources/app/Action/TaskAssignCategoryLabel.php +++ b/sources/app/Action/TaskAssignCategoryLabel.php @@ -2,7 +2,7 @@ namespace Action; -use Model\GithubWebhook; +use Integration\GithubWebhook; /** * Set a category automatically according to a label @@ -67,7 +67,7 @@ class TaskAssignCategoryLabel extends Base 'category_id' => isset($data['category_id']) ? $data['category_id'] : $this->getParam('category_id'), ); - return $this->taskModification->update($values, false); + return $this->taskModification->update($values); } /** diff --git a/sources/app/Action/TaskAssignColorCategory.php b/sources/app/Action/TaskAssignColorCategory.php index 68bca5d..a362c68 100644 --- a/sources/app/Action/TaskAssignColorCategory.php +++ b/sources/app/Action/TaskAssignColorCategory.php @@ -67,7 +67,7 @@ class TaskAssignColorCategory extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->taskModification->update($values, false); + return $this->taskModification->update($values); } /** diff --git a/sources/app/Action/TaskAssignColorUser.php b/sources/app/Action/TaskAssignColorUser.php index d419ab4..6161514 100644 --- a/sources/app/Action/TaskAssignColorUser.php +++ b/sources/app/Action/TaskAssignColorUser.php @@ -68,7 +68,7 @@ class TaskAssignColorUser extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->taskModification->update($values, false); + return $this->taskModification->update($values); } /** diff --git a/sources/app/Action/TaskAssignCurrentUser.php b/sources/app/Action/TaskAssignCurrentUser.php index 9317bf8..ff3aaee 100644 --- a/sources/app/Action/TaskAssignCurrentUser.php +++ b/sources/app/Action/TaskAssignCurrentUser.php @@ -62,12 +62,16 @@ class TaskAssignCurrentUser extends Base */ public function doAction(array $data) { + if (! $this->userSession->isLogged()) { + return false; + } + $values = array( 'id' => $data['task_id'], - 'owner_id' => $this->acl->getUserId(), + 'owner_id' => $this->userSession->getId(), ); - return $this->taskModification->update($values, false); + return $this->taskModification->update($values); } /** diff --git a/sources/app/Action/TaskAssignSpecificUser.php b/sources/app/Action/TaskAssignSpecificUser.php index c3b979c..4c96f7f 100644 --- a/sources/app/Action/TaskAssignSpecificUser.php +++ b/sources/app/Action/TaskAssignSpecificUser.php @@ -68,7 +68,7 @@ class TaskAssignSpecificUser extends Base 'owner_id' => $this->getParam('user_id'), ); - return $this->taskModification->update($values, false); + return $this->taskModification->update($values); } /** diff --git a/sources/app/Action/TaskAssignUser.php b/sources/app/Action/TaskAssignUser.php index d01c407..cf2a9a4 100644 --- a/sources/app/Action/TaskAssignUser.php +++ b/sources/app/Action/TaskAssignUser.php @@ -2,7 +2,7 @@ namespace Action; -use Model\GithubWebhook; +use Integration\GithubWebhook; /** * Assign a task to someone @@ -64,7 +64,7 @@ class TaskAssignUser extends Base 'owner_id' => $data['owner_id'], ); - return $this->taskModification->update($values, false); + return $this->taskModification->update($values); } /** diff --git a/sources/app/Action/TaskClose.php b/sources/app/Action/TaskClose.php index 6cf9be0..760dfd8 100644 --- a/sources/app/Action/TaskClose.php +++ b/sources/app/Action/TaskClose.php @@ -2,7 +2,8 @@ namespace Action; -use Model\GithubWebhook; +use Integration\GitlabWebhook; +use Integration\GithubWebhook; use Model\Task; /** @@ -25,6 +26,8 @@ class TaskClose extends Base Task::EVENT_MOVE_COLUMN, GithubWebhook::EVENT_COMMIT, GithubWebhook::EVENT_ISSUE_CLOSED, + GitlabWebhook::EVENT_COMMIT, + GitlabWebhook::EVENT_ISSUE_CLOSED, ); } @@ -39,6 +42,8 @@ class TaskClose extends Base switch ($this->event_name) { case GithubWebhook::EVENT_COMMIT: case GithubWebhook::EVENT_ISSUE_CLOSED: + case GitlabWebhook::EVENT_COMMIT: + case GitlabWebhook::EVENT_ISSUE_CLOSED: return array(); default: return array('column_id' => t('Column')); @@ -56,6 +61,8 @@ class TaskClose extends Base switch ($this->event_name) { case GithubWebhook::EVENT_COMMIT: case GithubWebhook::EVENT_ISSUE_CLOSED: + case GitlabWebhook::EVENT_COMMIT: + case GitlabWebhook::EVENT_ISSUE_CLOSED: return array('task_id'); default: return array('task_id', 'column_id'); @@ -86,6 +93,8 @@ class TaskClose extends Base switch ($this->event_name) { case GithubWebhook::EVENT_COMMIT: case GithubWebhook::EVENT_ISSUE_CLOSED: + case GitlabWebhook::EVENT_COMMIT: + case GitlabWebhook::EVENT_ISSUE_CLOSED: return true; default: return $data['column_id'] == $this->getParam('column_id'); diff --git a/sources/app/Action/TaskCreation.php b/sources/app/Action/TaskCreation.php index 0c79168..1c093ee 100644 --- a/sources/app/Action/TaskCreation.php +++ b/sources/app/Action/TaskCreation.php @@ -2,7 +2,8 @@ namespace Action; -use Model\GithubWebhook; +use Integration\GithubWebhook; +use Integration\GitlabWebhook; /** * Create automatically a task from a webhook @@ -22,6 +23,7 @@ class TaskCreation extends Base { return array( GithubWebhook::EVENT_ISSUE_OPENED, + GitlabWebhook::EVENT_ISSUE_OPENED, ); } @@ -63,7 +65,7 @@ class TaskCreation extends Base 'project_id' => $data['project_id'], 'title' => $data['title'], 'reference' => $data['reference'], - 'description' => $data['description'], + 'description' => isset($data['description']) ? $data['description'] : '', )); } diff --git a/sources/app/Action/TaskOpen.php b/sources/app/Action/TaskOpen.php index fc29e9e..73f1fad 100644 --- a/sources/app/Action/TaskOpen.php +++ b/sources/app/Action/TaskOpen.php @@ -2,7 +2,7 @@ namespace Action; -use Model\GithubWebhook; +use Integration\GithubWebhook; /** * Open automatically a task diff --git a/sources/app/Auth/Base.php b/sources/app/Auth/Base.php index 9633af4..e023e4f 100644 --- a/sources/app/Auth/Base.php +++ b/sources/app/Auth/Base.php @@ -2,7 +2,6 @@ namespace Auth; -use Core\Tool; use Pimple\Container; /** @@ -14,6 +13,7 @@ use Pimple\Container; * @property \Model\Acl $acl * @property \Model\LastLogin $lastLogin * @property \Model\User $user + * @property \Model\UserSession $userSession */ abstract class Base { @@ -54,6 +54,6 @@ abstract class Base */ public function __get($name) { - return Tool::loadModel($this->container, $name); + return $this->container[$name]; } } diff --git a/sources/app/Auth/Database.php b/sources/app/Auth/Database.php index 47dc8e6..2804b9a 100644 --- a/sources/app/Auth/Database.php +++ b/sources/app/Auth/Database.php @@ -3,7 +3,7 @@ namespace Auth; use Model\User; -use Core\Request; +use Event\AuthEvent; /** * Database authentication @@ -33,18 +33,8 @@ class Database extends Base $user = $this->db->table(User::TABLE)->eq('username', $username)->eq('is_ldap_user', 0)->findOne(); if ($user && password_verify($password, $user['password'])) { - - // Update user session - $this->user->updateSession($user); - - // Update login history - $this->lastLogin->create( - self::AUTH_NAME, - $user['id'], - Request::getIpAddress(), - Request::getUserAgent() - ); - + $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/GitHub.php b/sources/app/Auth/GitHub.php index 034f926..0e335fb 100644 --- a/sources/app/Auth/GitHub.php +++ b/sources/app/Auth/GitHub.php @@ -2,7 +2,7 @@ namespace Auth; -use Core\Request; +use Event\AuthEvent; use OAuth\Common\Storage\Session; use OAuth\Common\Consumer\Credentials; use OAuth\Common\Http\Uri\UriFactory; @@ -35,18 +35,8 @@ class GitHub extends Base $user = $this->user->getByGitHubId($github_id); if ($user) { - - // Create the user session - $this->user->updateSession($user); - - // Update login history - $this->lastLogin->create( - self::AUTH_NAME, - $user['id'], - Request::getIpAddress(), - Request::getUserAgent() - ); - + $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/Google.php b/sources/app/Auth/Google.php index 587ecde..e7abae0 100644 --- a/sources/app/Auth/Google.php +++ b/sources/app/Auth/Google.php @@ -2,7 +2,7 @@ namespace Auth; -use Core\Request; +use Event\AuthEvent; use OAuth\Common\Storage\Session; use OAuth\Common\Consumer\Credentials; use OAuth\Common\Http\Uri\UriFactory; @@ -36,18 +36,8 @@ class Google extends Base $user = $this->user->getByGoogleId($google_id); if ($user) { - - // Create the user session - $this->user->updateSession($user); - - // Update login history - $this->lastLogin->create( - self::AUTH_NAME, - $user['id'], - Request::getIpAddress(), - Request::getUserAgent() - ); - + $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 82307e8..b344061 100644 --- a/sources/app/Auth/Ldap.php +++ b/sources/app/Auth/Ldap.php @@ -2,7 +2,7 @@ namespace Auth; -use Core\Request; +use Event\AuthEvent; /** * LDAP model @@ -54,15 +54,8 @@ class Ldap extends Base } // We open the session - $this->user->updateSession($user); - - // Update login history - $this->lastLogin->create( - self::AUTH_NAME, - $user['id'], - Request::getIpAddress(), - Request::getUserAgent() - ); + $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/RememberMe.php b/sources/app/Auth/RememberMe.php index cb8a9b4..4736442 100644 --- a/sources/app/Auth/RememberMe.php +++ b/sources/app/Auth/RememberMe.php @@ -3,6 +3,7 @@ namespace Auth; use Core\Request; +use Event\AuthEvent; use Core\Security; /** @@ -100,15 +101,11 @@ class RememberMe extends Base ); // Create the session - $this->user->updateSession($this->user->getById($record['user_id'])); - $this->acl->isRememberMe(true); + $this->userSession->refresh($this->user->getById($record['user_id'])); - // Update last login infos - $this->lastLogin->create( - self::AUTH_NAME, - $this->acl->getUserId(), - Request::getIpAddress(), - Request::getUserAgent() + $this->container['dispatcher']->dispatch( + 'auth.success', + new AuthEvent(self::AUTH_NAME, $this->userSession->getId()) ); return true; diff --git a/sources/app/Auth/ReverseProxy.php b/sources/app/Auth/ReverseProxy.php index 5aca881..b84550c 100644 --- a/sources/app/Auth/ReverseProxy.php +++ b/sources/app/Auth/ReverseProxy.php @@ -2,8 +2,7 @@ namespace Auth; -use Core\Request; -use Core\Security; +use Event\AuthEvent; /** * ReverseProxy backend @@ -38,16 +37,8 @@ class ReverseProxy extends Base $user = $this->user->getByUsername($login); } - // Create the user session - $this->user->updateSession($user); - - // Update login history - $this->lastLogin->create( - self::AUTH_NAME, - $user['id'], - Request::getIpAddress(), - Request::getUserAgent() - ); + $this->userSession->refresh($user); + $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id'])); return true; } diff --git a/sources/app/Console/Base.php b/sources/app/Console/Base.php index f955b42..aeafbef 100644 --- a/sources/app/Console/Base.php +++ b/sources/app/Console/Base.php @@ -2,7 +2,6 @@ namespace Console; -use Core\Tool; use Pimple\Container; use Symfony\Component\Console\Command\Command; @@ -17,6 +16,7 @@ use Symfony\Component\Console\Command\Command; * @property \Model\ProjectPermission $projectPermission * @property \Model\ProjectAnalytic $projectAnalytic * @property \Model\ProjectDailySummary $projectDailySummary + * @property \Model\SubtaskExport $subtaskExport * @property \Model\Task $task * @property \Model\TaskExport $taskExport * @property \Model\TaskFinder $taskFinder @@ -52,6 +52,6 @@ abstract class Base extends Command */ public function __get($name) { - return Tool::loadModel($this->container, $name); + return $this->container[$name]; } } diff --git a/sources/app/Console/ProjectDailySummaryCalculation.php b/sources/app/Console/ProjectDailySummaryCalculation.php index 04c4083..b2ada1b 100644 --- a/sources/app/Console/ProjectDailySummaryCalculation.php +++ b/sources/app/Console/ProjectDailySummaryCalculation.php @@ -3,9 +3,7 @@ namespace Console; use Model\Project; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class ProjectDailySummaryCalculation extends Base diff --git a/sources/app/Console/ProjectDailySummaryExport.php b/sources/app/Console/ProjectDailySummaryExport.php index 6b96fdd..07841d5 100644 --- a/sources/app/Console/ProjectDailySummaryExport.php +++ b/sources/app/Console/ProjectDailySummaryExport.php @@ -5,7 +5,6 @@ namespace Console; use Core\Tool; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class ProjectDailySummaryExport extends Base diff --git a/sources/app/Console/SubtaskExport.php b/sources/app/Console/SubtaskExport.php new file mode 100644 index 0000000..167a922 --- /dev/null +++ b/sources/app/Console/SubtaskExport.php @@ -0,0 +1,34 @@ +setName('export:subtasks') + ->setDescription('Subtasks CSV export') + ->addArgument('project_id', InputArgument::REQUIRED, 'Project id') + ->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)') + ->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $data = $this->subtaskExport->export( + $input->getArgument('project_id'), + $input->getArgument('start_date'), + $input->getArgument('end_date') + ); + + if (is_array($data)) { + Tool::csv($data); + } + } +} diff --git a/sources/app/Console/TaskExport.php b/sources/app/Console/TaskExport.php index dea71fe..2ecd45e 100644 --- a/sources/app/Console/TaskExport.php +++ b/sources/app/Console/TaskExport.php @@ -5,7 +5,6 @@ namespace Console; use Core\Tool; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class TaskExport extends Base diff --git a/sources/app/Console/TaskOverdueNotification.php b/sources/app/Console/TaskOverdueNotification.php index aa70fd0..86a7d1b 100644 --- a/sources/app/Console/TaskOverdueNotification.php +++ b/sources/app/Console/TaskOverdueNotification.php @@ -3,7 +3,6 @@ namespace Console; use Symfony\Component\Console\Helper\Table; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; diff --git a/sources/app/Controller/Action.php b/sources/app/Controller/Action.php index 22358cb..2b58dca 100644 --- a/sources/app/Controller/Action.php +++ b/sources/app/Controller/Action.php @@ -17,7 +17,7 @@ class Action extends Base */ public function index() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $this->response->html($this->projectLayout('action/index', array( 'values' => array('project_id' => $project['id']), @@ -42,7 +42,7 @@ class Action extends Base */ public function event() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues(); if (empty($values['action_name']) || empty($values['project_id'])) { @@ -64,7 +64,7 @@ class Action extends Base */ public function params() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues(); if (empty($values['action_name']) || empty($values['project_id']) || empty($values['event_name'])) { @@ -101,7 +101,7 @@ class Action extends Base */ public function create() { - $this->doCreation($this->getProjectManagement(), $this->request->getValues()); + $this->doCreation($this->getProject(), $this->request->getValues()); } /** @@ -135,7 +135,7 @@ class Action extends Base */ public function confirm() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $this->response->html($this->projectLayout('action/remove', array( 'action' => $this->action->getById($this->request->getIntegerParam('action_id')), @@ -154,7 +154,7 @@ class Action extends Base public function remove() { $this->checkCSRFParam(); - $project = $this->getProjectManagement(); + $project = $this->getProject(); $action = $this->action->getById($this->request->getIntegerParam('action_id')); if ($action && $this->action->remove($action['id'])) { diff --git a/sources/app/Controller/Analytic.php b/sources/app/Controller/Analytic.php index 6c49089..8b0684d 100644 --- a/sources/app/Controller/Analytic.php +++ b/sources/app/Controller/Analytic.php @@ -20,8 +20,8 @@ class Analytic extends Base */ private function layout($template, array $params) { - $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); - $params['analytic_content_for_layout'] = $this->template->load($template, $params); + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); + $params['analytic_content_for_layout'] = $this->template->render($template, $params); return $this->template->layout('analytic/layout', $params); } diff --git a/sources/app/Controller/App.php b/sources/app/Controller/App.php index c88fd92..aa2673a 100644 --- a/sources/app/Controller/App.php +++ b/sources/app/Controller/App.php @@ -2,9 +2,7 @@ namespace Controller; -use Model\Project as ProjectModel; use Model\SubTask as SubTaskModel; -use Helper; /** * Application controller @@ -36,7 +34,7 @@ class App extends Base $direction = $this->request->getStringParam('direction'); $order = $this->request->getStringParam('order'); - $user_id = $this->acl->getUserId(); + $user_id = $this->userSession->getId(); $projects = $this->projectPermission->getMemberProjects($user_id); $project_ids = array_keys($projects); @@ -57,6 +55,11 @@ class App extends Base * 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) { @@ -94,6 +97,11 @@ class App extends Base * 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) { @@ -132,10 +140,15 @@ class App extends Base * Get projects pagination * * @access public + * @param array $project_ids + * @param string $paginate + * @param integer $offset + * @param string $order + * @param string $direction */ - private function getProjectPagination($project_ids, $paginate, $offset, $order, $direction) + private function getProjectPagination(array $project_ids, $paginate, $offset, $order, $direction) { - $limit = 5; + $limit = 10; if (! in_array($order, array('id', 'name'))) { $order = 'name'; @@ -178,8 +191,9 @@ class App extends Base $this->response->html('

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

'); } else { - $this->response->html(Helper\markdown($payload['text'])); + $this->response->html( + $this->template->markdown($payload['text']) + ); } } - } diff --git a/sources/app/Controller/Base.php b/sources/app/Controller/Base.php index 5027cf3..8a5354a 100644 --- a/sources/app/Controller/Base.php +++ b/sources/app/Controller/Base.php @@ -3,13 +3,13 @@ namespace Controller; use Pimple\Container; -use Core\Tool; use Core\Security; use Core\Request; use Core\Response; use Core\Template; use Core\Session; use Model\LastLogin; +use Symfony\Component\EventDispatcher\Event; /** * Base controller @@ -17,6 +17,8 @@ use Model\LastLogin; * @package controller * @author Frederic Guillot * + * @property \Core\Session $session + * @property \Core\Template $template * @property \Model\Acl $acl * @property \Model\Authentication $authentication * @property \Model\Action $action @@ -49,6 +51,7 @@ use Model\LastLogin; * @property \Model\SubtaskHistory $subtaskHistory * @property \Model\TimeTracking $timeTracking * @property \Model\User $user + * @property \Model\UserSession $userSession * @property \Model\Webhook $webhook */ abstract class Base @@ -69,22 +72,6 @@ abstract class Base */ protected $response; - /** - * Template instance - * - * @accesss protected - * @var \Core\Template - */ - protected $template; - - /** - * Session instance - * - * @accesss public - * @var \Core\Session - */ - protected $session; - /** * Container instance * @@ -104,8 +91,6 @@ abstract class Base $this->container = $container; $this->request = new Request; $this->response = new Response; - $this->session = new Session; - $this->template = new Template; } /** @@ -115,9 +100,15 @@ abstract class Base */ public function __destruct() { - // foreach ($this->container['db']->getLogMessages() as $message) { - // $this->container['logger']->addDebug($message); - // } + if (DEBUG) { + + foreach ($this->container['db']->getLogMessages() as $message) { + $this->container['logger']->debug($message); + } + + $this->container['logger']->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nb_queries)); + $this->container['logger']->debug('RENDERING={time}', array('time' => microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'])); + } } /** @@ -129,19 +120,16 @@ abstract class Base */ public function __get($name) { - return Tool::loadModel($this->container, $name); + return $this->container[$name]; } /** - * Method executed before each action + * Send HTTP headers * - * @access public + * @access private */ - public function beforeAction($controller, $action) + private function sendHeaders($action) { - // Start the session - $this->session->open(BASE_URL_DIRECTORY); - // HTTP secure headers $this->response->csp(array('style-src' => "'self' 'unsafe-inline'")); $this->response->nosniff(); @@ -155,12 +143,33 @@ abstract class Base if (ENABLE_HSTS) { $this->response->hsts(); } + } - $this->config->setupTranslations(); - $this->config->setupTimezone(); + /** + * Method executed before each action + * + * @access public + */ + public function beforeAction($controller, $action) + { + // Start the session + $this->session->open(BASE_URL_DIRECTORY); + $this->sendHeaders($action); + $this->container['dispatcher']->dispatch('session.bootstrap', new Event); - // Authentication - if (! $this->authentication->isAuthenticated($controller, $action)) { + if (! $this->acl->isPublicAction($controller, $action)) { + $this->handleAuthenticatedUser($controller, $action); + } + } + + /** + * Check page access and authentication + * + * @access public + */ + public function handleAuthenticatedUser($controller, $action) + { + if (! $this->authentication->isAuthenticated()) { if ($this->request->isAjax()) { $this->response->text('Not Authorized', 401); @@ -169,33 +178,8 @@ abstract class Base $this->response->redirect('?controller=user&action=login&redirect_query='.urlencode($this->request->getQueryString())); } - // Check if the user is allowed to see this page - if (! $this->acl->isPageAccessAllowed($controller, $action)) { - $this->response->redirect('?controller=user&action=forbidden'); - } - - // Attach events - $this->attachEvents(); - } - - /** - * Attach events - * - * @access private - */ - private function attachEvents() - { - $models = array( - 'projectActivity', // Order is important - 'projectDailySummary', - 'action', - 'project', - 'webhook', - 'notification', - ); - - foreach ($models as $model) { - $this->$model->attachEvents(); + if (! $this->acl->isAllowed($controller, $action, $this->request->getIntegerParam('project_id', 0))) { + $this->forbidden(); } } @@ -239,19 +223,6 @@ abstract class Base } } - /** - * Check if the current user have access to the given project - * - * @access protected - * @param integer $project_id Project id - */ - protected function checkProjectPermissions($project_id) - { - if ($this->acl->isRegularUser() && ! $this->projectPermission->isUserAllowed($project_id, $this->acl->getUserId())) { - $this->forbidden(); - } - } - /** * Redirection when there is no project in the database * @@ -273,14 +244,10 @@ abstract class Base */ protected function taskLayout($template, array $params) { - if (isset($params['task']) && $this->taskPermission->canRemoveTask($params['task']) === false) { - $params['hide_remove_menu'] = true; - } - - $content = $this->template->load($template, $params); + $content = $this->template->render($template, $params); $params['task_content_for_layout'] = $content; $params['title'] = $params['task']['project_name'].' > '.$params['task']['title']; - $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); return $this->template->layout('task/layout', $params); } @@ -295,10 +262,10 @@ abstract class Base */ protected function projectLayout($template, array $params) { - $content = $this->template->load($template, $params); + $content = $this->template->render($template, $params); $params['project_content_for_layout'] = $content; $params['title'] = $params['project']['name'] === $params['title'] ? $params['title'] : $params['project']['name'].' > '.$params['title']; - $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); return $this->template->layout('project/layout', $params); } @@ -313,12 +280,10 @@ abstract class Base { $task = $this->taskFinder->getDetails($this->request->getIntegerParam('task_id')); - if (! $task) { + if (! $task || $task['project_id'] != $this->request->getIntegerParam('project_id')) { $this->notfound(); } - $this->checkProjectPermissions($task['project_id']); - return $task; } @@ -339,29 +304,6 @@ abstract class Base $this->response->redirect('?controller=project'); } - $this->checkProjectPermissions($project['id']); - - return $project; - } - - /** - * Common method to get a project with administration rights - * - * @access protected - * @return array - */ - protected function getProjectManagement() - { - $project = $this->project->getById($this->request->getIntegerParam('project_id')); - - if (! $project) { - $this->notfound(); - } - - if ($this->acl->isRegularUser() && ! $this->projectPermission->adminAllowed($project['id'], $this->acl->getUserId())) { - $this->forbidden(); - } - return $project; } } diff --git a/sources/app/Controller/Board.php b/sources/app/Controller/Board.php index 7d498f8..48f2b51 100644 --- a/sources/app/Controller/Board.php +++ b/sources/app/Controller/Board.php @@ -2,10 +2,6 @@ namespace Controller; -use Model\Project as ProjectModel; -use Model\User as UserModel; -use Core\Security; - /** * Board controller * @@ -22,7 +18,7 @@ class Board extends Base public function moveColumn() { $this->checkCSRFParam(); - $project = $this->getProjectManagement(); + $project = $this->getProject(); $column_id = $this->request->getIntegerParam('column_id'); $direction = $this->request->getStringParam('direction'); @@ -43,7 +39,7 @@ class Board extends Base $task = $this->getTask(); $project = $this->project->getById($task['project_id']); - $this->response->html($this->template->load('board/assignee', array( + $this->response->html($this->template->render('board/assignee', array( 'values' => $task, 'users_list' => $this->projectPermission->getMemberList($project['id']), 'project' => $project, @@ -58,7 +54,6 @@ class Board extends Base public function updateAssignee() { $values = $this->request->getValues(); - $this->checkProjectPermissions($values['project_id']); list($valid,) = $this->taskValidator->validateAssigneeModification($values); @@ -82,7 +77,7 @@ class Board extends Base $task = $this->getTask(); $project = $this->project->getById($task['project_id']); - $this->response->html($this->template->load('board/category', array( + $this->response->html($this->template->render('board/category', array( 'values' => $task, 'categories_list' => $this->category->getList($project['id']), 'project' => $project, @@ -97,7 +92,6 @@ class Board extends Base public function updateCategory() { $values = $this->request->getValues(); - $this->checkProjectPermissions($values['project_id']); list($valid,) = $this->taskValidator->validateCategoryModification($values); @@ -130,12 +124,14 @@ class Board extends Base // Display the board with a specific layout $this->response->html($this->template->layout('board/public', array( 'project' => $project, - 'columns' => $this->board->get($project['id']), + 'swimlanes' => $this->board->getBoard($project['id']), 'categories' => $this->category->getList($project['id'], false), 'title' => $project['name'], 'no_layout' => true, 'not_editable' => true, 'board_public_refresh_interval' => $this->config->get('board_public_refresh_interval'), + 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), + 'board_highlight_period' => $this->config->get('board_highlight_period'), ))); } @@ -146,16 +142,16 @@ class Board extends Base */ public function index() { - $last_seen_project_id = $this->user->getLastSeenProjectId(); - $favorite_project_id = $this->user->getFavoriteProjectId(); + $last_seen_project_id = $this->userSession->getLastSeenProjectId(); + $favorite_project_id = $this->userSession->getFavoriteProjectId(); $project_id = $last_seen_project_id ?: $favorite_project_id; if (! $project_id) { - $projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); + $projects = $this->projectPermission->getAllowedProjects($this->userSession->getId()); if (empty($projects)) { - if ($this->acl->isAdminUser()) { + if ($this->userSession->isAdmin()) { $this->redirectNoProject(); } @@ -177,18 +173,18 @@ class Board extends Base public function show($project_id = 0) { $project = $this->getProject($project_id); - $projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); + $projects = $this->projectPermission->getAllowedProjects($this->userSession->getId()); $board_selector = $projects; unset($board_selector[$project['id']]); - $this->user->storeLastSeenProjectId($project['id']); + $this->userSession->storeLastSeenProjectId($project['id']); $this->response->html($this->template->layout('board/index', array( 'users' => $this->projectPermission->getMemberList($project['id'], true, true), 'projects' => $projects, 'project' => $project, - 'board' => $this->board->get($project['id']), + 'swimlanes' => $this->board->getBoard($project['id']), 'categories' => $this->category->getList($project['id'], true, true), 'title' => $project['name'], 'board_selector' => $board_selector, @@ -202,11 +198,10 @@ class Board extends Base * * @access public */ - public function edit() + public function edit(array $values = array(), array $errors = array()) { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $columns = $this->board->getColumns($project['id']); - $values = array(); foreach ($columns as $column) { $values['title['.$column['id'].']'] = $column['title']; @@ -214,7 +209,7 @@ class Board extends Base } $this->response->html($this->projectLayout('board/edit', array( - 'errors' => array(), + 'errors' => $errors, 'values' => $values + array('project_id' => $project['id']), 'columns' => $columns, 'project' => $project, @@ -229,7 +224,7 @@ class Board extends Base */ public function update() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $columns = $this->board->getColumns($project['id']); $data = $this->request->getValues(); $values = $columns_list = array(); @@ -253,13 +248,7 @@ class Board extends Base } } - $this->response->html($this->projectLayout('board/edit', array( - 'errors' => $errors, - 'values' => $values + array('project_id' => $project['id']), - 'columns' => $columns, - 'project' => $project, - 'title' => t('Edit board') - ))); + $this->edit($values, $errors); } /** @@ -269,7 +258,7 @@ class Board extends Base */ public function add() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $columns = $this->board->getColumnsList($project['id']); $data = $this->request->getValues(); $values = array(); @@ -291,13 +280,7 @@ class Board extends Base } } - $this->response->html($this->projectLayout('board/edit', array( - 'errors' => $errors, - 'values' => $values + $data, - 'columns' => $columns, - 'project' => $project, - 'title' => t('Edit board') - ))); + $this->edit($values, $errors); } /** @@ -307,7 +290,7 @@ class Board extends Base */ public function remove() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); if ($this->request->getStringParam('remove') === 'yes') { @@ -339,35 +322,38 @@ class Board extends Base { $project_id = $this->request->getIntegerParam('project_id'); - if ($project_id > 0 && $this->request->isAjax()) { - - if (! $this->projectPermission->isUserAllowed($project_id, $this->acl->getUserId())) { - $this->response->text('Forbidden', 403); - } - - $values = $this->request->getJson(); - - if ($this->taskPosition->movePosition($project_id, $values['task_id'], $values['column_id'], $values['position'])) { - - $this->response->html( - $this->template->load('board/show', array( - 'project' => $this->project->getById($project_id), - 'board' => $this->board->get($project_id), - 'categories' => $this->category->getList($project_id, false), - 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), - 'board_highlight_period' => $this->config->get('board_highlight_period'), - )), - 201 - ); - } - else { - - $this->response->status(400); - } + if (! $project_id || ! $this->request->isAjax()) { + return $this->response->status(403); } - else { - $this->response->status(403); + + if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) { + $this->response->text('Forbidden', 403); } + + $values = $this->request->getJson(); + + $result =$this->taskPosition->movePosition( + $project_id, + $values['task_id'], + $values['column_id'], + $values['position'], + $values['swimlane_id'] + ); + + if (! $result) { + return $this->response->status(400); + } + + $this->response->html( + $this->template->render('board/show', array( + 'project' => $this->project->getById($project_id), + 'swimlanes' => $this->board->getBoard($project_id), + 'categories' => $this->category->getList($project_id, false), + 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), + 'board_highlight_period' => $this->config->get('board_highlight_period'), + )), + 201 + ); } /** @@ -377,33 +363,30 @@ class Board extends Base */ public function check() { - if ($this->request->isAjax()) { - - $project_id = $this->request->getIntegerParam('project_id'); - $timestamp = $this->request->getIntegerParam('timestamp'); - - if ($project_id > 0 && ! $this->projectPermission->isUserAllowed($project_id, $this->acl->getUserId())) { - $this->response->text('Forbidden', 403); - } - - if ($this->project->isModifiedSince($project_id, $timestamp)) { - $this->response->html( - $this->template->load('board/show', array( - 'project' => $this->project->getById($project_id), - 'board' => $this->board->get($project_id), - 'categories' => $this->category->getList($project_id, false), - 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), - 'board_highlight_period' => $this->config->get('board_highlight_period'), - )) - ); - } - else { - $this->response->status(304); - } + if (! $this->request->isAjax()) { + return $this->response->status(403); } - else { - $this->response->status(403); + + $project_id = $this->request->getIntegerParam('project_id'); + $timestamp = $this->request->getIntegerParam('timestamp'); + + if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) { + $this->response->text('Forbidden', 403); } + + if (! $this->project->isModifiedSince($project_id, $timestamp)) { + return $this->response->status(304); + } + + $this->response->html( + $this->template->render('board/show', array( + 'project' => $this->project->getById($project_id), + 'swimlanes' => $this->board->getBoard($project_id), + 'categories' => $this->category->getList($project_id, false), + 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), + 'board_highlight_period' => $this->config->get('board_highlight_period'), + )) + ); } /** @@ -414,8 +397,9 @@ class Board extends Base public function subtasks() { $task = $this->getTask(); - $this->response->html($this->template->load('board/subtasks', array( - 'subtasks' => $this->subTask->getAll($task['id']) + $this->response->html($this->template->render('board/subtasks', array( + 'subtasks' => $this->subTask->getAll($task['id']), + 'task' => $task, ))); } @@ -429,8 +413,9 @@ class Board extends Base $task = $this->getTask(); $this->subTask->toggleStatus($this->request->getIntegerParam('subtask_id')); - $this->response->html($this->template->load('board/subtasks', array( - 'subtasks' => $this->subTask->getAll($task['id']) + $this->response->html($this->template->render('board/subtasks', array( + 'subtasks' => $this->subTask->getAll($task['id']), + 'task' => $task, ))); } @@ -443,8 +428,9 @@ class Board extends Base { $task = $this->getTask(); - $this->response->html($this->template->load('board/files', array( - 'files' => $this->file->getAll($task['id']) + $this->response->html($this->template->render('board/files', array( + 'files' => $this->file->getAll($task['id']), + 'task' => $task, ))); } @@ -457,7 +443,7 @@ class Board extends Base { $task = $this->getTask(); - $this->response->html($this->template->load('board/comments', array( + $this->response->html($this->template->render('board/comments', array( 'comments' => $this->comment->getAll($task['id']) ))); } @@ -471,7 +457,7 @@ class Board extends Base { $task = $this->getTask(); - $this->response->html($this->template->load('board/description', array( + $this->response->html($this->template->render('board/description', array( 'task' => $task ))); } diff --git a/sources/app/Controller/Category.php b/sources/app/Controller/Category.php index 27c0d9f..68961a0 100644 --- a/sources/app/Controller/Category.php +++ b/sources/app/Controller/Category.php @@ -14,7 +14,7 @@ class Category extends Base * Get the category (common method between actions) * * @access private - * @param $project_id + * @param integer $project_id * @return array */ private function getCategory($project_id) @@ -36,7 +36,7 @@ class Category extends Base */ public function index(array $values = array(), array $errors = array()) { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $this->response->html($this->projectLayout('category/index', array( 'categories' => $this->category->getList($project['id'], false), @@ -48,13 +48,13 @@ class Category extends Base } /** - * Validate and save a new project + * Validate and save a new category * * @access public */ public function save() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues(); list($valid, $errors) = $this->category->validateCreation($values); @@ -80,7 +80,7 @@ class Category extends Base */ public function edit(array $values = array(), array $errors = array()) { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $category = $this->getCategory($project['id']); $this->response->html($this->projectLayout('category/edit', array( @@ -98,7 +98,7 @@ class Category extends Base */ public function update() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues(); list($valid, $errors) = $this->category->validateModification($values); @@ -124,7 +124,7 @@ class Category extends Base */ public function confirm() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $category = $this->getCategory($project['id']); $this->response->html($this->projectLayout('category/remove', array( @@ -142,7 +142,7 @@ class Category extends Base public function remove() { $this->checkCSRFParam(); - $project = $this->getProjectManagement(); + $project = $this->getProject(); $category = $this->getCategory($project['id']); if ($this->category->remove($category['id'])) { diff --git a/sources/app/Controller/Comment.php b/sources/app/Controller/Comment.php index fb21353..9796ea3 100644 --- a/sources/app/Controller/Comment.php +++ b/sources/app/Controller/Comment.php @@ -24,7 +24,7 @@ class Comment extends Base $this->notfound(); } - if (! $this->acl->isAdminUser() && $comment['user_id'] != $this->acl->getUserId()) { + if (! $this->userSession->isAdmin() && $comment['user_id'] != $this->userSession->getId()) { $this->response->html($this->template->layout('comment/forbidden', array( 'title' => t('Access Forbidden') ))); @@ -44,7 +44,7 @@ class Comment extends Base if (empty($values)) { $values = array( - 'user_id' => $this->acl->getUserId(), + 'user_id' => $this->userSession->getId(), 'task_id' => $task['id'], ); } @@ -78,7 +78,7 @@ class Comment extends Base $this->session->flashError(t('Unable to create your comment.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comments'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#comments'); } $this->create($values, $errors); @@ -125,7 +125,7 @@ class Comment extends Base $this->session->flashError(t('Unable to update your comment.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comment-'.$comment['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#comment-'.$comment['id']); } $this->edit($values, $errors); @@ -166,6 +166,6 @@ class Comment extends Base $this->session->flashError(t('Unable to remove this comment.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comments'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#comments'); } } diff --git a/sources/app/Controller/Config.php b/sources/app/Controller/Config.php index 199259d..9005c30 100644 --- a/sources/app/Controller/Config.php +++ b/sources/app/Controller/Config.php @@ -20,10 +20,10 @@ class Config extends Base */ private function layout($template, array $params) { - $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); $params['values'] = $this->config->getAll(); $params['errors'] = array(); - $params['config_content_for_layout'] = $this->template->load($template, $params); + $params['config_content_for_layout'] = $this->template->render($template, $params); return $this->template->layout('config/layout', $params); } diff --git a/sources/app/Controller/Export.php b/sources/app/Controller/Export.php new file mode 100644 index 0000000..1997a4e --- /dev/null +++ b/sources/app/Controller/Export.php @@ -0,0 +1,75 @@ +getProject(); + $from = $this->request->getStringParam('from'); + $to = $this->request->getStringParam('to'); + + if ($from && $to) { + $data = $this->$model->$method($project['id'], $from, $to); + $this->response->forceDownload($filename.'.csv'); + $this->response->csv($data); + } + + $this->response->html($this->projectLayout('export/'.$action, array( + 'values' => array( + 'controller' => 'export', + 'action' => $action, + 'project_id' => $project['id'], + 'from' => $from, + 'to' => $to, + ), + 'errors' => array(), + 'date_format' => $this->config->get('application_date_format'), + 'date_formats' => $this->dateParser->getAvailableFormats(), + 'project' => $project, + 'title' => $page_title, + ))); + } + + /** + * Task export + * + * @access public + */ + public function tasks() + { + $this->common('taskExport', 'export', t('Tasks'), 'tasks', t('Tasks Export')); + } + + /** + * Subtask export + * + * @access public + */ + public function subtasks() + { + $this->common('subtaskExport', 'export', t('Subtasks'), 'subtasks', t('Subtasks Export')); + } + + /** + * Daily project summary export + * + * @access public + */ + public function summary() + { + $this->common('projectDailySummary', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export')); + } +} diff --git a/sources/app/Controller/File.php b/sources/app/Controller/File.php index ae44cac..6305261 100644 --- a/sources/app/Controller/File.php +++ b/sources/app/Controller/File.php @@ -37,11 +37,11 @@ class File extends Base $task = $this->getTask(); if ($this->file->upload($task['project_id'], $task['id'], 'files') === true) { - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#attachments'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#attachments'); } else { $this->session->flashError(t('Unable to upload the file.')); - $this->response->redirect('?controller=file&action=create&task_id='.$task['id']); + $this->response->redirect('?controller=file&action=create&task_id='.$task['id'].'&project_id='.$task['project_id']); } } @@ -61,7 +61,7 @@ class File extends Base $this->response->binary(file_get_contents($filename)); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); } /** @@ -75,8 +75,9 @@ class File extends Base $file = $this->file->getById($this->request->getIntegerParam('file_id')); if ($file['task_id'] == $task['id']) { - $this->response->html($this->template->load('file/open', array( - 'file' => $file + $this->response->html($this->template->render('file/open', array( + 'file' => $file, + 'task' => $task, ))); } } @@ -119,7 +120,7 @@ class File extends Base $this->session->flashError(t('Unable to remove this file.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); } /** diff --git a/sources/app/Controller/Project.php b/sources/app/Controller/Project.php index 83c81ca..d0da53d 100644 --- a/sources/app/Controller/Project.php +++ b/sources/app/Controller/Project.php @@ -2,8 +2,6 @@ namespace Controller; -use Model\Task as TaskModel; - /** * Project controller * @@ -19,7 +17,7 @@ class Project extends Base */ public function index() { - $projects = $this->project->getAll($this->acl->isRegularUser()); + $projects = $this->project->getAll(! $this->userSession->isAdmin()); $nb_projects = count($projects); $active_projects = array(); $inactive_projects = array(); @@ -34,7 +32,7 @@ class Project extends Base } $this->response->html($this->template->layout('project/index', array( - 'board_selector' => $this->projectPermission->getAllowedProjects($this->acl->getUserId()), + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), 'active_projects' => $active_projects, 'inactive_projects' => $inactive_projects, 'nb_projects' => $nb_projects, @@ -54,77 +52,10 @@ class Project extends Base $this->response->html($this->projectLayout('project/show', array( 'project' => $project, 'stats' => $this->project->getStats($project['id']), - 'webhook_token' => $this->config->get('webhook_token'), 'title' => $project['name'], ))); } - /** - * Task export - * - * @access public - */ - public function exportTasks() - { - $project = $this->getProjectManagement(); - $from = $this->request->getStringParam('from'); - $to = $this->request->getStringParam('to'); - - if ($from && $to) { - $data = $this->taskExport->export($project['id'], $from, $to); - $this->response->forceDownload('Tasks_'.date('Y_m_d_H_i').'.csv'); - $this->response->csv($data); - } - - $this->response->html($this->projectLayout('project/export_tasks', array( - 'values' => array( - 'controller' => 'project', - 'action' => 'exportTasks', - 'project_id' => $project['id'], - 'from' => $from, - 'to' => $to, - ), - 'errors' => array(), - 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), - 'project' => $project, - 'title' => t('Tasks Export') - ))); - } - - /** - * Daily project summary export - * - * @access public - */ - public function exportDailyProjectSummary() - { - $project = $this->getProjectManagement(); - $from = $this->request->getStringParam('from'); - $to = $this->request->getStringParam('to'); - - if ($from && $to) { - $data = $this->projectDailySummary->getAggregatedMetrics($project['id'], $from, $to); - $this->response->forceDownload('Daily_Summary_'.date('Y_m_d_H_i').'.csv'); - $this->response->csv($data); - } - - $this->response->html($this->projectLayout('project/export_daily_summary', array( - 'values' => array( - 'controller' => 'project', - 'action' => 'exportDailyProjectSummary', - 'project_id' => $project['id'], - 'from' => $from, - 'to' => $to, - ), - 'errors' => array(), - 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), - 'project' => $project, - 'title' => t('Daily project summary export') - ))); - } - /** * Public access management * @@ -132,7 +63,7 @@ class Project extends Base */ public function share() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $switch = $this->request->getStringParam('switch'); if ($switch === 'enable' || $switch === 'disable') { @@ -154,6 +85,22 @@ class Project extends Base ))); } + /** + * Integrations page + * + * @access public + */ + public function integration() + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('project/integrations', array( + 'project' => $project, + 'title' => t('Integrations'), + 'webhook_token' => $this->config->get('webhook_token'), + ))); + } + /** * Display a form to edit a project * @@ -161,7 +108,7 @@ class Project extends Base */ public function edit(array $values = array(), array $errors = array()) { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $this->response->html($this->projectLayout('project/edit', array( 'values' => empty($values) ? $project : $values, @@ -178,7 +125,7 @@ class Project extends Base */ public function update() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues(); list($valid, $errors) = $this->project->validateModification($values); @@ -203,7 +150,7 @@ class Project extends Base */ public function users() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $this->response->html($this->projectLayout('project/users', array( 'project' => $project, @@ -219,7 +166,7 @@ class Project extends Base */ public function allowEverybody() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues() + array('is_everybody_allowed' => 0); list($valid,) = $this->projectPermission->validateProjectModification($values); @@ -248,7 +195,37 @@ class Project extends Base if ($valid) { - if ($this->projectPermission->allowUser($values['project_id'], $values['user_id'])) { + if ($this->projectPermission->addMember($values['project_id'], $values['user_id'])) { + $this->session->flash(t('Project updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update this project.')); + } + } + + $this->response->redirect('?controller=project&action=users&project_id='.$values['project_id']); + } + + /** + * Change the role of a project member + * + * @access public + */ + public function role() + { + $this->checkCSRFParam(); + + $values = array( + 'project_id' => $this->request->getIntegerParam('project_id'), + 'user_id' => $this->request->getIntegerParam('user_id'), + 'is_owner' => $this->request->getIntegerParam('is_owner'), + ); + + list($valid,) = $this->projectPermission->validateUserModification($values); + + if ($valid) { + + if ($this->projectPermission->changeRole($values['project_id'], $values['user_id'], $values['is_owner'])) { $this->session->flash(t('Project updated successfully.')); } else { @@ -277,7 +254,7 @@ class Project extends Base if ($valid) { - if ($this->projectPermission->revokeUser($values['project_id'], $values['user_id'])) { + if ($this->projectPermission->revokeMember($values['project_id'], $values['user_id'])) { $this->session->flash(t('Project updated successfully.')); } else { @@ -295,7 +272,7 @@ class Project extends Base */ public function remove() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); if ($this->request->getStringParam('remove') === 'yes') { @@ -324,7 +301,7 @@ class Project extends Base */ public function duplicate() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); if ($this->request->getStringParam('duplicate') === 'yes') { @@ -352,7 +329,7 @@ class Project extends Base */ public function disable() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); if ($this->request->getStringParam('disable') === 'yes') { @@ -380,7 +357,7 @@ class Project extends Base */ public function enable() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); if ($this->request->getStringParam('enable') === 'yes') { @@ -416,7 +393,7 @@ class Project extends Base $this->forbidden(true); } - $this->response->xml($this->template->load('project/feed', array( + $this->response->xml($this->template->render('project/feed', array( 'events' => $this->projectActivity->getProject($project['id']), 'project' => $project, ))); @@ -432,7 +409,7 @@ class Project extends Base $project = $this->getProject(); $this->response->html($this->template->layout('project/activity', array( - 'board_selector' => $this->projectPermission->getAllowedProjects($this->acl->getUserId()), + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), 'events' => $this->projectActivity->getProject($project['id']), 'project' => $project, 'title' => t('%s\'s activity', $project['name']) @@ -461,7 +438,7 @@ class Project extends Base } $this->response->html($this->template->layout('project/search', array( - 'board_selector' => $this->projectPermission->getAllowedProjects($this->acl->getUserId()), + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), 'tasks' => $tasks, 'nb_tasks' => $nb_tasks, 'pagination' => array( @@ -504,7 +481,7 @@ class Project extends Base $nb_tasks = $this->taskPaginator->countClosedTasks($project['id']); $this->response->html($this->template->layout('project/tasks', array( - 'board_selector' => $this->projectPermission->getAllowedProjects($this->acl->getUserId()), + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), 'pagination' => array( 'controller' => 'project', 'action' => 'tasks', @@ -531,10 +508,10 @@ class Project extends Base */ public function create(array $values = array(), array $errors = array()) { - $is_private = $this->request->getIntegerParam('private', $this->acl->isRegularUser()); + $is_private = $this->request->getIntegerParam('private', $this->userSession->isAdmin() ? 0 : 1); $this->response->html($this->template->layout('project/new', array( - 'board_selector' => $this->projectPermission->getAllowedProjects($this->acl->getUserId()), + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), 'values' => empty($values) ? array('is_private' => $is_private) : $values, 'errors' => $errors, 'title' => $is_private ? t('New private project') : t('New project'), @@ -553,7 +530,7 @@ class Project extends Base if ($valid) { - $project_id = $this->project->create($values, $this->acl->getUserId(), true); + $project_id = $this->project->create($values, $this->userSession->getId(), true); if ($project_id) { $this->session->flash(t('Your project have been created successfully.')); diff --git a/sources/app/Controller/Subtask.php b/sources/app/Controller/Subtask.php index 948f3c7..0521b89 100644 --- a/sources/app/Controller/Subtask.php +++ b/sources/app/Controller/Subtask.php @@ -73,10 +73,10 @@ class Subtask extends Base } if (isset($values['another_subtask']) && $values['another_subtask'] == 1) { - $this->response->redirect('?controller=subtask&action=create&task_id='.$task['id'].'&another_subtask=1'); + $this->response->redirect('?controller=subtask&action=create&task_id='.$task['id'].'&another_subtask=1&project_id='.$task['project_id']); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#subtasks'); } $this->create($values, $errors); @@ -110,7 +110,7 @@ class Subtask extends Base public function update() { $task = $this->getTask(); - $subtask = $this->getSubtask(); + $this->getSubtask(); $values = $this->request->getValues(); list($valid, $errors) = $this->subTask->validateModification($values); @@ -124,7 +124,7 @@ class Subtask extends Base $this->session->flashError(t('Unable to update your sub-task.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#subtasks'); } $this->edit($values, $errors); @@ -164,7 +164,7 @@ class Subtask extends Base $this->session->flashError(t('Unable to remove this sub-task.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#subtasks'); } /** @@ -181,6 +181,6 @@ class Subtask extends Base $this->session->flashError(t('Unable to update your sub-task.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#subtasks'); } } diff --git a/sources/app/Controller/Swimlane.php b/sources/app/Controller/Swimlane.php new file mode 100644 index 0000000..de2f1f1 --- /dev/null +++ b/sources/app/Controller/Swimlane.php @@ -0,0 +1,256 @@ +swimlane->getById($this->request->getIntegerParam('swimlane_id')); + + if (! $swimlane) { + $this->session->flashError(t('Swimlane not found.')); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project_id); + } + + return $swimlane; + } + + /** + * List of swimlanes for a given project + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('swimlane/index', array( + 'default_swimlane' => $this->swimlane->getDefault($project['id']), + 'active_swimlanes' => $this->swimlane->getAllByStatus($project['id'], SwimlaneModel::ACTIVE), + 'inactive_swimlanes' => $this->swimlane->getAllByStatus($project['id'], SwimlaneModel::INACTIVE), + 'values' => $values + array('project_id' => $project['id']), + 'errors' => $errors, + 'project' => $project, + 'title' => t('Swimlanes') + ))); + } + + /** + * Validate and save a new swimlane + * + * @access public + */ + public function save() + { + $project = $this->getProject(); + + $values = $this->request->getValues(); + list($valid, $errors) = $this->swimlane->validateCreation($values); + + if ($valid) { + + if ($this->swimlane->create($project['id'], $values['name'])) { + $this->session->flash(t('Your swimlane have been created successfully.')); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + else { + $this->session->flashError(t('Unable to create your swimlane.')); + } + } + + $this->index($values, $errors); + } + + /** + * Change the default swimlane + * + * @access public + */ + public function change() + { + $project = $this->getProject(); + + $values = $this->request->getValues(); + list($valid,) = $this->swimlane->validateDefaultModification($values); + + if ($valid) { + + if ($this->swimlane->updateDefault($values)) { + $this->session->flash(t('The default swimlane have been updated successfully.')); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + else { + $this->session->flashError(t('Unable to update this swimlane.')); + } + } + + $this->index(); + } + + /** + * Edit a swimlane (display the form) + * + * @access public + */ + public function edit(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + $swimlane = $this->getSwimlane($project['id']); + + $this->response->html($this->projectLayout('swimlane/edit', array( + 'values' => empty($values) ? $swimlane : $values, + 'errors' => $errors, + 'project' => $project, + 'title' => t('Swimlanes') + ))); + } + + /** + * Edit a swimlane (validate the form and update the database) + * + * @access public + */ + public function update() + { + $project = $this->getProject(); + + $values = $this->request->getValues(); + list($valid, $errors) = $this->swimlane->validateModification($values); + + if ($valid) { + + if ($this->swimlane->rename($values['id'], $values['name'])) { + $this->session->flash(t('Swimlane updated successfully.')); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + else { + $this->session->flashError(t('Unable to update this swimlane.')); + } + } + + $this->edit($values, $errors); + } + + /** + * Confirmation dialog before removing a swimlane + * + * @access public + */ + public function confirm() + { + $project = $this->getProject(); + $swimlane = $this->getSwimlane($project['id']); + + $this->response->html($this->projectLayout('swimlane/remove', array( + 'project' => $project, + 'swimlane' => $swimlane, + 'title' => t('Remove a swimlane') + ))); + } + + /** + * Remove a swimlane + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + if ($this->swimlane->remove($project['id'], $swimlane_id)) { + $this->session->flash(t('Swimlane removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this swimlane.')); + } + + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + + /** + * Disable a swimlane + * + * @access public + */ + public function disable() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + if ($this->swimlane->disable($project['id'], $swimlane_id)) { + $this->session->flash(t('Swimlane updated successfully.')); + } else { + $this->session->flashError(t('Unable to update this swimlane.')); + } + + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + + /** + * Enable a swimlane + * + * @access public + */ + public function enable() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + if ($this->swimlane->enable($project['id'], $swimlane_id)) { + $this->session->flash(t('Swimlane updated successfully.')); + } else { + $this->session->flashError(t('Unable to update this swimlane.')); + } + + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + + /** + * Move up a swimlane + * + * @access public + */ + public function moveup() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + $this->swimlane->moveUp($project['id'], $swimlane_id); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + + /** + * Move down a swimlane + * + * @access public + */ + public function movedown() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + $this->swimlane->moveDown($project['id'], $swimlane_id); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } +} diff --git a/sources/app/Controller/Task.php b/sources/app/Controller/Task.php index 8d38317..7f85f36 100644 --- a/sources/app/Controller/Task.php +++ b/sources/app/Controller/Task.php @@ -89,11 +89,12 @@ class Task extends Base public function create(array $values = array(), array $errors = array()) { $project = $this->getProject(); - $method = $this->request->isAjax() ? 'load' : 'layout'; + $method = $this->request->isAjax() ? 'render' : 'layout'; if (empty($values)) { $values = array( + 'swimlane_id' => $this->request->getIntegerParam('swimlane_id'), 'column_id' => $this->request->getIntegerParam('column_id'), 'color_id' => $this->request->getStringParam('color_id'), 'owner_id' => $this->request->getIntegerParam('owner_id'), @@ -125,9 +126,7 @@ class Task extends Base { $project = $this->getProject(); $values = $this->request->getValues(); - $values['creator_id'] = $this->acl->getUserId(); - - $this->checkProjectPermissions($project['id']); + $values['creator_id'] = $this->userSession->getId(); list($valid, $errors) = $this->taskValidator->validateCreation($values); @@ -142,7 +141,7 @@ class Task extends Base $this->response->redirect('?controller=task&action=create&'.http_build_query($values)); } else { - $this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']); + $this->response->redirect('?controller=board&action=show&project_id='.$project['id']); } } else { @@ -158,16 +157,20 @@ class Task extends Base * * @access public */ - public function edit() + public function edit(array $values = array(), array $errors = array()) { $task = $this->getTask(); $ajax = $this->request->isAjax(); - $this->dateParser->format($task, array('date_due')); + if (empty($values)) { + $values = $task; + } + + $this->dateParser->format($values, array('date_due')); $params = array( - 'values' => $task, - 'errors' => array(), + 'values' => $values, + 'errors' => $errors, 'task' => $task, 'users_list' => $this->projectPermission->getMemberList($task['project_id']), 'colors_list' => $this->color->getList(), @@ -178,7 +181,7 @@ class Task extends Base ); if ($ajax) { - $this->response->html($this->template->load('task/edit', $params)); + $this->response->html($this->template->render('task/edit', $params)); } else { $this->response->html($this->taskLayout('task/edit', $params)); @@ -206,7 +209,7 @@ class Task extends Base $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); } else { - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); } } else { @@ -214,18 +217,7 @@ class Task extends Base } } - $this->response->html($this->taskLayout('task/edit', array( - 'values' => $values, - 'errors' => $errors, - 'task' => $task, - 'columns_list' => $this->board->getColumnsList($values['project_id']), - 'users_list' => $this->projectPermission->getMemberList($values['project_id']), - 'colors_list' => $this->color->getList(), - 'categories_list' => $this->category->getList($values['project_id']), - 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), - 'ajax' => $this->request->isAjax(), - ))); + $this->edit($values, $errors); } /** @@ -247,7 +239,7 @@ class Task extends Base $this->session->flashError(t('Unable to update your task.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); } /** @@ -269,7 +261,7 @@ class Task extends Base $this->session->flashError(t('Unable to close this task.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); } $this->response->html($this->taskLayout('task/close', array( @@ -296,7 +288,7 @@ class Task extends Base $this->session->flashError(t('Unable to open this task.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); } $this->response->html($this->taskLayout('task/open', array( @@ -351,10 +343,10 @@ class Task extends Base if ($task_id) { $this->session->flash(t('Task created successfully.')); - $this->response->redirect('?controller=task&action=show&task_id='.$task_id); + $this->response->redirect('?controller=task&action=show&task_id='.$task_id.'&project_id='.$task['project_id']); } else { $this->session->flashError(t('Unable to create this task.')); - $this->response->redirect('?controller=task&action=duplicate&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=duplicate&task_id='.$task['id'].'&project_id='.$task['project_id']); } } @@ -392,7 +384,7 @@ class Task extends Base $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); } else { - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); } } } @@ -409,7 +401,7 @@ class Task extends Base ); if ($ajax) { - $this->response->html($this->template->load('task/edit_description', $params)); + $this->response->html($this->template->render('task/edit_description', $params)); } else { $this->response->html($this->taskLayout('task/edit_description', $params)); @@ -426,7 +418,7 @@ class Task extends Base $task = $this->getTask(); $values = $task; $errors = array(); - $projects_list = $this->projectPermission->getMemberProjects($this->acl->getUserId()); + $projects_list = $this->projectPermission->getMemberProjects($this->userSession->getId()); unset($projects_list[$task['project_id']]); @@ -439,7 +431,7 @@ class Task extends Base if ($this->taskDuplication->moveToProject($task['id'], $values['project_id'])) { $this->session->flash(t('Task updated successfully.')); - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$values['project_id']); } else { $this->session->flashError(t('Unable to update your task.')); @@ -465,7 +457,7 @@ class Task extends Base $task = $this->getTask(); $values = $task; $errors = array(); - $projects_list = $this->projectPermission->getMemberProjects($this->acl->getUserId()); + $projects_list = $this->projectPermission->getMemberProjects($this->userSession->getId()); unset($projects_list[$task['project_id']]); @@ -478,7 +470,7 @@ class Task extends Base $task_id = $this->taskDuplication->duplicateToProject($task['id'], $values['project_id']); if ($task_id) { $this->session->flash(t('Task created successfully.')); - $this->response->redirect('?controller=task&action=show&task_id='.$task_id); + $this->response->redirect('?controller=task&action=show&task_id='.$task_id.'&project_id='.$values['project_id']); } else { $this->session->flashError(t('Unable to create your task.')); diff --git a/sources/app/Controller/User.php b/sources/app/Controller/User.php index 93b5ca1..7fddf70 100644 --- a/sources/app/Controller/User.php +++ b/sources/app/Controller/User.php @@ -18,7 +18,7 @@ class User extends Base public function logout() { $this->checkCSRFParam(); - $this->authentication->backend('rememberMe')->destroy($this->acl->getUserId()); + $this->authentication->backend('rememberMe')->destroy($this->userSession->getId()); $this->session->close(); $this->response->redirect('?controller=user&action=login'); } @@ -30,7 +30,7 @@ class User extends Base */ public function login(array $values = array(), array $errors = array()) { - if ($this->acl->isLogged()) { + if ($this->userSession->isLogged()) { $this->response->redirect('?controller=app'); } @@ -76,9 +76,9 @@ class User extends Base */ private function layout($template, array $params) { - $content = $this->template->load($template, $params); + $content = $this->template->render($template, $params); $params['user_content_for_layout'] = $content; - $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); if (isset($params['user'])) { $params['title'] = ($params['user']['name'] ?: $params['user']['username']).' (#'.$params['user']['id'].')'; @@ -101,7 +101,7 @@ class User extends Base $this->notfound(); } - if ($this->acl->isRegularUser() && $this->acl->getUserId() != $user['id']) { + if (! $this->userSession->isAdmin() && $this->userSession->getId() != $user['id']) { $this->forbidden(); } @@ -125,7 +125,7 @@ class User extends Base $this->response->html( $this->template->layout('user/index', array( - 'board_selector' => $this->projectPermission->getAllowedProjects($this->acl->getUserId()), + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), 'projects' => $this->project->getList(), 'nb_users' => $nb_users, 'users' => $users, @@ -151,7 +151,9 @@ class User extends Base public function create(array $values = array(), array $errors = array()) { $this->response->html($this->template->layout('user/new', array( - 'board_selector' => $this->projectPermission->getAllowedProjects($this->acl->getUserId()), + 'timezones' => $this->config->getTimezones(true), + 'languages' => $this->config->getLanguages(true), + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), 'projects' => $this->project->getList(), 'errors' => $errors, 'values' => $values, @@ -194,6 +196,8 @@ class User extends Base $this->response->html($this->layout('user/show', array( 'projects' => $this->projectPermission->getAllowedProjects($user['id']), 'user' => $user, + 'timezones' => $this->config->getTimezones(true), + 'languages' => $this->config->getLanguages(true), ))); } @@ -328,7 +332,7 @@ class User extends Base $values = $this->request->getValues(); - if ($this->acl->isAdminUser()) { + if ($this->userSession->isAdmin()) { $values += array('is_admin' => 0); } else { @@ -358,6 +362,8 @@ class User extends Base 'errors' => $errors, 'projects' => $this->projectPermission->filterProjects($this->project->getList(), $user['id']), 'user' => $user, + 'timezones' => $this->config->getTimezones(true), + 'languages' => $this->config->getLanguages(true), ))); } @@ -404,16 +410,16 @@ class User extends Base if (is_array($profile)) { // If the user is already logged, link the account otherwise authenticate - if ($this->acl->isLogged()) { + if ($this->userSession->isLogged()) { - if ($this->authentication->backend('google')->updateUser($this->acl->getUserId(), $profile)) { + if ($this->authentication->backend('google')->updateUser($this->userSession->getId(), $profile)) { $this->session->flash(t('Your Google Account is linked to your profile successfully.')); } else { $this->session->flashError(t('Unable to link your Google Account.')); } - $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); + $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId()); } else if ($this->authentication->backend('google')->authenticate($profile['id'])) { $this->response->redirect('?controller=app'); @@ -441,14 +447,14 @@ class User extends Base public function unlinkGoogle() { $this->checkCSRFParam(); - if ($this->authentication->backend('google')->unlink($this->acl->getUserId())) { + if ($this->authentication->backend('google')->unlink($this->userSession->getId())) { $this->session->flash(t('Your Google Account is not linked anymore to your profile.')); } else { $this->session->flashError(t('Unable to unlink your Google Account.')); } - $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); + $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId()); } /** @@ -466,16 +472,16 @@ class User extends Base if (is_array($profile)) { // If the user is already logged, link the account otherwise authenticate - if ($this->acl->isLogged()) { + if ($this->userSession->isLogged()) { - if ($this->authentication->backend('gitHub')->updateUser($this->acl->getUserId(), $profile)) { + if ($this->authentication->backend('gitHub')->updateUser($this->userSession->getId(), $profile)) { $this->session->flash(t('Your GitHub account was successfully linked to your profile.')); } else { $this->session->flashError(t('Unable to link your GitHub Account.')); } - $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); + $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId()); } else if ($this->authentication->backend('gitHub')->authenticate($profile['id'])) { $this->response->redirect('?controller=app'); @@ -506,13 +512,13 @@ class User extends Base $this->authentication->backend('gitHub')->revokeGitHubAccess(); - if ($this->authentication->backend('gitHub')->unlink($this->acl->getUserId())) { + if ($this->authentication->backend('gitHub')->unlink($this->userSession->getId())) { $this->session->flash(t('Your GitHub account is no longer linked to your profile.')); } else { $this->session->flashError(t('Unable to unlink your GitHub Account.')); } - $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); + $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId()); } } diff --git a/sources/app/Controller/Webhook.php b/sources/app/Controller/Webhook.php index dcd66a1..1ae3b0a 100644 --- a/sources/app/Controller/Webhook.php +++ b/sources/app/Controller/Webhook.php @@ -57,7 +57,27 @@ class Webhook extends Base $result = $this->githubWebhook->parsePayload( $this->request->getHeader('X-Github-Event'), - $this->request->getJson() + $this->request->getJson() ?: array() + ); + + echo $result ? 'PARSED' : 'IGNORED'; + } + + /** + * Handle Gitlab webhooks + * + * @access public + */ + public function gitlab() + { + if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) { + $this->response->text('Not Authorized', 401); + } + + $this->gitlabWebhook->setProjectId($this->request->getIntegerParam('project_id')); + + $result = $this->gitlabWebhook->parsePayload( + $this->request->getJson() ?: array() ); echo $result ? 'PARSED' : 'IGNORED'; diff --git a/sources/app/Core/Cache.php b/sources/app/Core/Cache.php new file mode 100644 index 0000000..670a76e --- /dev/null +++ b/sources/app/Core/Cache.php @@ -0,0 +1,58 @@ +container = $container; + $this->init(); + } + + /** + * Proxy cache + * + * Note: Arguments must be scalar types + * + * @access public + * @param string $container Container name + * @param string $method Container method + * @return mixed + */ + public function proxy($container, $method) + { + $args = func_get_args(); + $key = 'proxy_'.implode('_', $args); + $result = $this->get($key); + + if ($result === null) { + $result = call_user_func_array(array($this->container[$container], $method), array_splice($args, 2)); + $this->set($key, $result); + } + + return $result; + } +} diff --git a/sources/app/Core/Event.php b/sources/app/Core/Event.php deleted file mode 100644 index 935f8b9..0000000 --- a/sources/app/Core/Event.php +++ /dev/null @@ -1,175 +0,0 @@ -listeners[$eventName])) { - $this->listeners[$eventName] = array(); - } - - $this->listeners[$eventName][] = $listener; - } - - /** - * Trigger an event - * - * @access public - * @param string $eventName Event name - * @param array $data Event data - */ - public function trigger($eventName, array $data) - { - if (! $this->isEventTriggered($eventName)) { - - $this->events[$eventName] = $data; - - if (isset($this->listeners[$eventName])) { - - foreach ($this->listeners[$eventName] as $listener) { - - $this->lastEvent = $eventName; - - if ($listener->execute($data)) { - $this->lastListener = get_class($listener); - } - } - } - } - } - - /** - * Get the last listener executed - * - * @access public - * @return string Event name - */ - public function getLastListenerExecuted() - { - return $this->lastListener; - } - - /** - * Get the last fired event - * - * @access public - * @return string Event name - */ - public function getLastTriggeredEvent() - { - return $this->lastEvent; - } - - /** - * Get a list of triggered events - * - * @access public - * @return array - */ - public function getTriggeredEvents() - { - return $this->events; - } - - /** - * Get a list of triggered events - * - * @access public - * @return array - */ - public function getEventData($eventName) - { - return isset($this->events[$eventName]) ? $this->events[$eventName] : array(); - } - - /** - * Check if an event have been triggered - * - * @access public - * @param string $eventName Event name - * @return bool - */ - public function isEventTriggered($eventName) - { - return isset($this->events[$eventName]); - } - - /** - * Flush the list of triggered events - * - * @access public - */ - public function clearTriggeredEvents() - { - $this->events = array(); - $this->lastEvent = ''; - } - - /** - * Check if a listener bind to an event - * - * @access public - * @param string $eventName Event name - * @param mixed $instance Instance name or object itself - * @return bool Yes or no - */ - public function hasListener($eventName, $instance) - { - if (isset($this->listeners[$eventName])) { - foreach ($this->listeners[$eventName] as $listener) { - if ($listener instanceof $instance) { - return true; - } - } - } - - return false; - } -} diff --git a/sources/app/Core/FileCache.php b/sources/app/Core/FileCache.php new file mode 100644 index 0000000..2037f27 --- /dev/null +++ b/sources/app/Core/FileCache.php @@ -0,0 +1,41 @@ +container = $container; + } + + /** + * Load automatically models + * + * @access public + * @param string $name Model name + * @return mixed + */ + public function __get($name) + { + return $this->container[$name]; + } + + /** + * Proxy cache helper for acl::isManagerActionAllowed() + * + * @access public + * @param integer $project_id + * @return boolean + */ + public function isManager($project_id) + { + if ($this->userSession->isAdmin()) { + return true; + } + + return $this->container['memoryCache']->proxy('acl', 'isManagerActionAllowed', $project_id); + } + + /** + * Return the user full name + * + * @param array $user User properties + * @return string + */ + public function getFullname(array $user = array()) + { + return $this->user->getFullname(empty($user) ? $_SESSION['user'] : $user); + } + + /** + * HTML escaping + * + * @param string $value Value to escape + * @return string + */ + public function e($value) + { + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false); + } + + /** + * Add a Javascript asset + * + * @param string $filename Filename + * @return string + */ + public function js($filename) + { + return ''; + } + + /** + * Add a stylesheet asset + * + * @param string $filename Filename + * @return string + */ + public function css($filename) + { + return ''; + } + + /** + * Display the form error class + * + * @param array $errors Error list + * @param string $name Field name + * @return string + */ + public function errorClass(array $errors, $name) + { + return ! isset($errors[$name]) ? '' : ' form-error'; + } + + /** + * Display a list of form errors + * + * @param array $errors List of errors + * @param string $name Field name + * @return string + */ + public function errorList(array $errors, $name) + { + $html = ''; + + if (isset($errors[$name])) { + + $html .= ''; + } + + return $html; + } + + /** + * Get an escaped form value + * + * @param mixed $values Values + * @param string $name Field name + * @return string + */ + public function formValue($values, $name) + { + if (isset($values->$name)) { + return 'value="'.$this->e($values->$name).'"'; + } + + return isset($values[$name]) ? 'value="'.$this->e($values[$name]).'"' : ''; + } + + /** + * Hidden CSRF token field + * + * @return string + */ + public function formCsrf() + { + return ''; + } + + /** + * Display a hidden form field + * + * @param string $name Field name + * @param array $values Form values + * @return string + */ + public function formHidden($name, array $values = array()) + { + return 'formValue($values, $name).'/>'; + } + + /** + * Display a select field + * + * @param string $name Field name + * @param array $options Options + * @param array $values Form values + * @param array $errors Form errors + * @param string $class CSS class + * @return string + */ + public function formSelect($name, array $options, array $values = array(), array $errors = array(), $class = '') + { + $html = ''; + $html .= $this->errorList($errors, $name); + + return $html; + } + + /** + * Display a radio field group + * + * @param string $name Field name + * @param array $options Options + * @param array $values Form values + * @return string + */ + public function formRadios($name, array $options, array $values = array()) + { + $html = ''; + + foreach ($options as $value => $label) { + $html .= $this->formRadio($name, $label, $value, isset($values[$name]) && $values[$name] == $value); + } + + return $html; + } + + /** + * Display a radio field + * + * @param string $name Field name + * @param string $label Form label + * @param string $value Form value + * @param boolean $selected Field selected or not + * @param string $class CSS class + * @return string + */ + public function formRadio($name, $label, $value, $selected = false, $class = '') + { + return ''; + } + + /** + * Display a checkbox field + * + * @param string $name Field name + * @param string $label Form label + * @param string $value Form value + * @param boolean $checked Field selected or not + * @param string $class CSS class + * @return string + */ + public function formCheckbox($name, $label, $value, $checked = false, $class = '') + { + return ''; + } + + /** + * Display a form label + * + * @param string $name Field name + * @param string $label Form label + * @param array $attributes HTML attributes + * @return string + */ + public function formLabel($label, $name, array $attributes = array()) + { + return ''; + } + + /** + * Display a textarea + * + * @param string $name Field name + * @param array $values Form values + * @param array $errors Form errors + * @param array $attributes HTML attributes + * @param string $class CSS class + * @return string + */ + public function formTextarea($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') + { + $class .= $this->errorClass($errors, $name); + + $html = ''; + $html .= $this->errorList($errors, $name); + + return $html; + } + + /** + * Display a input field + * + * @param string $type HMTL input tag type + * @param string $name Field name + * @param array $values Form values + * @param array $errors Form errors + * @param array $attributes HTML attributes + * @param string $class CSS class + * @return string + */ + public function formInput($type, $name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') + { + $class .= $this->errorClass($errors, $name); + + $html = 'formValue($values, $name).' class="'.$class.'" '; + $html .= implode(' ', $attributes).'/>'; + if (in_array('required', $attributes)) $html .= '*'; + $html .= $this->errorList($errors, $name); + + return $html; + } + + /** + * Display a text field + * + * @param string $name Field name + * @param array $values Form values + * @param array $errors Form errors + * @param array $attributes HTML attributes + * @param string $class CSS class + * @return string + */ + public function formText($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') + { + return $this->formInput('text', $name, $values, $errors, $attributes, $class); + } + + /** + * Display a password field + * + * @param string $name Field name + * @param array $values Form values + * @param array $errors Form errors + * @param array $attributes HTML attributes + * @param string $class CSS class + * @return string + */ + public function formPassword($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') + { + return $this->formInput('password', $name, $values, $errors, $attributes, $class); + } + + /** + * Display an email field + * + * @param string $name Field name + * @param array $values Form values + * @param array $errors Form errors + * @param array $attributes HTML attributes + * @param string $class CSS class + * @return string + */ + public function formEmail($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') + { + return $this->formInput('email', $name, $values, $errors, $attributes, $class); + } + + /** + * Display a number field + * + * @param string $name Field name + * @param array $values Form values + * @param array $errors Form errors + * @param array $attributes HTML attributes + * @param string $class CSS class + * @return string + */ + public function formNumber($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') + { + return $this->formInput('number', $name, $values, $errors, $attributes, $class); + } + + /** + * Display a numeric field (allow decimal number) + * + * @param string $name Field name + * @param array $values Form values + * @param array $errors Form errors + * @param array $attributes HTML attributes + * @param string $class CSS class + * @return string + */ + public function formNumeric($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') + { + return $this->formInput('text', $name, $values, $errors, $attributes, $class.' form-numeric'); + } + + /** + * Link + * + * a('link', 'task', 'show', array('task_id' => $task_id)) + * + * @param string $label Link label + * @param string $controller Controller name + * @param string $action Action name + * @param array $params Url parameters + * @param boolean $csrf Add a CSRF token + * @param string $class CSS class attribute + * @param boolean $new_tab Open the link in a new tab + * @return string + */ + public function a($label, $controller, $action, array $params = array(), $csrf = false, $class = '', $title = '', $new_tab = false) + { + return ''.$label.''; + } + + /** + * URL query string + * + * u('task', 'show', array('task_id' => $task_id)) + * + * @param string $controller Controller name + * @param string $action Action name + * @param array $params Url parameters + * @param boolean $csrf Add a CSRF token + * @return string + */ + public function u($controller, $action, array $params = array(), $csrf = false) + { + $html = '?controller='.$controller.'&action='.$action; + + if ($csrf) { + $params['csrf_token'] = Security::getCSRFToken(); + } + + foreach ($params as $key => $value) { + $html .= '&'.$key.'='.$value; + } + + return $html; + } + + /** + * Pagination links + * + * @param array $pagination Pagination information + * @return string + */ + public function paginate(array $pagination) + { + extract($pagination); + + if ($pagination['offset'] === 0 && ($total - $pagination['offset']) <= $limit) { + return ''; + } + + $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')); + } + + /** + * Markdown transformation + * + * @param string $text Markdown content + * @param array $link Link parameters for replacement + * @return string + */ + 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; + } + + /** + * Get the current URL without the querystring + * + * @return string + */ + public function getCurrentBaseUrl() + { + $url = Request::isHTTPS() ? 'https://' : 'http://'; + $url .= $_SERVER['SERVER_NAME']; + $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT']; + $url .= dirname($_SERVER['PHP_SELF']) !== '/' ? dirname($_SERVER['PHP_SELF']).'/' : '/'; + + return $url; + } + + /** + * Dispplay the flash session message + * + * @param string $html HTML wrapper + * @return string + */ + public function flash($html) + { + return $this->flashMessage('flash_message', $html); + } + + /** + * Display the flash session error message + * + * @param string $html HTML wrapper + * @return string + */ + public function flashError($html) + { + return $this->flashMessage('flash_error_message', $html); + } + + /** + * Fetch and remove a flash session message + * + * @access private + * @param string $name Message name + * @param string $html HTML wrapper + * @return string + */ + private function flashMessage($name, $html) + { + $data = ''; + + if (isset($this->session[$name])) { + $data = sprintf($html, $this->e($this->session[$name])); + unset($this->session[$name]); + } + + return $data; + } + + /** + * Format a file size + * + * @param integer $size Size in bytes + * @param integer $precision Precision + * @return string + */ + public function formatBytes($size, $precision = 2) + { + $base = log($size) / log(1024); + $suffixes = array('', 'k', 'M', 'G', 'T'); + + return round(pow(1024, $base - floor($base)), $precision).$suffixes[(int)floor($base)]; + } + + /** + * Truncate a long text + * + * @param string $value Text + * @param integer $max_length Max Length + * @param string $end Text end + * @return string + */ + public function summary($value, $max_length = 85, $end = '[...]') + { + $length = strlen($value); + + if ($length > $max_length) { + return substr($value, 0, $max_length).' '.$end; + } + + return $value; + } + + /** + * Return true if needle is contained in the haystack + * + * @param string $haystack Haystack + * @param string $needle Needle + * @return boolean + */ + public function contains($haystack, $needle) + { + return strpos($haystack, $needle) !== false; + } + + /** + * Return a value from a dictionary + * + * @param mixed $id Key + * @param array $listing Dictionary + * @param string $default_value Value displayed when the key doesn't exists + * @return string + */ + public function inList($id, array $listing, $default_value = '?') + { + if (isset($listing[$id])) { + return $this->e($listing[$id]); + } + + return $default_value; + } +} diff --git a/sources/app/Core/Listener.php b/sources/app/Core/Listener.php deleted file mode 100644 index 9c96cd5..0000000 --- a/sources/app/Core/Listener.php +++ /dev/null @@ -1,21 +0,0 @@ -storage[$key] = $value; + } + + public function get($key) + { + return isset($this->storage[$key]) ? $this->storage[$key] : null; + } + + public function flush() + { + $this->storage = array(); + } + + public function remove($key) + { + unset($this->storage[$key]); + } +} diff --git a/sources/app/Core/Session.php b/sources/app/Core/Session.php index 3305eca..0e5f742 100644 --- a/sources/app/Core/Session.php +++ b/sources/app/Core/Session.php @@ -2,13 +2,15 @@ namespace Core; +use ArrayAccess; + /** * Session class * * @package core * @author Frederic Guillot */ -class Session +class Session implements ArrayAccess { /** * Sesion lifetime @@ -59,7 +61,7 @@ class Session ini_set('session.entropy_length', '32'); ini_set('session.hash_bits_per_character', 6); - // If session was autostarted with session.auto_start = 1 in php.ini destroy it + // If the session was autostarted with session.auto_start = 1 in php.ini destroy it if (isset($_SESSION)) { session_destroy(); } @@ -88,19 +90,17 @@ class Session $_SESSION = array(); // Destroy the session cookie - if (ini_get('session.use_cookies')) { - $params = session_get_cookie_params(); + $params = session_get_cookie_params(); - setcookie( - session_name(), - '', - time() - 42000, - $params['path'], - $params['domain'], - $params['secure'], - $params['httponly'] - ); - } + setcookie( + session_name(), + '', + time() - 42000, + $params['path'], + $params['domain'], + $params['secure'], + $params['httponly'] + ); // Destroy session data session_destroy(); @@ -127,4 +127,24 @@ class Session { $_SESSION['flash_error_message'] = $message; } + + public function offsetSet($offset, $value) + { + $_SESSION[$offset] = $value; + } + + public function offsetExists($offset) + { + return isset($_SESSION[$offset]); + } + + public function offsetUnset($offset) + { + unset($_SESSION[$offset]); + } + + public function offsetGet($offset) + { + return isset($_SESSION[$offset]) ? $_SESSION[$offset] : null; + } } diff --git a/sources/app/Core/Template.php b/sources/app/Core/Template.php index da61f57..9688c2a 100644 --- a/sources/app/Core/Template.php +++ b/sources/app/Core/Template.php @@ -10,7 +10,7 @@ use LogicException; * @package core * @author Frederic Guillot */ -class Template +class Template extends Helper { /** * Template path @@ -20,18 +20,18 @@ class Template const PATH = 'app/Template/'; /** - * Load a template + * Render a template * * Example: * - * $template->load('template_name', ['bla' => 'value']); + * $template->render('template_name', ['bla' => 'value']); * * @access public * @params string $__template_name Template name * @params array $__template_args Key/Value map of template variables * @return string */ - public function load($__template_name, array $__template_args = array()) + public function render($__template_name, array $__template_args = array()) { $__template_file = self::PATH.$__template_name.'.php'; @@ -57,9 +57,9 @@ class Template */ public function layout($template_name, array $template_args = array(), $layout_name = 'layout') { - return $this->load( + return $this->render( $layout_name, - $template_args + array('content_for_layout' => $this->load($template_name, $template_args)) + $template_args + array('content_for_layout' => $this->render($template_name, $template_args)) ); } } diff --git a/sources/app/Core/Tool.php b/sources/app/Core/Tool.php index c010d93..ade99ca 100644 --- a/sources/app/Core/Tool.php +++ b/sources/app/Core/Tool.php @@ -2,8 +2,6 @@ namespace Core; -use Pimple\Container; - /** * Tool class * @@ -33,23 +31,4 @@ class Tool fclose($fp); } } - - /** - * Load and register a model - * - * @static - * @access public - * @param Pimple\Container $container Container instance - * @param string $name Model name - * @return mixed - */ - public static function loadModel(Container $container, $name) - { - if (! isset($container[$name])) { - $class = '\Model\\'.ucfirst($name); - $container[$name] = new $class($container); - } - - return $container[$name]; - } } diff --git a/sources/app/Core/Translator.php b/sources/app/Core/Translator.php index 1954ee2..0f5a77c 100644 --- a/sources/app/Core/Translator.php +++ b/sources/app/Core/Translator.php @@ -181,5 +181,8 @@ class Translator if (file_exists($filename)) { self::$locales = require $filename; } + else { + self::$locales = array(); + } } } diff --git a/sources/app/Event/AuthEvent.php b/sources/app/Event/AuthEvent.php new file mode 100644 index 0000000..ec1bec9 --- /dev/null +++ b/sources/app/Event/AuthEvent.php @@ -0,0 +1,27 @@ +auth_name = $auth_name; + $this->user_id = $user_id; + } + + public function getUserId() + { + return $this->user_id; + } + + public function getAuthType() + { + return $this->auth_name; + } +} diff --git a/sources/app/Event/Base.php b/sources/app/Event/Base.php deleted file mode 100644 index 0217fa0..0000000 --- a/sources/app/Event/Base.php +++ /dev/null @@ -1,79 +0,0 @@ -container = $container; - } - - /** - * Return class information - * - * @access public - * @return string - */ - public function __toString() - { - return get_called_class(); - } - - /** - * Load automatically models - * - * @access public - * @param string $name Model name - * @return mixed - */ - public function __get($name) - { - return Tool::loadModel($this->container, $name); - } - - /** - * Get event namespace - * - * Event = task.close | Namespace = task - * - * @access public - * @return string - */ - public function getEventNamespace() - { - $event_name = $this->container['event']->getLastTriggeredEvent(); - return substr($event_name, 0, strpos($event_name, '.')); - } -} diff --git a/sources/app/Event/CommentEvent.php b/sources/app/Event/CommentEvent.php new file mode 100644 index 0000000..75a132d --- /dev/null +++ b/sources/app/Event/CommentEvent.php @@ -0,0 +1,7 @@ +container = $values; + } + + public function getAll() + { + return $this->container; + } + + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + $this->container[] = $value; + } else { + $this->container[$offset] = $value; + } + } + + public function offsetExists($offset) + { + return isset($this->container[$offset]); + } + + public function offsetUnset($offset) + { + unset($this->container[$offset]); + } + + public function offsetGet($offset) + { + return isset($this->container[$offset]) ? $this->container[$offset] : null; + } +} diff --git a/sources/app/Event/NotificationListener.php b/sources/app/Event/NotificationListener.php deleted file mode 100644 index 3c04932..0000000 --- a/sources/app/Event/NotificationListener.php +++ /dev/null @@ -1,83 +0,0 @@ -template = $template; - } - - /** - * Execute the action - * - * @access public - * @param array $data Event data dictionary - * @return bool True if the action was executed or false when not executed - */ - public function execute(array $data) - { - $values = $this->getTemplateData($data); - $users = $this->notification->getUsersList($values['task']['project_id']); - - if ($users) { - $this->notification->sendEmails($this->template, $users, $values); - return true; - } - - return false; - } - - /** - * Fetch data for the mail template - * - * @access public - * @param array $data Event data - * @return array - */ - public function getTemplateData(array $data) - { - $values = array(); - - switch ($this->getEventNamespace()) { - case 'task': - $values['task'] = $this->taskFinder->getDetails($data['task_id']); - break; - case 'subtask': - $values['subtask'] = $this->subtask->getById($data['id'], true); - $values['task'] = $this->taskFinder->getDetails($data['task_id']); - break; - case 'file': - $values['file'] = $data; - $values['task'] = $this->taskFinder->getDetails($data['task_id']); - break; - case 'comment': - $values['comment'] = $this->comment->getById($data['id']); - $values['task'] = $this->taskFinder->getDetails($values['comment']['task_id']); - break; - } - - return $values; - } -} diff --git a/sources/app/Event/ProjectActivityListener.php b/sources/app/Event/ProjectActivityListener.php deleted file mode 100644 index 75efe65..0000000 --- a/sources/app/Event/ProjectActivityListener.php +++ /dev/null @@ -1,61 +0,0 @@ -getValues($data); - - return $this->projectActivity->createEvent( - $values['task']['project_id'], - $values['task']['id'], - $this->acl->getUserId(), - $this->container['event']->getLastTriggeredEvent(), - $values - ); - } - - return false; - } - - /** - * Get event activity data - * - * @access private - * @param array $data Event data dictionary - * @return array - */ - private function getValues(array $data) - { - $values = array(); - $values['task'] = $this->taskFinder->getDetails($data['task_id']); - - switch ($this->getEventNamespace()) { - case 'subtask': - $values['subtask'] = $this->subTask->getById($data['id'], true); - break; - case 'comment': - $values['comment'] = $this->comment->getById($data['id']); - break; - } - - return $values; - } -} diff --git a/sources/app/Event/ProjectDailySummaryListener.php b/sources/app/Event/ProjectDailySummaryListener.php deleted file mode 100644 index cd593ab..0000000 --- a/sources/app/Event/ProjectDailySummaryListener.php +++ /dev/null @@ -1,28 +0,0 @@ -projectDailySummary->updateTotals($data['project_id'], date('Y-m-d')); - } - - return false; - } -} diff --git a/sources/app/Event/ProjectModificationDateListener.php b/sources/app/Event/ProjectModificationDateListener.php deleted file mode 100644 index abc176b..0000000 --- a/sources/app/Event/ProjectModificationDateListener.php +++ /dev/null @@ -1,30 +0,0 @@ -project->updateModificationDate($data['project_id']); - } - - return false; - } -} diff --git a/sources/app/Event/SubtaskEvent.php b/sources/app/Event/SubtaskEvent.php new file mode 100644 index 0000000..229db86 --- /dev/null +++ b/sources/app/Event/SubtaskEvent.php @@ -0,0 +1,7 @@ +url = $url; - } - - /** - * Execute the action - * - * @access public - * @param array $data Event data dictionary - * @return bool True if the action was executed or false when not executed - */ - public function execute(array $data) - { - $this->webhook->notify($this->url, $data); - return true; - } -} diff --git a/sources/app/Integration/Base.php b/sources/app/Integration/Base.php new file mode 100644 index 0000000..babf8c8 --- /dev/null +++ b/sources/app/Integration/Base.php @@ -0,0 +1,49 @@ +container = $container; + } + + /** + * Load automatically class from the container + * + * @access public + * @param string $name + * @return mixed + */ + public function __get($name) + { + return $this->container[$name]; + } +} diff --git a/sources/app/Model/GithubWebhook.php b/sources/app/Integration/GithubWebhook.php similarity index 83% rename from sources/app/Model/GithubWebhook.php rename to sources/app/Integration/GithubWebhook.php index 9c8bd36..fd0b49f 100644 --- a/sources/app/Model/GithubWebhook.php +++ b/sources/app/Integration/GithubWebhook.php @@ -1,11 +1,14 @@ event->trigger(self::EVENT_COMMIT, array('task_id' => $task_id) + $task); + if ($task['is_active'] == Task::STATUS_OPEN && $task['project_id'] == $this->project_id) { + $this->container['dispatcher']->dispatch( + self::EVENT_COMMIT, + new GenericEvent(array('task_id' => $task_id) + $task) + ); } } @@ -146,7 +152,11 @@ class GithubWebhook extends Base 'task_id' => $task['id'], ); - $this->event->trigger(self::EVENT_ISSUE_COMMENT, $event); + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_COMMENT, + new GenericEvent($event) + ); + return true; } @@ -169,7 +179,11 @@ class GithubWebhook extends Base 'description' => $issue['body']."\n\n[".t('Github Issue').']('.$issue['html_url'].')', ); - $this->event->trigger(self::EVENT_ISSUE_OPENED, $event); + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_OPENED, + new GenericEvent($event) + ); + return true; } @@ -191,7 +205,11 @@ class GithubWebhook extends Base 'reference' => $issue['number'], ); - $this->event->trigger(self::EVENT_ISSUE_CLOSED, $event); + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_CLOSED, + new GenericEvent($event) + ); + return true; } @@ -216,7 +234,11 @@ class GithubWebhook extends Base 'reference' => $issue['number'], ); - $this->event->trigger(self::EVENT_ISSUE_REOPENED, $event); + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_REOPENED, + new GenericEvent($event) + ); + return true; } @@ -244,7 +266,11 @@ class GithubWebhook extends Base 'reference' => $issue['number'], ); - $this->event->trigger(self::EVENT_ISSUE_ASSIGNEE_CHANGE, $event); + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_ASSIGNEE_CHANGE, + new GenericEvent($event) + ); + return true; } @@ -271,7 +297,11 @@ class GithubWebhook extends Base 'reference' => $issue['number'], ); - $this->event->trigger(self::EVENT_ISSUE_ASSIGNEE_CHANGE, $event); + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_ASSIGNEE_CHANGE, + new GenericEvent($event) + ); + return true; } @@ -299,7 +329,11 @@ class GithubWebhook extends Base 'label' => $label['name'], ); - $this->event->trigger(self::EVENT_ISSUE_LABEL_CHANGE, $event); + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_LABEL_CHANGE, + new GenericEvent($event) + ); + return true; } @@ -328,7 +362,11 @@ class GithubWebhook extends Base 'category_id' => 0, ); - $this->event->trigger(self::EVENT_ISSUE_LABEL_CHANGE, $event); + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_LABEL_CHANGE, + new GenericEvent($event) + ); + return true; } diff --git a/sources/app/Integration/GitlabWebhook.php b/sources/app/Integration/GitlabWebhook.php new file mode 100644 index 0000000..f5df32a --- /dev/null +++ b/sources/app/Integration/GitlabWebhook.php @@ -0,0 +1,213 @@ +project_id = $project_id; + } + + /** + * Parse events + * + * @access public + * @param array $payload Gitlab event + * @return boolean + */ + public function parsePayload(array $payload) + { + switch ($this->getType($payload)) { + case self::TYPE_PUSH: + return $this->handlePushEvent($payload); + case self::TYPE_ISSUE; + return $this->handleIssueEvent($payload); + } + + return false; + } + + /** + * Get event type + * + * @access public + * @param array $payload Gitlab event + * @return string + */ + public function getType(array $payload) + { + if (isset($payload['object_kind']) && $payload['object_kind'] === 'issue') { + return self::TYPE_ISSUE; + } + + if (isset($payload['commits'])) { + return self::TYPE_PUSH; + } + + return ''; + } + + /** + * Parse push event + * + * @access public + * @param array $payload Gitlab event + * @return boolean + */ + public function handlePushEvent(array $payload) + { + foreach ($payload['commits'] as $commit) { + $this->handleCommit($commit); + } + + return true; + } + + /** + * 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; + } + + /** + * Parse issue event + * + * @access public + * @param array $payload Gitlab event + * @return boolean + */ + public function handleIssueEvent(array $payload) + { + switch ($payload['object_attributes']['state']) { + case 'opened': + return $this->handleIssueOpened($payload['object_attributes']); + case 'closed': + return $this->handleIssueClosed($payload['object_attributes']); + } + + return false; + } + + /** + * Handle new issues + * + * @access public + * @param array $issue Issue data + * @return boolean + */ + public function handleIssueOpened(array $issue) + { + $event = array( + 'project_id' => $this->project_id, + 'reference' => $issue['id'], + 'title' => $issue['title'], + 'description' => $issue['description']."\n\n[".t('Gitlab Issue').']('.$issue['url'].')', + ); + + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_OPENED, + new GenericEvent($event) + ); + + return true; + } + + /** + * Handle issue closing + * + * @access public + * @param array $issue Issue data + * @return boolean + */ + public function handleIssueClosed(array $issue) + { + $task = $this->taskFinder->getByReference($issue['id']); + + if ($task) { + $event = array( + 'project_id' => $this->project_id, + 'task_id' => $task['id'], + 'reference' => $issue['id'], + ); + + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_CLOSED, + new GenericEvent($event) + ); + + return true; + } + + return false; + } +} diff --git a/sources/app/Locale/da_DK/translations.php b/sources/app/Locale/da_DK/translations.php index c4fcc1c..3287aa5 100644 --- a/sources/app/Locale/da_DK/translations.php +++ b/sources/app/Locale/da_DK/translations.php @@ -182,7 +182,7 @@ return array( 'Change assignee' => 'Ændre ansvarlig', 'Change assignee for the task "%s"' => 'Ændre ansvarlig for opgaven: "%s"', 'Timezone' => 'Tidszone', - 'Sorry, I didn\'t found this information in my database!' => 'Denne information kunne ikke findes i databasen!', + 'Sorry, I didn\'t find this information in my database!' => 'Denne information kunne ikke findes i databasen!', 'Page not found' => 'Siden er ikke fundet', 'Complexity' => 'Kompleksitet', 'limit' => 'Begrænsning', @@ -194,7 +194,7 @@ return array( 'Allow this user' => 'Tillad denne bruger', 'Only those users have access to this project:' => 'Kunne disse brugere har adgang til dette projekt:', 'Don\'t forget that administrators have access to everything.' => 'Glem ikke at administratorer har adgang til alt.', - 'revoke' => 'fjern', + 'Revoke' => 'Fjern', 'List of authorized users' => 'Liste over autoriserede brugere', 'User' => 'Bruger', 'Nobody have access to this project.' => 'Ingen har adgang til dette projekt.', @@ -213,6 +213,7 @@ return array( 'Invalid date' => 'Ugyldig dato', 'Must be done before %B %e, %Y' => 'Skal være fuldført inden %d.%m.%Y', '%B %e, %Y' => '%d.%m.%Y', + // '%b %e, %Y' => '', 'Automatic actions' => 'Automatiske handlinger', 'Your automatic action have been created successfully.' => 'Din automatiske handling er oprettet.', 'Unable to create your automatic action.' => 'Din automatiske handling kunne ikke oprettes.', @@ -468,18 +469,18 @@ return array( 'Unable to change the password.' => 'Adgangskoden kunne ikke ændres.', 'Change category for the task "%s"' => 'Skift kategori for opgaven "%s"', 'Change category' => 'Skift kategori', - '%s updated the task #%d' => '%s opdatert opgaven #%d', - '%s open the task #%d' => '%s åben opgaven #%d', - '%s moved the task #%d to the position #%d in the column "%s"' => '%s flyt opgaven #%d til positionen #%d i kolonnen "%s"', - '%s moved the task #%d to the column "%s"' => '%s flyttede opgaven #%d til kolonnen "%s"', - '%s created the task #%d' => '%s oprettede opgaven #%d', - '%s closed the task #%d' => '', - '%s created a subtask for the task #%d' => '%s oprettede en under-opgave for opgaven #%d', - '%s updated a subtask for the task #%d' => '%s opdaterede en under-opgave for opgaven #%d', + '%s updated the task %s' => '%s opdatert opgaven %s', + '%s opened the task %s' => '%s åben opgaven %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s flyt opgaven %s til positionen #%d i kolonnen "%s"', + '%s moved the task %s to the column "%s"' => '%s flyttede opgaven %s til kolonnen "%s"', + '%s created the task %s' => '%s oprettede opgaven %s', + '%s closed the task %s' => '', + '%s created a subtask for the task %s' => '%s oprettede en under-opgave for opgaven %s', + '%s updated a subtask for the task %s' => '%s opdaterede en under-opgave for opgaven %s', 'Assigned to %s with an estimate of %s/%sh' => 'Tildelt til %s med en estimering på %s/%sh', 'Not assigned, estimate of %sh' => 'Ikke tildelt, estimeret til %sh', - '%s updated a comment on the task #%d' => '%s opdateret en kommentar på opgaven #%d', - '%s commented the task #%d' => '%s har kommenteret opgaven #%d', + '%s updated a comment on the task %s' => '%s opdateret en kommentar på opgaven %s', + '%s commented the task %s' => '%s har kommenteret opgaven %s', '%s\'s activity' => '%s\'s aktvitet', 'No activity.' => 'Ingen aktivitet', 'RSS feed' => 'RSS feed', @@ -498,7 +499,7 @@ return array( 'Default columns for new projects (Comma-separated)' => 'Standard kolonne for nye projekter (kommasepareret)', '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 change the assignee of the task #%d to %s' => '%s skift 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)', @@ -555,8 +556,8 @@ return array( // 'Webhooks' => '', // 'API' => '', // 'Integration' => '', - // 'Github webhook' => '', - // 'Help on Github webhook' => '', + // 'Github webhooks' => '', + // 'Help on Github webhooks' => '', // 'Create a comment from an external provider' => '', // 'Github issue comment created' => '', // 'Configure' => '', @@ -602,4 +603,49 @@ return array( // '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)' => '', + // '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' => '', ); diff --git a/sources/app/Locale/de_DE/translations.php b/sources/app/Locale/de_DE/translations.php index 9881223..c85c97c 100644 --- a/sources/app/Locale/de_DE/translations.php +++ b/sources/app/Locale/de_DE/translations.php @@ -49,7 +49,7 @@ return array( 'No project' => 'Keine Projekte', 'Project' => 'Projekt', 'Status' => 'Status', - 'Tasks' => 'Aufgabe', + 'Tasks' => 'Aufgaben', 'Board' => 'Pinnwand', 'Actions' => 'Aktionen', 'Inactive' => 'Inaktiv', @@ -182,7 +182,7 @@ return array( 'Change assignee' => 'Zuständigkeit ändern', 'Change assignee for the task "%s"' => 'Zuständigkeit für diese Aufgabe ändern: "%s"', 'Timezone' => 'Zeitzone', - 'Sorry, I didn\'t found this information in my database!' => 'Diese Information wurde in der Datenbank nicht gefunden!', + 'Sorry, I didn\'t find this information in my database!' => 'Diese Information wurde in der Datenbank nicht gefunden!', 'Page not found' => 'Seite nicht gefunden', 'Complexity' => 'Komplexität', 'limit' => 'Limit', @@ -194,7 +194,7 @@ return array( 'Allow this user' => 'Diesen Benutzer autorisieren', 'Only those users have access to this project:' => 'Nur diese Benutzer haben Zugriff zum Projekt:', 'Don\'t forget that administrators have access to everything.' => 'Nicht vergessen: Administratoren haben überall Zugriff.', - 'revoke' => 'entfernen', + 'Revoke' => 'Entfernen', 'List of authorized users' => 'Liste der autorisierten Benutzer', 'User' => 'Benutzer', 'Nobody have access to this project.' => 'Niemand hat Zugriff auf dieses Projekt.', @@ -202,9 +202,9 @@ return array( 'Comments' => 'Kommentare', 'Post comment' => 'Kommentieren', 'Write your text in Markdown' => 'Schreibe deinen Text in Markdown-Syntax', - 'Leave a comment' => 'Kommentar eingeben...', + 'Leave a comment' => 'Kommentar eingeben', 'Comment is required' => 'Ein Kommentar wird benötigt', - 'Leave a description' => 'Beschreibung eingeben...', + 'Leave a description' => 'Beschreibung eingeben', 'Comment added successfully.' => 'Kommentar erfolgreich hinzugefügt.', 'Unable to create your comment.' => 'Hinzufügen eines Kommentars nicht möglich.', 'The description is required' => 'Eine Beschreibung wird benötigt', @@ -213,8 +213,9 @@ return array( 'Invalid date' => 'Ungültiges Datum', 'Must be done before %B %e, %Y' => 'Muss vor dem %d.%m.%Y erledigt werden', '%B %e, %Y' => '%d.%m.%Y', + // '%b %e, %Y' => '', 'Automatic actions' => 'Automatische Aktionen', - 'Your automatic action have been created successfully.' => 'Die Automatische Aktion wurde erfolgreich erstellt.', + 'Your automatic action have been created successfully.' => 'Die automatische Aktion wurde erfolgreich erstellt.', 'Unable to create your automatic action.' => 'Erstellen der automatischen Aktion nicht möglich.', 'Remove an action' => 'Aktion löschen', 'Unable to remove this action.' => 'Löschen der Aktion nicht möglich.', @@ -222,8 +223,8 @@ return array( 'Automatic actions for the project "%s"' => 'Automatische Aktionen für das Projekt "%s"', 'Defined actions' => 'Definierte Aktionen', 'Add an action' => 'Aktion hinzufügen', - 'Event name' => 'Ereignis', - 'Action name' => 'Aktion', + 'Event name' => 'Ereignisname', + 'Action name' => 'Aktionsname', 'Action parameters' => 'Aktionsparameter', 'Action' => 'Aktion', 'Event' => 'Ereignis', @@ -331,7 +332,7 @@ return array( 'Remove a file' => 'Datei löschen', 'Unable to remove this file.' => 'Löschen der Datei nicht möglich.', 'File removed successfully.' => 'Datei erfolgreich gelöscht.', - 'Attach a document' => 'Datei anhängen', + 'Attach a document' => 'Dokument anhängen', 'Do you really want to remove this file: "%s"?' => 'Soll diese Datei wirklich gelöscht werden: "%s"?', 'open' => 'öffnen', 'Attachments' => 'Anhänge', @@ -366,7 +367,7 @@ return array( 'Sub-task added successfully.' => 'Teilaufgabe erfolgreich angelegt.', 'Maximum size: ' => 'Maximalgröße: ', 'Unable to upload the file.' => 'Hochladen der Datei nicht möglich.', - 'Display another project' => 'Zu Projekt wechseln...', + 'Display another project' => 'Zu Projekt wechseln', 'Your GitHub account was successfully linked to your profile.' => 'GitHub Account erfolgreich mit dem Profil verbunden.', 'Unable to link your GitHub Account.' => 'Verbindung mit diesem GitHub Account nicht möglich.', 'GitHub authentication failed' => 'Zugriff mit GitHub fehlgeschlagen', @@ -420,7 +421,7 @@ return array( '[Kanboard] Notification' => '[Kanboard] Benachrichtigung', 'I want to receive notifications only for those projects:' => 'Ich möchte nur für diese Projekte Benachrichtigungen erhalten:', 'view the task on Kanboard' => 'diese Aufgabe auf dem Kanboard zeigen', - 'Public access' => 'Öffentlich', + 'Public access' => 'Öffentlicher Zugriff', 'Category management' => 'Kategorien verwalten', 'User management' => 'Benutzer verwalten', 'Active tasks' => 'Aktive Aufgaben', @@ -448,12 +449,12 @@ return array( 'Username:' => 'Benutzername', 'Name:' => 'Name', 'Email:' => 'E-Mail', - 'Default project:' => 'Standardprojekt', - 'Notifications:' => 'Benachrichtigungen', + 'Default project:' => 'Standardprojekt:', + 'Notifications:' => 'Benachrichtigungen:', 'Notifications' => 'Benachrichtigungen', 'Group:' => 'Gruppe', 'Regular user' => 'Standardbenutzer', - 'Account type:' => 'Accounttyp', + 'Account type:' => 'Accounttyp:', 'Edit profile' => 'Profil bearbeiten', 'Change password' => 'Passwort ändern', 'Password modification' => 'Passwortänderung', @@ -468,18 +469,18 @@ return array( 'Unable to change the password.' => 'Passwort konnte nicht geändert werden.', 'Change category for the task "%s"' => 'Kategorie der Aufgabe "%s" ändern', 'Change category' => 'Kategorie ändern', - '%s updated the task #%d' => '%s hat die Aufgabe #%d aktualisiert', - '%s open the task #%d' => '%s hat die Aufgabe #%d geöffnet', - '%s moved the task #%d to the position #%d in the column "%s"' => '%s hat die Aufgabe #%d auf die Position #%d in der Spalte "%s" verschoben', - '%s moved the task #%d to the column "%s"' => '%s hat die Aufgabe #%d in die Spalte "%s" verschoben', - '%s created the task #%d' => '%s hat die Aufgabe #%d angelegt', - '%s closed the task #%d' => '%s hat die Aufgabe #%d geschlossen', - '%s created a subtask for the task #%d' => '%s hat eine Teilaufgabe für die Aufgabe #%d angelegt', - '%s updated a subtask for the task #%d' => '%s hat eine Teilaufgabe der Aufgabe #%d verändert', + '%s updated the task %s' => '%s hat die Aufgabe %s aktualisiert', + '%s opened the task %s' => '%s hat die Aufgabe %s geöffnet', + '%s moved the task %s to the position #%d in the column "%s"' => '%s hat die Aufgabe %s auf die Position #%d in der Spalte "%s" verschoben', + '%s moved the task %s to the column "%s"' => '%s hat die Aufgabe %s in die Spalte "%s" verschoben', + '%s created the task %s' => '%s hat die Aufgabe %s angelegt', + '%s closed the task %s' => '%s hat die Aufgabe %s geschlossen', + '%s created a subtask for the task %s' => '%s hat eine Teilaufgabe für die Aufgabe %s angelegt', + '%s updated a subtask for the task %s' => '%s hat eine Teilaufgabe der Aufgabe %s verändert', 'Assigned to %s with an estimate of %s/%sh' => 'An %s zugewiesen mit einer Schätzung von %s/%s Stunden', 'Not assigned, estimate of %sh' => 'Nicht zugewiesen, Schätzung von %s Stunden', - '%s updated a comment on the task #%d' => '%s hat einen Kommentat der Aufgabe #%d aktualisiert', - '%s commented the task #%d' => '%s hat die Aufgabe #%d kommentiert', + '%s updated a comment on the task %s' => '%s hat einen Kommentat der Aufgabe %s aktualisiert', + '%s commented the task %s' => '%s hat die Aufgabe %s kommentiert', '%s\'s activity' => '%s\'s Aktivität', 'No activity.' => 'Keine Aktivität.', 'RSS feed' => 'RSS Feed', @@ -498,7 +499,7 @@ return array( 'Default columns for new projects (Comma-separated)' => 'Standardspalten für neue Projekte (komma-getrennt)', '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 change the assignee of the task #%d to %s' => '%s hat die Zustä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)', @@ -511,7 +512,7 @@ return array( 'Github issue assignee change' => 'Github Fehlerzuständigkeit geändert', 'Github issue label change' => 'Github Fehlerkennzeichnung verändert', 'Create a task from an external provider' => 'Eine Aufgabe durch einen externen Provider hinzufügen', - // 'Change the assignee based on an external username' => '', + 'Change the assignee based on an external username' => 'Zuordnung ändern basierend auf externem Benutzernamen', 'Change the category based on an external label' => 'Kategorie basierend auf einer externen Kennzeichnung ändern', 'Reference' => 'Referenz', 'Reference: %s' => 'Referenz: %s', @@ -544,8 +545,8 @@ return array( 'Time spent: %s hours' => 'Aufgewendete Zeit: %s Stunden', 'Started on %B %e, %Y' => 'Gestartet am %B %e %Y', 'Start date' => 'Startdatum', - 'Time estimated' => 'Geplante Zeit', - 'There is nothing assigned to you.' => 'Es ist nichts an Sie zugewiesen.', + 'Time estimated' => 'Geschätzte Zeit', + 'There is nothing assigned to you.' => 'Ihnen ist nichts zugewiesen.', 'My tasks' => 'Meine Aufgaben', 'Activity stream' => 'Letzte Aktivitäten', 'Dashboard' => 'Dashboard', @@ -555,11 +556,11 @@ return array( 'Webhooks' => 'Webhooks', 'API' => 'API', 'Integration' => 'Integration', - 'Github webhook' => 'Github Webhook', - 'Help on Github webhook' => 'Hilfe bei einem Github Webhook', + 'Github webhooks' => 'Github Webhook', + 'Help on Github webhooks' => 'Hilfe für Github Webhooks', 'Create a comment from an external provider' => 'Kommentar eines externen Providers hinzufügen', 'Github issue comment created' => 'Github Fehler Kommentar hinzugefügt', - 'Configure' => 'konfigurieren', + 'Configure' => 'Einstellungen', 'Project management' => 'Projektmanagement', 'My projects' => 'Meine Projekte', 'Columns' => 'Spalten', @@ -602,4 +603,49 @@ return array( 'Nothing to preview...' => 'Nichts in der Vorschau anzuzeigen ...', 'Preview' => 'Vorschau', 'Write' => 'Ändern', + 'Active swimlanes' => 'Aktive Swimlane', + 'Add a new swimlane' => 'Eine neue Swimlane hinzufügen', + 'Change default swimlane' => 'Standard Swimlane ändern', + 'Default swimlane' => 'Standard Swimlane', + 'Do you really want to remove this swimlane: "%s"?' => 'Diese Swimlane wirklich ändern: "%s"?', + 'Inactive swimlanes' => 'Inaktive Swimlane', + 'Set project manager' => 'zum Projektmanager machen', + 'Set project member' => 'zum Projektmitglied machen', + 'Remove a swimlane' => 'Swimlane entfernen', + 'Rename' => 'umbenennen', + 'Show default swimlane' => 'Standard Swimlane anzeigen', + 'Swimlane modification for the project "%s"' => 'Swimlane Änderung für das Projekt "% s"', + 'Swimlane not found.' => 'Swimlane nicht gefunden', + 'Swimlane removed successfully.' => 'Swimlane erfolgreich entfernt.', + 'Swimlanes' => 'Swimlanes', + 'Swimlane updated successfully.' => 'Swimlane erfolgreich geändert.', + 'The default swimlane have been updated successfully.' => 'Die standard Swimlane wurden erfolgreich aktualisiert. Die standard Swimlane wurden erfolgreich aktualisiert.', + 'Unable to create your swimlane.' => 'Es ist nicht möglich die Swimlane zu erstellen.', + 'Unable to remove this swimlane.' => 'Es ist nicht möglich die Swimlane zu entfernen.', + 'Unable to update this swimlane.' => 'Es ist nicht möglich die Swimöane zu ändern.', + 'Your swimlane have been created successfully.' => 'Die Swimlane wurde erfolgreich angelegt.', + 'Example: "Bug, Feature Request, Improvement"' => 'Beispiel: "Bug, Funktionswünsche, Verbesserung"', + 'Default categories for new projects (Comma-separated)' => 'Standard Kategorien für neue Projekte (Komma-getrennt)', + 'Gitlab commit received' => 'Gitlab commit erhalten', + 'Gitlab issue opened' => 'Gitlab Thema eröffnet', + 'Gitlab issue closed' => 'Gitlab Thema geschlossen', + 'Gitlab webhooks' => 'Gitlab Webhook', + 'Help on Gitlab webhooks' => 'Hilfe für Gitlab Webhooks', + 'Integrations' => 'Integration', + 'Integration with third-party services' => 'Integration von Fremdleistungen', + 'Role for this project' => 'Rolle für dieses Projekt', + 'Project manager' => 'Projektmanager', + 'Project member' => 'Projektmitglied', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Ein Projektmanager kann die Projekteinstellungen ändern und hat mehr Rechte als ein normaler Benutzer.', + 'Gitlab Issue' => 'Gitlab Thema', + 'Subtask Id' => 'Teilaufgaben Id', + 'Subtasks' => 'Teilaufgaben', + 'Subtasks Export' => 'Teilaufgaben Export', + 'Subtasks exportation for "%s"' => 'Teilaufgaben Export für "%s"', + 'Task Title' => 'Aufgaben Titel', + 'Untitled' => 'unbetitelt', + 'Application default' => 'Anwendungsstandard', + 'Language:' => 'Sprache:', + 'Timezone:' => 'Zeitzone:', + // 'Next' => '', ); diff --git a/sources/app/Locale/es_ES/translations.php b/sources/app/Locale/es_ES/translations.php index a261832..5dafde9 100644 --- a/sources/app/Locale/es_ES/translations.php +++ b/sources/app/Locale/es_ES/translations.php @@ -182,7 +182,7 @@ return array( 'Change assignee' => 'Cambiar la persona asignada', 'Change assignee for the task "%s"' => 'Cambiar la persona asignada por la tarea « %s »', 'Timezone' => 'Zona horaria', - 'Sorry, I didn\'t found this information in my database!' => 'Lo siento no he encontrado información en la base de datos!', + 'Sorry, I didn\'t find this information in my database!' => 'Lo siento no he encontrado información en la base de datos!', 'Page not found' => 'Página no encontrada', 'Complexity' => 'Complejidad', 'limit' => 'límite', @@ -194,7 +194,7 @@ return array( 'Allow this user' => 'Autorizar este usuario', 'Only those users have access to this project:' => 'Solo estos usuarios tienen acceso a este proyecto:', 'Don\'t forget that administrators have access to everything.' => 'No olvide que los administradores tienen acceso a todo.', - 'revoke' => 'revocar', + 'Revoke' => 'Revocar', 'List of authorized users' => 'Lista de los usuarios autorizados', 'User' => 'Usuario', // 'Nobody have access to this project.' => '', @@ -213,6 +213,7 @@ return array( 'Invalid date' => 'Fecha no válida', 'Must be done before %B %e, %Y' => 'Debe de estar hecho antes del %d/%m/%Y', '%B %e, %Y' => '%d/%m/%Y', + // '%b %e, %Y' => '', 'Automatic actions' => 'Acciones automatizadas', 'Your automatic action have been created successfully.' => 'La acción automatizada ha sido creada correctamente.', 'Unable to create your automatic action.' => 'No se puede crear esta acción automatizada.', @@ -452,7 +453,7 @@ return array( 'Notifications:' => 'Notificaciones:', // 'Notifications' => '', 'Group:' => 'Grupo:', - 'Regular user' => 'Usuario regular:', + 'Regular user' => 'Usuario regular', 'Account type:' => 'Tipo de Cuenta:', 'Edit profile' => 'Editar perfil', 'Change password' => 'Cambiar contraseña', @@ -468,18 +469,18 @@ return array( 'Unable to change the password.' => 'No pude cambiar la contraseña.', 'Change category for the task "%s"' => 'Cambiar la categoría de la tarea "%s"', 'Change category' => 'Cambiar categoría', - '%s updated the task #%d' => '%s actualizó la tarea #%d', - '%s open the task #%d' => '%s abrió la tarea #%d', - '%s moved the task #%d to the position #%d in the column "%s"' => '%s movió la tarea #%d a la posición #%d de la columna "%s"', - '%s moved the task #%d to the column "%s"' => '%s movió la tarea #%d a la columna "%s"', - '%s created the task #%d' => '%s creó la tarea #%d', - '%s closed the task #%d' => '%s cerró la tarea #%d', - '%s created a subtask for the task #%d' => '%s creó una subtarea para la tarea #%d', - '%s updated a subtask for the task #%d' => '%s actualizó una subtarea para la tarea #%d', + '%s updated the task %s' => '%s actualizó la tarea %s', + '%s opened the task %s' => '%s abrió la tarea %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s movió la tarea %s a la posición #%d de la columna "%s"', + '%s moved the task %s to the column "%s"' => '%s movió la tarea %s a la columna "%s"', + '%s created the task %s' => '%s creó la tarea %s', + '%s closed the task %s' => '%s cerró la tarea %s', + '%s created a subtask for the task %s' => '%s creó una subtarea para la tarea %s', + '%s updated a subtask for the task %s' => '%s actualizó una subtarea para la tarea %s', 'Assigned to %s with an estimate of %s/%sh' => 'Asignada a %s con una estimación de %s/%sh', 'Not assigned, estimate of %sh' => 'No asignada, se estima en %sh', - '%s updated a comment on the task #%d' => '%s actualizó un comentario de la tarea #%d', - '%s commented the task #%d' => '%s comentó la tarea #%d', + '%s updated a comment on the task %s' => '%s actualizó un comentario de la tarea %s', + '%s commented the task %s' => '%s comentó la tarea %s', '%s\'s activity' => 'Actividad de %s', 'No activity.' => 'Sin actividad', 'RSS feed' => 'Fichero RSS', @@ -498,7 +499,7 @@ return array( 'Default columns for new projects (Comma-separated)' => 'Columnas por defecto de los nuevos proyectos (Separadas mediante comas)', 'Task assignee change' => 'Cambiar persona asignada a la tarea', // '%s change the assignee of the task #%d to %s' => '', - // '%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)', @@ -555,8 +556,8 @@ return array( // 'Webhooks' => '', // 'API' => '', // 'Integration' => '', - // 'Github webhook' => '', - // 'Help on Github webhook' => '', + // 'Github webhooks' => '', + // 'Help on Github webhooks' => '', // 'Create a comment from an external provider' => '', // 'Github issue comment created' => '', // 'Configure' => '', @@ -602,4 +603,49 @@ return array( // '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)' => '', + // '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' => '', ); diff --git a/sources/app/Locale/fi_FI/translations.php b/sources/app/Locale/fi_FI/translations.php index c0bc4bb..c3f1fbd 100644 --- a/sources/app/Locale/fi_FI/translations.php +++ b/sources/app/Locale/fi_FI/translations.php @@ -182,19 +182,19 @@ return array( 'Change assignee' => 'Vaihda suorittajaa', 'Change assignee for the task "%s"' => 'Vaihda suorittajaa tehtävälle %s', 'Timezone' => 'Aikavyöhyke', - 'Sorry, I didn\'t found this information in my database!' => 'Anteeksi, en löytänyt tätä tietoa tietokannastani', + 'Sorry, I didn\'t find this information in my database!' => 'Anteeksi, en löytänyt tätä tietoa tietokannastani', 'Page not found' => 'Sivua ei löydy', 'Complexity' => 'Monimutkaisuus', 'limit' => 'raja', 'Task limit' => 'Tehtävien maksimimäärä', - // 'Task count' => '', + 'Task count' => 'Tehtävien määrä', 'This value must be greater than %d' => 'Arvon täytyy olla suurempi kuin %d', 'Edit project access list' => 'Muuta projektin käyttäjiä', 'Edit users access' => 'Muuta käyttäjien pääsyä', 'Allow this user' => 'Salli tämä projekti', 'Only those users have access to this project:' => 'Vain näillä käyttäjillä on pääsy projektiin:', 'Don\'t forget that administrators have access to everything.' => 'Muista että ylläpitäjät pääsevät kaikkialle.', - 'revoke' => 'poista', + 'Revoke' => 'Poista', 'List of authorized users' => 'Sallittujen käyttäjien lista', 'User' => 'Käyttäjät', // 'Nobody have access to this project.' => '', @@ -213,6 +213,7 @@ return array( 'Invalid date' => 'Virheellinen päiväys', 'Must be done before %B %e, %Y' => 'Täytyy suorittaa ennen %d.%m.%Y', '%B %e, %Y' => '%d.%m.%Y', + // '%b %e, %Y' => '', 'Automatic actions' => 'Automaattiset toiminnot', 'Your automatic action have been created successfully.' => 'Toiminto suoritettiin onnistuneesti.', 'Unable to create your automatic action.' => 'Automaattisen toiminnon luominen epäonnistui.', @@ -367,14 +368,14 @@ return array( 'Maximum size: ' => 'Maksimikoko: ', 'Unable to upload the file.' => 'Tiedoston lataus epäonnistui.', 'Display another project' => 'Näytä toinen projekti', - // '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' => '', + 'Your GitHub account was successfully linked to your profile.' => 'Github-tilisi on onnistuneesti liitetty profiiliisi', + 'Unable to link your GitHub Account.' => 'Github-tilin liittäminen epäonnistui', + 'GitHub authentication failed' => 'Github-todennus epäonnistui', + 'Your GitHub account is no longer linked to your profile.' => 'Github-tiliäsi ei ole enää liitetty profiiliisi.', + 'Unable to unlink your GitHub Account.' => 'Github-tilisi liitoksen poisto epäonnistui', + 'Login with my GitHub Account' => 'Kirjaudu sisään Github-tililläni', + 'Link my GitHub Account' => 'Liitä Github-tilini', + 'Unlink my GitHub Account' => 'Poista liitos Github-tiliini', 'Created by %s' => 'Luonut: %s', 'Last modified on %B %e, %Y at %k:%M %p' => 'Viimeksi muokattu %B %e, %Y kello %H:%M', 'Tasks Export' => 'Tehtävien vienti', @@ -388,24 +389,24 @@ return array( 'Completion date' => 'Valmistumispäivä', 'Webhook URL for task creation' => 'Webhook URL tehtävän luomiselle', 'Webhook URL for task modification' => 'Webhook URL tehtävän muokkaamiselle', - // '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:' => '', - // 'Status:' => '', - // 'Assignee:' => '', - // 'Time tracking:' => '', - // 'New sub-task' => '', - // 'New attachment added "%s"' => '', - // 'Comment updated' => '', - // 'New comment posted by %s' => '', + 'Clone' => 'Kahdenna', + 'Clone Project' => 'Kahdenna projekti', + 'Project cloned successfully.' => 'Projekti kahdennettu onnistuneesti', + 'Unable to clone this project.' => 'Projektin kahdennus epäonnistui', + 'Email notifications' => 'Sähköposti-ilmoitukset', + 'Enable email notifications' => 'Ota käyttöön sähköposti-ilmoitukset', + 'Task position:' => 'Tehtävän sijainti', + 'The task #%d have been opened.' => 'Tehtävä #%d on avattu', + 'The task #%d have been closed.' => 'Tehtävä #%d on suljettu', + 'Sub-task updated' => 'Alitehtävä päivitetty', + 'Title:' => 'Otsikko:', + 'Status:' => 'Tila:', + 'Assignee:' => 'Vastaanottaja:', + 'Time tracking:' => 'Ajan seuranta:', + 'New sub-task' => 'Uusi alitehtävä', + 'New attachment added "%s"' => 'Uusi liite lisätty "%s"', + '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)' => '', @@ -418,188 +419,233 @@ return array( // '[%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 #%d' => '', - // '%s open the task #%d' => '', - // '%s moved the task #%d to the position #%d in the column "%s"' => '', - // '%s moved the task #%d to the column "%s"' => '', - // '%s created the task #%d' => '', - // '%s closed the task #%d' => '', - // '%s created a subtask for the task #%d' => '', - // '%s updated a subtask for the task #%d' => '', - // 'Assigned to %s with an estimate of %s/%sh' => '', - // 'Not assigned, estimate of %sh' => '', - // '%s updated a comment on the task #%d' => '', - // '%s commented the task #%d' => '', - // '%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 change the assignee of the task #%d to %s' => '', + 'I want to receive notifications only for those projects:' => 'Haluan vastaanottaa ilmoituksia ainoastaan näistä projekteista:', + 'view the task on Kanboard' => 'katso tehtävää Kanboardissa', + 'Public access' => 'Julkinen käyttöoikeus', + 'Category management' => 'Kategorioiden hallinta', + 'User management' => 'Käyttäjähallinta', + 'Active tasks' => 'Aktiiviset tehtävät', + 'Disable public access' => 'Poista käytöstä julkinen käyttöoikeus', + 'Enable public access' => 'Ota käyttöön ', + 'Active projects' => 'Aktiiviset projektit', + 'Inactive projects' => 'Passiiviset projektit', + 'Public access disabled' => 'Julkinen käyttöoikeus ei ole käytössä', + 'Do you really want to disable this project: "%s"?' => 'Haluatko varmasti tehdä projektista "%s" passiivisen?', + 'Do you really want to duplicate this project: "%s"?' => 'Haluatko varmasti kahdentaa projektin "%s"?', + 'Do you really want to enable this project: "%s"?' => 'Haluatko varmasti aktivoida projektinen "%s"', + 'Project activation' => 'Projektin aktivointi', + 'Move the task to another project' => 'Siirrä tehtävä toiseen projektiin', + 'Move to another project' => 'Siirrä toiseen projektiin', + 'Do you really want to duplicate this task?' => 'Haluatko varmasti kahdentaa tämän tehtävän?', + 'Duplicate a task' => 'Kahdenna tehtävä', + 'External accounts' => 'Muut tilit', + 'Account type' => 'Tilin tyyppi', + 'Local' => 'Paikallinen', + 'Remote' => 'Etä', + 'Enabled' => 'Käytössä', + 'Disabled' => 'Pois käytöstä', + 'Google account linked' => 'Google-tili liitetty', + 'Github account linked' => 'Github-tili liitetty', + 'Username:' => 'Käyttäjänimi:', + 'Name:' => 'Nimi:', + 'Email:' => 'Sähköpostiosoite:', + 'Default project:' => 'Oletusprojekti:', + 'Notifications:' => 'Ilmoitukset:', + 'Notifications' => 'Ilmoitukset', + 'Group:' => 'Ryhmä:', + 'Regular user' => 'Peruskäyttäjä', + 'Account type:' => 'Tilin tyyppi:', + 'Edit profile' => 'Muokkaa profiilia', + 'Change password' => 'Vaihda salasana', + 'Password modification' => 'Salasanan vaihto', + 'External authentications' => 'Muut tunnistautumistavat', + 'Google Account' => 'Google-tili', + 'Github Account' => 'Github-tili', + 'Never connected.' => 'Ei koskaan liitetty.', + 'No account linked.' => 'Tiliä ei ole liitetty.', + 'Account linked.' => 'Tili on liitetty.', + 'No external authentication enabled.' => 'Muita tunnistautumistapoja ei ole otettu käyttöön.', + 'Password modified successfully.' => 'Salasana vaihdettu onnistuneesti.', + 'Unable to change the password.' => 'Salasanan vaihto epäonnistui.', + 'Change category for the task "%s"' => 'Vaihda tehtävän "%s" kategoria', + 'Change category' => 'Vaihda kategoria', + '%s updated the task %s' => '%s päivitti tehtävän %s', + '%s opened the task %s' => '%s avasi tehtävän %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s siirsi tehtävän %s %d. sarakkeessa "%s"', + '%s moved the task %s to the column "%s"' => '%s siirsi tehtävän %s sarakkeeseen "%s"', + '%s created the task %s' => '%s loi tehtävän %s', + '%s closed the task %s' => '%s sulki tehtävän %s', + '%s created a subtask for the task %s' => '%s loi alitehtävän tehtävälle %s', + '%s updated a subtask for the task %s' => '%s päivitti tehtävän %s alitehtävää', + 'Assigned to %s with an estimate of %s/%sh' => 'Annettu henkilölle %s arviolla %s/%sh', + 'Not assigned, estimate of %sh' => 'Ei annettu kenellekään, arvio %sh', + '%s updated a comment on the task %s' => '%s päivitti kommentia tehtävässä %s', + '%s commented the task %s' => '%s kommentoi tehtävää %s', + '%s\'s activity' => 'Henkilön %s toiminta', + 'No activity.' => 'Ei toimintaa.', + 'RSS feed' => 'RSS-syöte', + '%s updated a comment on the task #%d' => '%s päivitti kommenttia tehtävässä #%d', + '%s commented on the task #%d' => '%s kommentoi tehtävää #%d', + '%s updated a subtask for the task #%d' => '%s päivitti tehtävän #%d alitehtävää', + '%s created a subtask for the task #%d' => '%s loi alitehtävän tehtävälle #%d', + '%s updated the task #%d' => '%s päivitti tehtävää #%d', + '%s created the task #%d' => '%s loi tehtävän #%d', + '%s closed the task #%d' => '%s sulki tehtävän #%d', + '%s open the task #%d' => '%s avasi tehtävän #%d', + '%s moved the task #%d to the column "%s"' => '%s siirsi tehtävän #%d sarakkeeseen "%s"', + '%s moved the task #%d to the position %d in the column "%s"' => '%s siirsi tehtävän #%d %d. sarakkeessa %s', + 'Activity' => 'Toiminta', + 'Default values are "%s"' => 'Oletusarvot ovat "%s"', + 'Default columns for new projects (Comma-separated)' => 'Oletussarakkeet uusille projekteille', + '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)' => '', - // 'New password for the user "%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.' => '', + 'New password for the user "%s"' => 'Uusi salasana käyttäjälle "%s"', + 'Choose an event' => 'Valitse toiminta', + 'Github commit received' => 'Github-kommitti vastaanotettu', + 'Github issue opened' => 'Github-issue avattu', + 'Github issue closed' => 'Github-issue suljettu', + 'Github issue reopened' => 'Github-issue uudelleenavattu', + 'Github issue assignee change' => 'Github-issuen saajan vaihto', + 'Github issue label change' => 'Github-issuen labelin vaihto', + 'Create a task from an external provider' => 'Luo tehtävä ulkoiselta tarjoajalta', + 'Change the assignee based on an external username' => 'Vaihda tehtävän saajaa perustuen ulkoiseen käyttäjänimeen', + 'Change the category based on an external label' => 'Vaihda kategoriaa perustuen ulkoiseen labeliin', + 'Reference' => 'Viite', + 'Reference: %s' => 'Viite: %s', + 'Label' => 'Label', + 'Database' => 'Tietokanta', + 'About' => 'Tietoja', + 'Database driver:' => 'Tietokantaohjelmisto:', + 'Board settings' => 'Taulun asetukset', + 'URL and token' => 'URL ja token', + 'Webhook settings' => 'Webhookin asetukset', + 'URL for task creation:' => 'URL tehtävän luomiseksi:', + 'Reset token' => 'Vaihda token', + 'API endpoint:' => 'API päätepiste:', + 'Refresh interval for private board' => 'Päivitystiheys yksityisille tauluille', + 'Refresh interval for public board' => 'Päivitystiheys julkisille tauluille', + 'Task highlight period' => 'Tehtävän korostusaika', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Aika (sekunteina) kuinka kauan tehtävä voidaan katsoa äskettäin muokatuksi (0 poistaa toiminnon käytöstä, oletuksena 2 päivää)', + 'Frequency in second (60 seconds by default)' => 'Päivitystiheys sekunteina (60 sekuntia oletuksena)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Päivitystiheys sekunteina (0 poistaa toiminnon käytöstä, oletuksena 10 sekuntia)', + 'Application URL' => 'Sovelluksen URL', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Esimerkiksi: http://example.kanboard.net/ (käytetään sähköposti-ilmoituksissa)', + 'Token regenerated.' => 'Token uudelleenluotu.', + 'Date format' => 'Päiväyksen muoto', + 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO-muoto on aina hyväksytty, esimerkiksi %s ja %s', + 'New private project' => 'Uusi yksityinen projekti', + 'This project is private' => 'Tämä projekti on yksityinen', + 'Type here to create a new sub-task' => 'Kirjoita tähän luodaksesi uuden alitehtävän', + 'Add' => 'Lisää', + 'Estimated time: %s hours' => 'Arvioitu aika: %s tuntia', + 'Time spent: %s hours' => 'Aikaa kulunut: %s tuntia', + 'Started on %B %e, %Y' => 'Aloitettu %B %e, %Y', + 'Start date' => 'Aloituspäivä', + 'Time estimated' => 'Arvioitu aika', + 'There is nothing assigned to you.' => 'Ei tehtäviä, joihin sinut olisi merkitty tekijäksi.', + 'My tasks' => 'Minun tehtävät', + 'Activity stream' => 'Toiminta', + 'Dashboard' => 'Työpöytä', + 'Confirmation' => 'Vahvistus', + 'Allow everybody to access to this project' => 'Anna kaikille käyttöoikeus tähän projektiin', + 'Everybody have access to this project.' => 'Kaikilla on käyttöoikeus projektiin.', // 'Webhooks' => '', // 'API' => '', - // 'Integration' => '', - // 'Github webhook' => '', - // 'Help on Github webhook' => '', + 'Integration' => 'Integraatio', + // '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' => '', + 'Configure' => 'Konfiguroi', + 'Project management' => 'Projektin hallinta', + 'My projects' => 'Minun projektini', + 'Columns' => 'Sarakkeet', + 'Task' => 'Tehtävät', + 'Your are not member of any project.' => 'Et ole minkään projektin jäsen.', + 'Percentage' => 'Prosentti', + 'Number of tasks' => 'Tehtävien määrä', + 'Task distribution' => 'Tehtävien jakauma', + 'Reportings' => 'Raportoinnit', // 'Task repartition for "%s"' => '', - // 'Analytics' => '', - // 'Subtask' => '', - // 'My subtasks' => '', + 'Analytics' => 'Analytiikka', + 'Subtask' => 'Alitehtävä', + 'My subtasks' => 'Minun alitehtäväni', // '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' => '', + 'Clone this project' => 'Kahdenna projekti', + 'Column removed successfully.' => 'Sarake poistettu onnstuneesti.', + 'Edit Project' => 'Muokkaa projektia', + 'Github Issue' => 'Github-issue', + 'Not enough data to show the graph.' => 'Ei riittävästi dataa graafin näyttämiseksi.', + 'Previous' => 'Edellinen', + 'The id must be an integer' => 'ID:n on oltava kokonaisluku', + 'The project id must be an integer' => 'Projektin ID:n on oltava kokonaisluku', + 'The status must be an integer' => 'Tilan on oltava kokonaisluku', + 'The subtask id is required' => 'Alitehtävän ID vaaditaan', + 'The subtask id must be an integer' => 'Alitehtävän ID:ntulee olla kokonaisluku', + 'The task id is required' => 'Tehtävän ID vaaditaan', + 'The task id must be an integer' => 'Tehtävän ID on oltava kokonaisluku', + 'The user id must be an integer' => 'Käyttäjän ID on oltava kokonaisluku', + 'This value is required' => 'Tämä arvo on pakollinen', + 'This value must be numeric' => 'Tämän arvon tulee olla numeerinen', + 'Unable to create this task.' => 'Tehtävän luonti epäonnistui', + 'Cumulative flow diagram' => 'Kumulatiivinen vuokaavio', + 'Cumulative flow diagram for "%s"' => 'Kumulatiivinen vuokaavio kohteelle "%s"', + 'Daily project summary' => 'Päivittäinen yhteenveto', + 'Daily project summary export' => 'Päivittäisen yhteenvedon vienti', + 'Daily project summary export for "%s"' => 'Päivittäisen yhteenvedon vienti kohteeseen "%s"', + 'Exports' => 'Viennit', + 'This export contains the number of tasks per column grouped per day.' => 'Tämä tiedosto sisältää tehtäviä sarakkeisiin päiväkohtaisesti ryhmilteltyinä', + 'Nothing to preview...' => 'Ei esikatselua...', + 'Preview' => 'Ei esikatselua', + 'Write' => 'Kirjoita', + 'Active swimlanes' => 'Aktiiviset kaistat', + 'Add a new swimlane' => 'Lisää uusi kaista', + 'Change default swimlane' => 'Vaihda oletuskaistaa', + 'Default swimlane' => 'Oletuskaista', + 'Do you really want to remove this swimlane: "%s"?' => 'Haluatko varmasti poistaa tämän kaistan: "%s"?', + 'Inactive swimlanes' => 'Passiiviset kaistat', + // 'Set project manager' => '', + // 'Set project member' => '', + 'Remove a swimlane' => 'Poista kaista', + 'Rename' => 'Uudelleennimeä', + 'Show default swimlane' => 'Näytä oletuskaista', + 'Swimlane modification for the project "%s"' => 'Kaistamuutos projektille "%s"', + 'Swimlane not found.' => 'Kaistaa ei löydy', + 'Swimlane removed successfully.' => 'Kaista poistettu onnistuneesti.', + 'Swimlanes' => 'Kaistat', + 'Swimlane updated successfully.' => 'Kaista päivitetty onnistuneesti.', + 'The default swimlane have been updated successfully.' => 'Oletuskaista päivitetty onnistuneesti.', + 'Unable to create your swimlane.' => 'Kaistan luonti epäonnistui.', + 'Unable to remove this swimlane.' => 'Kaistan poisto epäonnistui.', + 'Unable to update this swimlane.' => 'Kaistan päivittäminen epäonnistui.', + 'Your swimlane have been created successfully.' => 'Kaista luotu onnistuneesti.', + 'Example: "Bug, Feature Request, Improvement"' => 'Esimerkiksi: "Bugit, Ominaisuuspyynnöt, Parannukset"', + 'Default categories for new projects (Comma-separated)' => 'Oletuskategoriat uusille projekteille (pilkuin eroteltu)', + // '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' => '', ); diff --git a/sources/app/Locale/fr_FR/translations.php b/sources/app/Locale/fr_FR/translations.php index 94ed70d..d067304 100644 --- a/sources/app/Locale/fr_FR/translations.php +++ b/sources/app/Locale/fr_FR/translations.php @@ -182,11 +182,11 @@ return array( 'Change assignee' => 'Changer la personne assignée', 'Change assignee for the task "%s"' => 'Changer la personne assignée pour la tâche « %s »', 'Timezone' => 'Fuseau horaire', - 'Sorry, I didn\'t found this information in my database!' => 'Désolé, je n\'ai pas trouvé cette information dans ma base de données !', + 'Sorry, I didn\'t find this information in my database!' => 'Désolé, je n\'ai pas trouvé cette information dans ma base de données !', 'Page not found' => 'Page introuvable', 'Complexity' => 'Complexité', 'limit' => 'limite', - 'Task limit' => 'Nombre maximum de tâches', + 'Task limit' => 'Tâches Max.', 'Task count' => 'Nombre de tâches', 'This value must be greater than %d' => 'Cette valeur doit être plus grande que %d', 'Edit project access list' => 'Modifier l\'accès au projet', @@ -194,7 +194,7 @@ return array( 'Allow this user' => 'Autoriser cet utilisateur', 'Only those users have access to this project:' => 'Seulement ces utilisateurs ont accès à ce projet :', 'Don\'t forget that administrators have access to everything.' => 'N\'oubliez pas que les administrateurs ont accès à tout.', - 'revoke' => 'révoquer', + 'Revoke' => 'Révoquer', 'List of authorized users' => 'Liste des utilisateurs autorisés', 'User' => 'Utilisateur', 'Nobody have access to this project.' => 'Personne n\'est autorisé à accéder au projet.', @@ -213,6 +213,7 @@ return array( 'Invalid date' => 'Date invalide', 'Must be done before %B %e, %Y' => 'Doit être fait avant le %d/%m/%Y', '%B %e, %Y' => '%d %B %Y', + '%b %e, %Y' => '%d/%m/%Y', 'Automatic actions' => 'Actions automatisées', 'Your automatic action have been created successfully.' => 'Votre action automatisée a été ajouté avec succès.', 'Unable to create your automatic action.' => 'Impossible de créer votre action automatisée.', @@ -468,18 +469,18 @@ return array( 'Unable to change the password.' => 'Impossible de changer le mot de passe.', 'Change category for the task "%s"' => 'Changer la catégorie pour la tâche « %s »', 'Change category' => 'Changer de catégorie', - '%s updated the task #%d' => '%s a mis à jour la tâche n°%d', - '%s open the task #%d' => '%s a ouvert la tâche n°%d', - '%s moved the task #%d to the position #%d in the column "%s"' => '%s a déplacé la tâche n°%d à la position n°%d dans la colonne « %s »', - '%s moved the task #%d to the column "%s"' => '%s a déplacé la tâche n°%d dans la colonne « %s »', - '%s created the task #%d' => '%s a créé la tâche n°%d', - '%s closed the task #%d' => '%s a fermé la tâche n°%d', - '%s created a subtask for the task #%d' => '%s a créé une sous-tâche pour la tâche n°%d', - '%s updated a subtask for the task #%d' => '%s a mis à jour une sous-tâche appartenant à la tâche n°%d', + '%s updated the task %s' => '%s a mis à jour la tâche %s', + '%s opened the task %s' => '%s a ouvert la tâche %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s a déplacé la tâche %s à la position n°%d dans la colonne « %s »', + '%s moved the task %s to the column "%s"' => '%s a déplacé la tâche %s dans la colonne « %s »', + '%s created the task %s' => '%s a créé la tâche %s', + '%s closed the task %s' => '%s a fermé la tâche %s', + '%s created a subtask for the task %s' => '%s a créé une sous-tâche pour la tâche %s', + '%s updated a subtask for the task %s' => '%s a mis à jour une sous-tâche appartenant à la tâche %s', 'Assigned to %s with an estimate of %s/%sh' => 'Assigné à %s avec un estimé de %s/%sh', 'Not assigned, estimate of %sh' => 'Personne assigné, estimé de %sh', - '%s updated a comment on the task #%d' => '%s a mis à jour un commentaire appartenant à la tâche n°%d', - '%s commented the task #%d' => '%s a ajouté un commentaire sur la tâche n°%d', + '%s updated a comment on the task %s' => '%s a mis à jour un commentaire appartenant à la tâche %s', + '%s commented the task %s' => '%s a ajouté un commentaire sur la tâche %s', '%s\'s activity' => 'Activité du projet %s', 'No activity.' => 'Aucune activité.', 'RSS feed' => 'Flux RSS', @@ -497,11 +498,11 @@ return array( 'Default values are "%s"' => 'Les valeurs par défaut sont « %s »', 'Default columns for new projects (Comma-separated)' => 'Colonnes par défaut pour les nouveaux projets (séparé par des virgules)', '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 #%d pour %s', - '%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][Column Change] %s (#%d)' => '[%s][Changement de colonne] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][Changement de position] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][Changement d\'assigné] %s (#%d)', + '%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)', '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', @@ -555,8 +556,8 @@ return array( 'Webhooks' => 'Webhooks', 'API' => 'API', 'Integration' => 'Intégration', - 'Github webhook' => 'Webhook Github', - 'Help on Github webhook' => 'Aide sur les webhooks Github', + 'Github webhooks' => 'Webhook Github', + 'Help on Github webhooks' => 'Aide sur les webhooks Github', 'Create a comment from an external provider' => 'Créer un commentaire depuis un fournisseur externe', 'Github issue comment created' => 'Commentaire créé sur un ticket Github', 'Configure' => 'Configurer', @@ -602,4 +603,50 @@ return array( 'Nothing to preview...' => 'Rien à prévisualiser...', 'Preview' => 'Prévisualiser', 'Write' => 'Écrire', + 'Active swimlanes' => 'Swimlanes actives', + 'Add a new swimlane' => 'Ajouter une nouvelle swimlane', + 'Change default swimlane' => 'Modifier la swimlane par défaut', + 'Default swimlane' => 'Swimlane par défaut', + 'Do you really want to remove this swimlane: "%s"?' => 'Voulez-vous vraiment supprimer cette swimlane : « %s » ?', + 'Inactive swimlanes' => 'Swimlanes inactives', + 'Set project manager' => 'Mettre chef de projet', + 'Set project member' => 'Mettre membre du projet', + 'Remove a swimlane' => 'Supprimer une swimlane', + 'Rename' => 'Renommer', + 'Show default swimlane' => 'Afficher la swimlane par défaut', + 'Swimlane modification for the project "%s"' => 'Modification d\'une swimlane pour le projet « %s »', + 'Swimlane not found.' => 'Cette swimlane est introuvable.', + 'Swimlane removed successfully.' => 'Swimlane supprimée avec succès.', + 'Swimlanes' => 'Swimlanes', + 'Swimlane updated successfully.' => 'Swimlane mise à jour avec succès.', + 'The default swimlane have been updated successfully.' => 'La swimlane par défaut a été mise à jour avec succès.', + 'Unable to create your swimlane.' => 'Impossible de créer votre swimlane.', + 'Unable to remove this swimlane.' => 'Impossible de supprimer cette swimlane.', + 'Unable to update this swimlane.' => 'Impossible de mettre à jour cette swimlane.', + 'Your swimlane have been created successfully.' => 'Votre swimlane a été créée avec succès.', + 'Example: "Bug, Feature Request, Improvement"' => 'Exemple: « Incident, Demande de fonctionnalité, Amélioration »', + 'Default categories for new projects (Comma-separated)' => 'Catégories par défaut pour les nouveaux projets (séparé par des virgules)', + 'Gitlab commit received' => '« Commit » reçu via Gitlab', + 'Gitlab issue opened' => 'Ouverture d\'un ticket sur Gitlab', + 'Gitlab issue closed' => 'Fermeture d\'un ticket sur Gitlab', + 'Gitlab webhooks' => 'Webhook Gitlab', + 'Help on Gitlab webhooks' => 'Aide sur les webhooks Gitlab', + 'Integrations' => 'Intégrations', + 'Integration with third-party services' => 'Intégration avec des services externes', + 'Role for this project' => 'Rôle pour ce projet', + 'Project manager' => 'Chef de projet', + 'Project member' => 'Membre du projet', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Un chef de projet peut changer les paramètres du projet et possède plus de privilèges qu\'un utilisateur standard.', + 'Gitlab Issue' => 'Ticket Gitlab', + 'Subtask Id' => 'Identifiant de la sous-tâche', + 'Subtasks' => 'Sous-tâches', + 'Subtasks Export' => 'Exportation des sous-tâches', + 'Subtasks exportation for "%s"' => 'Exportation des sous-tâches pour le projet « %s »', + 'Task Title' => 'Titre de la tâche', + 'Untitled' => 'Sans nom', + 'Application default' => 'Valeur par défaut de l\'application', + 'Language:' => 'Langue :', + 'Timezone:' => 'Fuseau horaire :', + 'Next' => 'Suivant', + '#%d' => 'n˚%d', ); diff --git a/sources/app/Locale/hu_HU/translations.php b/sources/app/Locale/hu_HU/translations.php new file mode 100644 index 0000000..b879b78 --- /dev/null +++ b/sources/app/Locale/hu_HU/translations.php @@ -0,0 +1,651 @@ + 'Semelyik', + 'edit' => 'szerkesztés', + 'Edit' => 'Szerkesztés', + 'remove' => 'eltávolít', + 'Remove' => 'Eltávolít', + 'Update' => 'Frissítés', + 'Yes' => 'Igen', + 'No' => 'Nincs', + 'cancel' => 'mégsem', + 'or' => 'vagy', + 'Yellow' => 'sárga', + 'Blue' => 'kék', + 'Green' => 'zöld', + 'Purple' => 'ibolya', + 'Red' => 'piros', + 'Orange' => 'narancs', + 'Grey' => 'szürke', + 'Save' => 'Mentés', + 'Login' => 'Bejelentkezés', + 'Official website:' => 'Hivatalos honlap:', + 'Unassigned' => 'Nincs felelős', + '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ó', + 'All users' => 'Minden felhasználó', + 'Username' => 'Felhasználónév', + 'Password' => 'Jelszó', + 'Default project' => 'Alapértelmezett projekt', + 'Administrator' => 'Rendszergazda', + 'Sign in' => 'Jelentkezzen be', + 'Users' => 'Felhasználók', + 'No user' => 'Nincs felhasználó', + 'Forbidden' => 'tiltott', + 'Access Forbidden' => 'Hozzáférés megtagadva', + 'Only administrators can access to this page.' => 'Csak a rendszergazdák férhetnek hozzá az oldalhoz.', + 'Edit user' => 'Felhasználó módosítása', + 'Logout' => 'Kilépés', + 'Bad username or password' => 'Rossz felhasználónév vagy jelszó', + 'users' => 'felhasználók', + 'projects' => 'projektek', + 'Edit project' => 'Projekt szerkesztése', + 'Name' => 'Név', + 'Activated' => 'Aktiválva', + 'Projects' => 'Projektek', + 'No project' => 'Nincs projekt', + 'Project' => 'Projekt', + 'Status' => 'Állapot', + 'Tasks' => 'Feladat', + 'Board' => 'Tábla', + 'Actions' => 'Műveletek', + 'Inactive' => 'Inaktív', + '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 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', + '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"', + 'All projects' => 'Minden projekt', + 'Change columns' => 'Oszlop módosítása', + 'Add a new column' => 'Új oszlop', + 'Title' => 'Cím', + 'Add Column' => 'Oszlopot hozzáad', + 'Project "%s"' => 'Projekt "%s"', + 'Nobody assigned' => 'Nincs felelős', + 'Assigned to %s' => 'Felelős: %s', + 'Remove a column' => 'Oszlop törlése', + 'Remove a column from a board' => 'Oszlop törlése a tábláról', + 'Unable to remove this column.' => 'Az oszlop törlése nem lehetséges.', + 'Do you really want to remove this column: "%s"?' => 'Valóban törölni akarja ezt az oszlopot: "%s"?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Az oszlophoz rendelt ÖSSZES FELADAT TÖRLŐDNI FOG!', + 'Settings' => 'Beállítások', + 'Application settings' => 'Alkalmazás beállítások', + 'Language' => 'Nyelv', + 'Webhook token:' => 'Webhook token:', + 'API token:' => 'API token:', + 'More information' => 'További információ', + 'Database size:' => 'Adatbázis méret:', + 'Download the database' => 'Adatbázis letöltése', + 'Optimize the database' => 'Adatbázis optimalizálása', + '(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:', + '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', + 'Column' => 'Oszlop', + 'Color' => 'Szín', + 'Assignee' => 'Felelős', + 'Create another task' => 'Új feladat létrehozása', + 'New task' => 'új feladat', + 'Open a task' => 'Feladat megnyitása', + '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', + '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', + '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', + 'The maximum length is %d characters' => 'A maximális hossz %d karakter', + 'The minimum length is %d characters' => 'A minimális hossza %d karakter', + 'The password is required' => 'Jelszó szükséges', + 'This value must be an integer' => 'Ez az érték csak egész szám lehet', + 'The username must be unique' => 'A felhasználó nevének egyedinek kell lennie', + 'The username must be alphanumeric' => 'A felhasználói név csak alfanumerikus lehet (betűk és számok)', + 'The user id is required' => 'A felhasználói azonosítót meg kell adni', + 'Passwords don\'t match' => 'A jelszavak nem egyeznek', + 'The confirmation is required' => 'Megerősítés szükséges', + 'The column is required' => 'Az oszlopot meg kell adni', + 'The project is required' => 'A projektet meg kell adni', + 'The color is required' => 'A színt meg kell adni', + 'The id is required' => 'Az ID-t (azonosítót) meg kell adni', + 'The project id is required' => 'A projekt ID-t (azonosítót) meg kell adni', + 'The project name is required' => 'A projekt nevét meg kell adni', + 'This project must be unique' => 'A projekt nevének egyedinek kell lennie', + '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.', + '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.', + '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 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.', + 'Task opened successfully.' => 'Feladat sikeresen megnyitva .', + 'Unable to close this task.' => 'A feladat lezárása nem sikerült.', + 'Task closed successfully.' => 'Feladat sikeresen lezárva.', + 'Unable to update your task.' => 'A feladat frissítése nem sikerült.', + 'Task updated successfully.' => 'Feladat sikeresen frissítve.', + 'Unable to create your task.' => 'A feladat létrehozása nem sikerült.', + '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 updated successfully.' => 'Felhasználó sikeresen frissítve.', + 'Unable to update your user.' => 'Felhasználó frissítése nem sikerült.', + 'User removed successfully.' => 'Felhasználó sikeresen törölve.', + 'Unable to remove this user.' => 'Felhasználó törlése nem sikerült.', + 'Board updated successfully.' => 'Tábla sikeresen frissítve.', + 'Ready' => 'Kész', + 'Backlog' => 'Napló', + 'Work in progress' => 'Dolgozom', + '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 ..', + '%B %e, %Y at %k:%M %p' => '%Y.%m.%d %H:%M', + 'Date created' => 'Létrehozás időpontja', + 'Date completed' => 'Befejezés időpontja', + 'Id' => 'ID', + 'No task' => 'Nincs feladat', + 'Completed tasks' => 'Elvégzett feladatok', + 'List of projects' => 'Projektek listája', + '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', + 'There is no column in your project!' => 'Nincs oszlop a projektben!', + 'Change assignee' => 'Felelős módosítása', + 'Change assignee for the task "%s"' => 'Feladat felelősének módosítása: "%s"', + 'Timezone' => 'Időzóna', + 'Sorry, I didn\'t find this information in my database!' => 'Ez az információ nem található az adatbázisban!', + 'Page not found' => 'Az oldal nem található', + 'Complexity' => 'Bonyolultság', + 'limit' => 'határ', + 'Task limit' => 'Maximális számú feladat', + 'Task count' => 'Feladatok száma', + 'This value must be greater than %d' => 'Az értéknek nagyobbnak kell lennie, mint %d', + 'Edit project access list' => 'Projekt hozzáférés módosítása', + 'Edit users access' => 'Felhasználók hozzáférésének módosítása', + 'Allow this user' => 'Engedélyezi ezt a felhasználót', + 'Only those users have access to this project:' => 'Csak ezek a felhasználók férhetnek hozzá a projekthez:', + 'Don\'t forget that administrators have access to everything.' => 'Ne felejtsük el: a rendszergazdák mindenhez hozzáférnek.', + 'Revoke' => 'Visszavon', + 'List of authorized users' => 'Az engedélyezett felhasználók', + 'User' => 'Felhasználó', + 'Nobody have access to this project.' => 'Senkinek sincs hozzáférése a projekthez.', + 'You are not allowed to access to this project.' => 'Nincs hozzáférési joga a projekthez.', + 'Comments' => 'Hozzászólások', + 'Post comment' => 'Hozzászólás elküldése', + 'Write your text in Markdown' => 'Írja be a szöveget Markdown szintaxissal', + 'Leave a comment' => 'Írjon hozzászólást ...', + 'Comment is required' => 'A hozzászólás mező kötelező', + 'Leave a description' => 'Írjon leírást ...', + 'Comment added successfully.' => 'Hozzászólás sikeresen elküldve.', + 'Unable to create your comment.' => 'Hozzászólás létrehozása nem lehetséges.', + 'The description is required' => 'A leírás szükséges', + 'Edit this task' => 'Feladat módosítása', + 'Due Date' => 'Határidő', + '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' => '', + '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"', + 'Defined actions' => 'Intézkedések', + 'Add an action' => 'Intézkedés létrehozása', + 'Event name' => 'Esemény neve', + 'Action name' => 'Intézkedés neve', + 'Action parameters' => 'Intézkedés paraméterei', + 'Action' => 'Intézkedés', + 'Event' => 'Esemény', + 'When the selected event occurs execute the corresponding action.' => 'Ha a kiválasztott esemény bekövetkezik, hajtsa végre a megfelelő intézkedéseket.', + 'Next step' => 'Következő lépés', + 'Define action parameters' => 'Határozza meg az intézkedés paramétereit', + 'Save this action' => 'Intézkedés mentése', + 'Do you really want to remove this action: "%s"?' => 'Valóban törölni akarja ezt az intézkedést: "%s"?', + 'Remove an automatic action' => 'Automatikus intézkedés törlése', + 'Close the task' => 'Feladat lezárása', + 'Assign the task to a specific user' => 'Feladat kiosztása megadott felhasználónak', + 'Assign the task to the person who does the action' => 'Feladat kiosztása az intézkedő személynek', + 'Duplicate the task to another project' => 'Feladat másolása másik projektbe', + 'Move a task to another column' => 'Feladat mozgatása másik oszlopba', + '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', + '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', + 'link' => 'link', + 'Update this comment' => 'Hozzászólás frissítése', + 'Comment updated successfully.' => 'Megjegyzés sikeresen frissítve.', + 'Unable to update your comment.' => 'Megjegyzés frissítése nem sikerült.', + 'Remove a comment' => 'Megjegyzés törlése', + 'Comment removed successfully.' => 'Megjegyzés sikeresen törölve.', + 'Unable to remove this comment.' => 'Megjegyzés törölése nem lehetséges.', + '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"', + 'The current password is required' => 'A jelenlegi jelszót meg kell adni', + 'Wrong password' => 'Hibás jelszó', + 'Reset all tokens' => 'Reseteld az összes tokent', + 'All tokens have been regenerated.' => 'Minden token újra lett generálva.', + 'Unknown' => 'Ismeretlen', + 'Last logins' => 'Legutóbbi bejelentkezések', + 'Login date' => 'Bejelentkezés dátuma', + 'Authentication method' => 'Azonosítási módszer', + 'IP address' => 'IP-cím', + 'User agent' => 'User Agent', + 'Persistent connections' => 'Tartós (perzisztens) kapcsolatok', + 'No session.' => 'Nincs session.', + 'Expiration date' => 'Lejárati dátum', + 'Remember Me' => 'Emlékezz rám', + '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', + 'Open' => 'Nyitott', + 'Closed' => 'Lezárt', + 'Search' => 'Keres', + 'Nothing found.' => 'Semmit sem találtam.', + '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', + 'Description' => 'Leírás', + '%d comments' => '%d megjegyzés', + '%d comment' => '%d megjegyzés', + 'Email address invalid' => 'Érvénytelen e-mail cím', + 'Your Google Account is not linked anymore to your profile.' => 'Google Fiók már nincs a profilhoz kapcsolva.', + 'Unable to unlink your Google Account.' => 'Leválasztás a Google fiókról nem lehetséges.', + 'Google authentication failed' => 'Google azonosítás sikertelen', + 'Unable to link your Google Account.' => 'Google profilhoz kapcsolás nem sikerült.', + 'Your Google Account is linked to your profile successfully.' => 'Sikeresen összekapcsolva a Google fiókkal.', + 'Email' => 'E-mail', + 'Link my Google Account' => 'Kapcsold össze a Google fiókkal', + 'Unlink my Google Account' => 'Válaszd le a Google fiókomat', + '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.', + '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"?', + 'Assign automatically a color based on a category' => 'Szín hozzárendelése automatikusan kategória alapján', + 'Assign automatically a category based on a color' => 'Kategória hozzárendelése automatikusan szín alapján', + 'Task creation or modification' => 'Feladat létrehozása vagy módosítása', + 'Category' => 'Kategória', + '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.', + '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.', + 'Remove a category' => 'Kategória törlése', + 'Category removed successfully.' => 'Kategória törlése megtörtént.', + 'Unable to remove this category.' => 'A kategória törlése nem lehetséges.', + 'Category modification for the project "%s"' => 'Kategória módosítása a projektben "%s"', + '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', + '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.', + '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', + 'Attachments' => 'Mellékletek', + 'Edit the task' => 'Feladat módosítása', + 'Edit the description' => 'Leírás szerkesztése', + 'Add a comment' => 'Új megjegyzés', + 'Edit a comment' => 'Megjegyzés szerkesztése', + 'Summary' => 'Összegzés', + '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"?', + 'Remaining:' => 'Hátralévő:', + 'hours' => 'óra', + 'spent' => 'eltöltött', + 'estimated' => 'becsült', + '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', + 'Time spent' => 'Eltöltött idő', + 'Edit a sub-task' => 'Részfeladat szerkesztése', + 'Remove a sub-task' => 'Részfeladat törlése', + 'The time must be a numeric value' => 'Idő csak számérték lehet', + 'Todo' => 'Teendő', + 'In progress' => 'Folyamatban', + 'Sub-task removed successfully.' => 'Részfeladat sikeresen törölve.', + 'Unable to remove this sub-task.' => 'Részfeladat törlése nem lehetséges.', + '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:', + '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.', + 'Unable to link your GitHub Account.' => 'Nem lehet csatolni a GitHub fiókot.', + 'GitHub authentication failed' => 'GitHub azonosítás sikertelen', + 'Your GitHub account is no longer linked to your profile.' => 'GitHub fiók már nincs profilhoz kapcsolva.', + 'Unable to unlink your GitHub Account.' => 'GitHub fiók leválasztása nem lehetséges.', + 'Login with my GitHub Account' => 'Jelentkezzen be GitHub fiókkal', + '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', + 'Tasks Export' => 'Feladatok exportálása', + 'Tasks exportation for "%s"' => 'Feladatok exportálása "%s" részére', + 'Start Date' => 'Kezdés dátuma', + 'End Date' => 'Befejezés dátuma', + 'Execute' => 'Végrehajt', + 'Task Id' => 'Feladat ID', + 'Creator' => 'Készítette', + 'Modification date' => 'Módosítás dátuma', + 'Completion date' => 'Befejezés határideje', + '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.', + 'Email notifications' => 'E-mail értesítések', + 'Enable email notifications' => 'Engedélyezze az e-mail értesítéseket', + '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', + '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)', + '[%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:', + 'view the task on Kanboard' => 'feladat megtekintése a Kanboardon', + 'Public access' => 'Nyilvános hozzáférés', + 'Category management' => 'Kategóriák kezelése', + 'User management' => 'Felhasználók kezelése', + 'Active tasks' => 'Aktív feladatok', + 'Disable public access' => 'Nyilvános hozzáférés letiltása', + 'Enable public access' => 'Nyilvános hozzáférés engedélyezése', + 'Active projects' => 'Aktív projektek', + 'Inactive projects' => 'Inaktív projektek', + 'Public access disabled' => 'Nyilvános hozzáférés letiltva', + 'Do you really want to disable this project: "%s"?' => 'Tényleg szeretné letiltani ezt a projektet: "%s"', + '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', + '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', + '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', + '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:', + 'Edit profile' => 'Profil szerkesztése', + 'Change password' => 'Jelszó módosítása', + 'Password modification' => 'Jelszó módosítása', + 'External authentications' => 'Külső azonosítás', + 'Google Account' => 'Google fiók', + 'Github Account' => 'Github fiók', + '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.', + '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', + '%s opened the task %s' => '%s megnyitott a feladatot %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s átmozgatta a feladatot %s #%d pozícióba a "%s" oszlopban', + '%s moved the task %s to the column "%s"' => '%s átmozgatta a feladatot %s "%s" oszlopba', + '%s created the task %s' => '%s létrehozta a feladatot %s', + '%s closed the task %s' => '%s lezárta a feladatot %s', + '%s created a subtask for the task %s' => '%s létrehozott egy részfeladat a feladathoz %s', + '%s updated a subtask for the task %s' => '%s frissített egy részfeladatot a feladathoz %s', + 'Assigned to %s with an estimate of %s/%sh' => '%s-nek kiosztva %s/%s óra becsült idő mellett', + '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', + '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', + '%s commented on the task #%d' => '%s megjegyzést tett a feladathoz #%d', + '%s updated a subtask for the task #%d' => '%s frissített egy részfeladatot a feladatban #%d', + '%s created a subtask for the task #%d' => '%s létrehozott egy részfeladatot a feladatban #%d', + '%s updated the task #%d' => '%s frissítette a feladatot #%d', + '%s created the task #%d' => '%s létrehozta a feladatot #%d', + '%s closed the task #%d' => '%s lezárta a feladatot #%d', + '%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"', + '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"', + '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 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', + 'Create a task from an external provider' => 'Feladat létrehozása külsős számára', + 'Change the assignee based on an external username' => 'Felelős módosítása külső felhasználónév alapján', + 'Change the category based on an external label' => 'Kategória módosítása külső címke alapján', + 'Reference' => 'Hivatkozás', + 'Reference: %s' => 'Hivatkozás: %s', + 'Label' => 'Címke', + 'Database' => 'Adatbázis', + 'About' => 'Kanboard információ', + 'Database driver:' => 'Adatbázis driver:', + 'Board settings' => 'Tábla beállítások', + '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:', + '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)', + '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)', + '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', + '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', + 'Start date' => 'Kezdés dátuma', + 'Time estimated' => 'Becsült időtartam', + 'There is nothing assigned to you.' => 'Nincs kiosztott feladat.', + 'My tasks' => 'Feladataim', + 'Activity stream' => 'Legutóbbi tevékenységek', + 'Dashboard' => 'Műszerfal', + 'Confirmation' => 'Megerősítés', + 'Allow everybody to access to this project' => 'Engedélyezze a projekt elérését mindenkinek', + 'Everybody have access to this project.' => 'Mindenki elérheti a projektet', + 'Webhooks' => 'Webhook', + 'API' => 'API', + 'Integration' => 'Integráció', + 'Github webhooks' => 'Github webhooks', + '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', + 'Project management' => 'Projekt menedzsment', + 'My projects' => 'Projektjeim', + 'Columns' => 'Oszlopok', + 'Task' => 'Feladat', + 'Your are not member of any project.' => 'Ön nem tagja projektnek.', + 'Percentage' => 'Százalék', + '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', + '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', + 'Clone this project' => 'Projekt megkettőzése', + 'Column removed successfully.' => 'Oszlop sikeresen eltávolítva.', + 'Edit Project' => 'Projekt szerkesztése', + 'Github Issue' => 'Github issue', + 'Not enough data to show the graph.' => 'Nincs elég adat a grafikonhoz.', + 'Previous' => 'Előző', + 'The id must be an integer' => 'Az ID csak egész szám lehet', + 'The project id must be an integer' => 'A projekt ID csak egész szám lehet', + 'The status must be an integer' => 'Az állapot csak egész szám lehet', + 'The subtask id is required' => 'A részfeladat ID-t meg kell adni', + 'The subtask id must be an integer' => 'A részfeladat ID csak egész szám lehet', + 'The task id is required' => 'A feladat ID-t meg kell adni', + 'The task id must be an integer' => 'A feladat ID csak egész szám lehet', + 'The user id must be an integer' => 'A felhasználói ID csak egész szám lehet', + 'This value is required' => 'Ez a mező kötelező', + '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', + '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', + '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' => '', +); diff --git a/sources/app/Locale/it_IT/translations.php b/sources/app/Locale/it_IT/translations.php index b7132bc..0777e17 100644 --- a/sources/app/Locale/it_IT/translations.php +++ b/sources/app/Locale/it_IT/translations.php @@ -182,7 +182,7 @@ return array( 'Change assignee' => 'Cambiare la persona assegnata', 'Change assignee for the task "%s"' => 'Cambiare la persona assegnata per il compito « %s »', 'Timezone' => 'Fuso orario', - 'Sorry, I didn\'t found this information in my database!' => 'Mi dispiace, non ho trovato questa informazione sulla base dati!', + 'Sorry, I didn\'t find this information in my database!' => 'Mi dispiace, non ho trovato questa informazione sulla base dati!', 'Page not found' => 'Pagina non trovata', // 'Complexity' => '', 'limit' => 'limite', @@ -194,7 +194,7 @@ return array( 'Allow this user' => 'Permettere a questo utente', 'Only those users have access to this project:' => 'Solo questi utenti hanno accesso a questo progetto:', 'Don\'t forget that administrators have access to everything.' => 'Non dimenticare che gli amministratori hanno accesso a tutto.', - 'revoke' => 'revocare', + 'Revoke' => 'Revocare', 'List of authorized users' => 'Lista di utenti autorizzati', 'User' => 'Utente', // 'Nobody have access to this project.' => '', @@ -213,6 +213,7 @@ return array( 'Invalid date' => 'Data sbagliata', // 'Must be done before %B %e, %Y' => '', // '%B %e, %Y' => '', + // '%b %e, %Y' => '', 'Automatic actions' => 'Azioni automatiche', 'Your automatic action have been created successfully.' => 'l\'azione automatica è stata creata correttamente.', 'Unable to create your automatic action.' => 'Non si può creare quest\'azione automatica.', @@ -468,18 +469,18 @@ return array( // 'Unable to change the password.' => '', // 'Change category for the task "%s"' => '', // 'Change category' => '', - // '%s updated the task #%d' => '', - // '%s open the task #%d' => '', - // '%s moved the task #%d to the position #%d in the column "%s"' => '', - // '%s moved the task #%d to the column "%s"' => '', - // '%s created the task #%d' => '', - // '%s closed the task #%d' => '', - // '%s created a subtask for the task #%d' => '', - // '%s updated a subtask for the task #%d' => '', + // '%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 #%d' => '', - // '%s commented the task #%d' => '', + // '%s updated a comment on the task %s' => '', + // '%s commented the task %s' => '', // '%s\'s activity' => '', // 'No activity.' => '', // 'RSS feed' => '', @@ -498,7 +499,7 @@ return array( // 'Default columns for new projects (Comma-separated)' => '', // 'Task assignee change' => '', // '%s change the assignee of the task #%d to %s' => '', - // '%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)' => '', @@ -555,8 +556,8 @@ return array( // 'Webhooks' => '', // 'API' => '', // 'Integration' => '', - // 'Github webhook' => '', - // 'Help on Github webhook' => '', + // 'Github webhooks' => '', + // 'Help on Github webhooks' => '', // 'Create a comment from an external provider' => '', // 'Github issue comment created' => '', // 'Configure' => '', @@ -602,4 +603,49 @@ return array( // '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)' => '', + // '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' => '', ); diff --git a/sources/app/Locale/ja_JP/translations.php b/sources/app/Locale/ja_JP/translations.php index 7a59ec0..f4f5215 100644 --- a/sources/app/Locale/ja_JP/translations.php +++ b/sources/app/Locale/ja_JP/translations.php @@ -182,7 +182,7 @@ return array( 'Change assignee' => '担当を変更する', 'Change assignee for the task "%s"' => 'タスク「%s」の担当を変更する', 'Timezone' => 'タイムゾーン', - 'Sorry, I didn\'t found this information in my database!' => 'データベース上で情報が見つかりませんでした!', + 'Sorry, I didn\'t find this information in my database!' => 'データベース上で情報が見つかりませんでした!', 'Page not found' => 'ページが見つかりません', 'Complexity' => '複雑さ', 'limit' => '制限', @@ -194,7 +194,7 @@ return array( 'Allow this user' => 'このユーザを許可する', 'Only those users have access to this project:' => 'これらのユーザのみがプロジェクトにアクセスできます:', 'Don\'t forget that administrators have access to everything.' => '管理者には全ての権限が与えられます。', - 'revoke' => '許可を取り下げる', + 'Revoke' => '許可を取り下げる', 'List of authorized users' => '許可されたユーザ', 'User' => 'ユーザ', 'Nobody have access to this project.' => 'だれもプロジェクトにアクセスできません。', @@ -213,6 +213,7 @@ return array( 'Invalid date' => '日付が無効です', 'Must be done before %B %e, %Y' => '%Y/%m/%d までに完了', '%B %e, %Y' => '%d %B %Y', + // '%b %e, %Y' => '', 'Automatic actions' => '自動アクションを管理する', 'Your automatic action have been created successfully.' => '自動アクションを作成しました。', 'Unable to create your automatic action.' => '自動アクションの作成に失敗しました。', @@ -468,18 +469,18 @@ return array( 'Unable to change the password.' => 'パスワードが変更できませんでした。', 'Change category for the task "%s"' => 'タスク「%s」のカテゴリの変更', 'Change category' => 'カテゴリの変更', - '%s updated the task #%d' => '%s がタスク #%d をアップデートしました', - '%s open the task #%d' => '%s がタスク #%d をオープンしました', - '%s moved the task #%d to the position #%d in the column "%s"' => '%s がタスク #%d をポジション #%d カラム %s に移動しました', - '%s moved the task #%d to the column "%s"' => '%s がタスク #%d をカラム「%s」に移動しました', - '%s created the task #%d' => '%s がタスク #%d を作成しました', - '%s closed the task #%d' => '%s がタスク #%d をクローズしました', - '%s created a subtask for the task #%d' => '%s がタスク #%d のサブタスクを追加しました', - '%s updated a subtask for the task #%d' => '%s がタスク #%d のサブタスクを更新しました', + '%s updated the task %s' => '%s がタスク %s をアップデートしました', + '%s opened the task %s' => '%s がタスク %s をオープンしました', + '%s moved the task %s to the position #%d in the column "%s"' => '%s がタスク %s をポジション #%d カラム %s に移動しました', + '%s moved the task %s to the column "%s"' => '%s がタスク %s をカラム「%s」に移動しました', + '%s created the task %s' => '%s がタスク %s を作成しました', + '%s closed the task %s' => '%s がタスク %s をクローズしました', + '%s created a subtask for the task %s' => '%s がタスク %s のサブタスクを追加しました', + '%s updated a subtask for the task %s' => '%s がタスク %s のサブタスクを更新しました', 'Assigned to %s with an estimate of %s/%sh' => '担当者 %s に予想 %s/%sh に変更されました', 'Not assigned, estimate of %sh' => '担当者無しで予想 %sh に変更されました', - '%s updated a comment on the task #%d' => '%s がタスク #%d のコメントを更新しました', - '%s commented the task #%d' => '%s がタスク #%d にコメントしました', + '%s updated a comment on the task %s' => '%s がタスク %s のコメントを更新しました', + '%s commented the task %s' => '%s がタスク %s にコメントしました', '%s\'s activity' => '%s のアクティビティ', 'No activity.' => 'アクティビティなし。', 'RSS feed' => 'RSS フィード', @@ -498,7 +499,7 @@ return array( 'Default columns for new projects (Comma-separated)' => '新規プロジェクトのデフォルトカラム (コンマで区切って入力)', 'Task assignee change' => '担当者の変更', '%s change the assignee of the task #%d to %s' => '%s がタスク #%d の担当を %s に変更しました', - '%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)', @@ -555,8 +556,8 @@ return array( // 'Webhooks' => '', // 'API' => '', // 'Integration' => '', - // 'Github webhook' => '', - // 'Help on Github webhook' => '', + // 'Github webhooks' => '', + // 'Help on Github webhooks' => '', // 'Create a comment from an external provider' => '', // 'Github issue comment created' => '', // 'Configure' => '', @@ -602,4 +603,49 @@ return array( // '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)' => '', + // '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' => '', ); diff --git a/sources/app/Locale/pl_PL/translations.php b/sources/app/Locale/pl_PL/translations.php index edc696b..408828a 100644 --- a/sources/app/Locale/pl_PL/translations.php +++ b/sources/app/Locale/pl_PL/translations.php @@ -182,7 +182,7 @@ return array( 'Change assignee' => 'Zmień odpowiedzialną osobę', 'Change assignee for the task "%s"' => 'Zmień odpowiedzialną osobę dla zadania "%s"', 'Timezone' => 'Strefa czasowa', - 'Sorry, I didn\'t found this information in my database!' => 'Niestety nie znaleziono tej informacji w bazie danych', + 'Sorry, I didn\'t find this information in my database!' => 'Niestety nie znaleziono tej informacji w bazie danych', 'Page not found' => 'Strona nie istnieje', 'Complexity' => 'Poziom trudności', 'limit' => 'limit', @@ -194,7 +194,7 @@ return array( 'Allow this user' => 'Dodaj użytkownika', 'Only those users have access to this project:' => 'Użytkownicy mający dostęp:', 'Don\'t forget that administrators have access to everything.' => 'Pamiętaj: Administratorzy mają zawsze dostęp do wszystkiego!', - 'revoke' => 'odbierz dostęp', + '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.' => '', @@ -213,6 +213,7 @@ return array( 'Invalid date' => 'Błędna data', 'Must be done before %B %e, %Y' => 'Termin do %e %B %Y', '%B %e, %Y' => '%e %B %Y', + // '%b %e, %Y' => '', 'Automatic actions' => 'Akcje automatyczne', 'Your automatic action have been created successfully.' => 'Twoja akcja została dodana', 'Unable to create your automatic action.' => 'Nie udało się utworzyć akcji', @@ -468,18 +469,18 @@ return array( // 'Unable to change the password.' => '', // 'Change category for the task "%s"' => '', // 'Change category' => '', - // '%s updated the task #%d' => '', - // '%s open the task #%d' => '', - // '%s moved the task #%d to the position #%d in the column "%s"' => '', - // '%s moved the task #%d to the column "%s"' => '', - // '%s created the task #%d' => '', - // '%s closed the task #%d' => '', - // '%s created a subtask for the task #%d' => '', - // '%s updated a subtask for the task #%d' => '', + // '%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 #%d' => '', - // '%s commented the task #%d' => '', + // '%s updated a comment on the task %s' => '', + // '%s commented the task %s' => '', // '%s\'s activity' => '', // 'No activity.' => '', // 'RSS feed' => '', @@ -498,7 +499,7 @@ return array( // 'Default columns for new projects (Comma-separated)' => '', // 'Task assignee change' => '', // '%s change the assignee of the task #%d to %s' => '', - // '%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)' => '', @@ -555,8 +556,8 @@ return array( // 'Webhooks' => '', // 'API' => '', // 'Integration' => '', - // 'Github webhook' => '', - // 'Help on Github webhook' => '', + // 'Github webhooks' => '', + // 'Help on Github webhooks' => '', // 'Create a comment from an external provider' => '', // 'Github issue comment created' => '', // 'Configure' => '', @@ -602,4 +603,49 @@ return array( // '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)' => '', + // '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' => '', ); diff --git a/sources/app/Locale/pt_BR/translations.php b/sources/app/Locale/pt_BR/translations.php index 7525445..f155b04 100644 --- a/sources/app/Locale/pt_BR/translations.php +++ b/sources/app/Locale/pt_BR/translations.php @@ -4,8 +4,8 @@ return array( 'None' => 'Nenhum', 'edit' => 'editar', 'Edit' => 'Editar', - 'remove' => 'apagar', - 'Remove' => 'Apagar', + 'remove' => 'remover', + 'Remove' => 'Remover', 'Update' => 'Atualizar', 'Yes' => 'Sim', 'No' => 'Não', @@ -14,175 +14,175 @@ return array( 'Yellow' => 'Amarelo', 'Blue' => 'Azul', 'Green' => 'Verde', - 'Purple' => 'Violeta', + 'Purple' => 'Roxo', 'Red' => 'Vermelho', 'Orange' => 'Laranja', 'Grey' => 'Cinza', 'Save' => 'Salvar', 'Login' => 'Login', - 'Official website:' => 'Site web oficial :', + 'Official website:' => 'Site oficial:', 'Unassigned' => 'Não Atribuída', 'View this task' => 'Ver esta tarefa', 'Remove user' => 'Remover usuário', - 'Do you really want to remove this user: "%s"?' => 'Quer realmente remover este usuário: "%s"?', + 'Do you really want to remove this user: "%s"?' => 'Você realmente deseja remover este usuário: "%s"?', 'New user' => 'Novo usuário', 'All users' => 'Todos os usuários', - 'Username' => 'Nome do usuário', + 'Username' => 'Nome de usuário', 'Password' => 'Senha', - 'Default project' => 'Projeto default', + 'Default project' => 'Projeto padrão', 'Administrator' => 'Administrador', - 'Sign in' => 'Logar', + 'Sign in' => 'Entrar', 'Users' => 'Usuários', 'No user' => 'Sem usuário', 'Forbidden' => 'Proibido', 'Access Forbidden' => 'Acesso negado', 'Only administrators can access to this page.' => 'Somente administradores têm acesso a esta página.', 'Edit user' => 'Editar usuário', - 'Logout' => 'Logout', + 'Logout' => 'Sair', 'Bad username or password' => 'Usuário ou senha inválidos', 'users' => 'usuários', 'projects' => 'projetos', 'Edit project' => 'Editar projeto', 'Name' => 'Nome', - 'Activated' => 'Ativo', + 'Activated' => 'Ativado', 'Projects' => 'Projetos', 'No project' => 'Nenhum projeto', 'Project' => 'Projeto', 'Status' => 'Status', 'Tasks' => 'Tarefas', - 'Board' => 'Quadro', + 'Board' => 'Board', 'Actions' => 'Ações', 'Inactive' => 'Inativo', 'Active' => 'Ativo', 'Column %d' => 'Coluna %d', 'Add this column' => 'Adicionar esta coluna', - '%d tasks on the board' => '%d tarefas no quadro', + '%d tasks on the board' => '%d tarefas no board', '%d tasks in total' => '%d tarefas no total', - 'Unable to update this board.' => 'Impossível atualizar este quadro.', - 'Edit board' => 'Modificar quadro', + 'Unable to update this board.' => 'Não foi possível atualizar este board.', + 'Edit board' => 'Editar board', 'Disable' => 'Desativar', 'Enable' => 'Ativar', 'New project' => 'Novo projeto', - 'Do you really want to remove this project: "%s"?' => 'Quer realmente remover este projeto: "%s" ?', + 'Do you really want to remove this project: "%s"?' => 'Você realmente deseja remover este projeto: "%s" ?', 'Remove project' => 'Remover projeto', - 'Boards' => 'Quadros', - 'Edit the board for "%s"' => 'Editar o quadro para "%s"', + 'Boards' => 'Boards', + 'Edit the board for "%s"' => 'Editar o board para "%s"', 'All projects' => 'Todos os projetos', 'Change columns' => 'Modificar colunas', 'Add a new column' => 'Adicionar uma nova coluna', 'Title' => 'Título', - 'Add Column' => 'Adicionar coluna', + 'Add Column' => 'Adicionar Coluna', 'Project "%s"' => 'Projeto "%s"', 'Nobody assigned' => 'Ninguém designado', 'Assigned to %s' => 'Designado para %s', 'Remove a column' => 'Remover uma coluna', - 'Remove a column from a board' => 'Remover uma coluna do quadro', - 'Unable to remove this column.' => 'Impossível remover esta coluna.', - 'Do you really want to remove this column: "%s"?' => 'Quer realmente remover esta coluna: "%s"?', - 'This action will REMOVE ALL TASKS associated to this column!' => 'Esta ação vai REMOVER TODAS AS TAREFAS associadas a esta coluna!', - 'Settings' => 'Preferências', - 'Application settings' => 'Preferências da aplicação', + 'Remove a column from a board' => 'Remover uma coluna do board', + 'Unable to remove this column.' => 'Não foi possível remover esta coluna.', + 'Do you really want to remove this column: "%s"?' => 'Você realmente deseja remover esta coluna: "%s"?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Esta ação irá REMOVER TODAS AS TAREFAS associadas a esta coluna!', + 'Settings' => 'Configurações', + 'Application settings' => 'Configurações da aplicação', 'Language' => 'Idioma', 'Webhook token:' => 'Token de webhooks:', 'API token:' => 'API Token:', - 'More information' => 'Mais informação', + 'More information' => 'Mais informações', 'Database size:' => 'Tamanho do banco de dados:', 'Download the database' => 'Download do banco de dados', 'Optimize the database' => 'Otimizar o banco de dados', '(VACUUM command)' => '(Comando VACUUM)', '(Gzip compressed Sqlite file)' => '(Arquivo Sqlite comprimido com Gzip)', 'User settings' => 'Configurações do usuário', - 'My default project:' => 'Meu projeto default:', - 'Close a task' => 'Encerrar uma tarefa', - 'Do you really want to close this task: "%s"?' => 'Quer realmente encerrar esta tarefa: "%s"?', + 'My default project:' => 'Meu projeto padrão:', + 'Close a task' => 'Finalizar uma tarefa', + 'Do you really want to close this task: "%s"?' => 'Você realmente deseja finalizar esta tarefa: "%s"?', 'Edit a task' => 'Editar uma tarefa', 'Column' => 'Coluna', 'Color' => 'Cor', 'Assignee' => 'Designação', - 'Create another task' => 'Criar uma outra tarefa (aproveitando os dados preenchidos)', + 'Create another task' => 'Criar outra tarefa', 'New task' => 'Nova tarefa', 'Open a task' => 'Abrir uma tarefa', - 'Do you really want to open this task: "%s"?' => 'Quer realmente abrir esta tarefa: "%s"?', - 'Back to the board' => 'Voltar ao quadro', + 'Do you really want to open this task: "%s"?' => 'Você realmente deseja abrir esta tarefa: "%s"?', + 'Back to the board' => 'Voltar ao board', 'Created on %B %e, %Y at %k:%M %p' => 'Criado em %d %B %Y às %H:%M', 'There is nobody assigned' => 'Não há ninguém designado', - 'Column on the board:' => 'Coluna no quadro:', + 'Column on the board:' => 'Coluna no board:', 'Status is open' => 'Status está aberto', - 'Status is closed' => 'Status está encerrado', - 'Close this task' => 'Encerrar esta tarefa', + 'Status is closed' => 'Status está finalizado', + 'Close this task' => 'Finalizar esta tarefa', 'Open this task' => 'Abrir esta tarefa', 'There is no description.' => 'Não há descrição.', 'Add a new task' => 'Adicionar uma nova tarefa', 'The username is required' => 'O nome de usuário é obrigatório', - 'The maximum length is %d characters' => 'O tamanho máximo são %d caracteres', - 'The minimum length is %d characters' => 'O tamanho mínimo são %d caracteres', + 'The maximum length is %d characters' => 'O tamanho máximo é %d caracteres', + 'The minimum length is %d characters' => 'O tamanho mínimo é %d caracteres', 'The password is required' => 'A senha é obrigatória', - 'This value must be an integer' => 'O valor deve ser um inteiro', + 'This value must be an integer' => 'O valor deve ser um número inteiro', 'The username must be unique' => 'O nome de usuário deve ser único', - 'The username must be alphanumeric' => 'O nome de usuário deve ser alfanumérico, sem espaços ou _', - 'The user id is required' => 'O id de usuário é obrigatório', - 'Passwords don\'t match' => 'As senhas não conferem', + 'The username must be alphanumeric' => 'O nome de usuário deve ser alfanumérico', + 'The user id is required' => 'O ID de usuário é obrigatório', + 'Passwords don\'t match' => 'As senhas não coincidem', 'The confirmation is required' => 'A confirmação é obrigatória', 'The column is required' => 'A coluna é obrigatória', 'The project is required' => 'O projeto é obrigatório', 'The color is required' => 'A cor é obrigatória', - 'The id is required' => 'O id é obrigatório', - 'The project id is required' => 'O id do projeto é obrigatório', + 'The id is required' => 'O ID é obrigatório', + 'The project id is required' => 'O ID do projeto é obrigatório', 'The project name is required' => 'O nome do projeto é obrigatório', 'This project must be unique' => 'Este projeto deve ser único', 'The title is required' => 'O título é obrigatório', 'The language is required' => 'O idioma é obrigatório', 'There is no active project, the first step is to create a new project.' => 'Não há projeto ativo. O primeiro passo é criar um novo projeto.', 'Settings saved successfully.' => 'Configurações salvas com sucesso.', - 'Unable to save your settings.' => 'Impossível salvar suas configurações.', - 'Database optimization done.' => 'Otimização do banco de dados terminada.', + 'Unable to save your settings.' => 'Não é possível salvar suas configurações.', + 'Database optimization done.' => 'Otimização do banco de dados finalizada.', 'Your project have been created successfully.' => 'Seu projeto foi criado com sucesso.', - 'Unable to create your project.' => 'Impossível criar seu projeto.', + 'Unable to create your project.' => 'Não é possível criar o seu projeto.', 'Project updated successfully.' => 'Projeto atualizado com sucesso.', - 'Unable to update this project.' => 'Impossível atualizar este projeto.', - 'Unable to remove this project.' => 'Impossível remover este projeto.', + 'Unable to update this project.' => 'Não é possível atualizar este projeto.', + 'Unable to remove this project.' => 'Não é possível remover este projeto.', 'Project removed successfully.' => 'Projeto removido com sucesso.', 'Project activated successfully.' => 'Projeto ativado com sucesso.', - 'Unable to activate this project.' => 'Impossível ativar este projeto.', - 'Project disabled successfully.' => 'Projeto desabilitado com sucesso.', - 'Unable to disable this project.' => 'Impossível desabilitar este projeto.', - 'Unable to open this task.' => 'Impossível abrir esta tarefa.', + 'Unable to activate this project.' => 'Não é possível ativar este projeto.', + 'Project disabled successfully.' => 'Projeto desativado com sucesso.', + 'Unable to disable this project.' => 'Não é possível desativar este projeto.', + 'Unable to open this task.' => 'Não é possível abrir esta tarefa.', 'Task opened successfully.' => 'Tarefa aberta com sucesso.', - 'Unable to close this task.' => 'Impossível encerrar esta tarefa.', - 'Task closed successfully.' => 'Tarefa encerrada com sucesso.', - 'Unable to update your task.' => 'Impossível atualizar sua tarefa.', + 'Unable to close this task.' => 'Não é possível finalizar esta tarefa.', + 'Task closed successfully.' => 'Tarefa finalizada com sucesso.', + 'Unable to update your task.' => 'Não é possível atualizar a sua tarefa.', 'Task updated successfully.' => 'Tarefa atualizada com sucesso.', - 'Unable to create your task.' => 'Impossível criar sua tarefa.', + 'Unable to create your task.' => 'Não é possível criar a sua tarefa.', 'Task created successfully.' => 'Tarefa criada com sucesso.', 'User created successfully.' => 'Usuário criado com sucesso.', - 'Unable to create your user.' => 'Impossível criar seu usuário.', + 'Unable to create your user.' => 'Não é possível criar o seu usuário.', 'User updated successfully.' => 'Usuário atualizado com sucesso.', - 'Unable to update your user.' => 'Impossível atualizar seu usuário.', + 'Unable to update your user.' => 'Não é possível atualizar o seu usuário.', 'User removed successfully.' => 'Usuário removido com sucesso.', - 'Unable to remove this user.' => 'Impossível remover este usuário.', - 'Board updated successfully.' => 'Quadro atualizado com sucesso.', + 'Unable to remove this user.' => 'Não é possível remover este usuário.', + 'Board updated successfully.' => 'Board atualizado com sucesso.', 'Ready' => 'Pronto', 'Backlog' => 'Backlog', 'Work in progress' => 'Em andamento', - 'Done' => 'Encerrado', + 'Done' => 'Finalizado', 'Application version:' => 'Versão da aplicação:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Encerrado em %d %B %Y às %H:%M', + 'Completed on %B %e, %Y at %k:%M %p' => 'Finalizado em %d %B %Y às %H:%M', '%B %e, %Y at %k:%M %p' => '%d %B %Y às %H:%M', 'Date created' => 'Data de criação', - 'Date completed' => 'Data de encerramento', + 'Date completed' => 'Data da finalização', 'Id' => 'Id', 'No task' => 'Nenhuma tarefa', - 'Completed tasks' => 'tarefas completadas', + 'Completed tasks' => 'Tarefas completadas', 'List of projects' => 'Lista de projetos', 'Completed tasks for "%s"' => 'Tarefas completadas por "%s"', - '%d closed tasks' => '%d tarefas encerradas', - 'No task for this project' => 'Nenhuma tarefa para este projeto', + '%d closed tasks' => '%d tarefas finalizadas', + 'No task for this project' => 'Não há tarefa para este projeto', 'Public link' => 'Link público', 'There is no column in your project!' => 'Não há colunas no seu projeto!', 'Change assignee' => 'Mudar a designação', 'Change assignee for the task "%s"' => 'Modificar designação para a tarefa "%s"', 'Timezone' => 'Fuso horário', - 'Sorry, I didn\'t found this information in my database!' => 'Desculpe, não encontrei esta informação no meu banco de dados!', + 'Sorry, I didn\'t find this information in my database!' => 'Desculpe, não encontrei esta informação no meu banco de dados!', 'Page not found' => 'Página não encontrada', 'Complexity' => 'Complexidade', 'limit' => 'limite', @@ -191,10 +191,10 @@ return array( 'This value must be greater than %d' => 'Este valor deve ser maior que %d', 'Edit project access list' => 'Editar lista de acesso ao projeto', 'Edit users access' => 'Editar acesso de usuários', - 'Allow this user' => 'Permitir esse usuário', - 'Only those users have access to this project:' => 'Somente estes usuários têm acesso a este projeto:', + 'Allow this user' => 'Permitir este usuário', + 'Only those users have access to this project:' => 'Somente esses usuários têm acesso a este projeto:', 'Don\'t forget that administrators have access to everything.' => 'Não esqueça que administradores têm acesso a tudo.', - 'revoke' => 'revogar', + 'Revoke' => 'Revogar', 'List of authorized users' => 'Lista de usuários autorizados', 'User' => 'Usuário', 'Nobody have access to this project.' => 'Ninguém tem acesso a este projeto.', @@ -205,19 +205,20 @@ return array( 'Leave a comment' => 'Deixe um comentário', 'Comment is required' => 'Comentário é obrigatório', 'Leave a description' => 'Deixe uma descrição', - 'Comment added successfully.' => 'Cpmentário adicionado com sucesso.', - 'Unable to create your comment.' => 'Impossível criar seu comentário.', + 'Comment added successfully.' => 'Comentário adicionado com sucesso.', + 'Unable to create your comment.' => 'Não é possível criar o seu comentário.', 'The description is required' => 'A descrição é obrigatória', 'Edit this task' => 'Editar esta tarefa', 'Due Date' => 'Data de vencimento', 'Invalid date' => 'Data inválida', - 'Must be done before %B %e, %Y' => 'Deve ser feito antes de %d %B %Y', + 'Must be done before %B %e, %Y' => 'Deve ser finalizado antes de %d %B %Y', '%B %e, %Y' => '%d %B %Y', + '%b %e, %Y' => '%d %B %Y', 'Automatic actions' => 'Ações automáticas', 'Your automatic action have been created successfully.' => 'Sua ação automética foi criada com sucesso.', - 'Unable to create your automatic action.' => 'Impossível criar sua ação automática.', + 'Unable to create your automatic action.' => 'Não é possível criar sua ação automática.', 'Remove an action' => 'Remover uma ação', - 'Unable to remove this action.' => 'Impossível remover esta ação.', + 'Unable to remove this action.' => 'Não é possível remover esta ação.', 'Action removed successfully.' => 'Ação removida com sucesso.', 'Automatic actions for the project "%s"' => 'Ações automáticas para o projeto "%s"', 'Defined actions' => 'Ações definidas', @@ -227,22 +228,22 @@ return array( 'Action parameters' => 'Parâmetros da ação', 'Action' => 'Ação', 'Event' => 'Evento', - 'When the selected event occurs execute the corresponding action.' => 'Quando o evento selecionado ocorrer, execute a ação correspondente.', + 'When the selected event occurs execute the corresponding action.' => 'Quando o evento selecionado ocorrer execute a ação correspondente.', 'Next step' => 'Próximo passo', 'Define action parameters' => 'Definir parêmetros da ação', 'Save this action' => 'Salvar esta ação', - 'Do you really want to remove this action: "%s"?' => 'Você quer realmente remover esta ação: "%s"?', - 'Remove an automatic action' => 'Remove uma ação automática', - 'Close the task' => 'Encerrar tarefa', + 'Do you really want to remove this action: "%s"?' => 'Você realmente deseja remover esta ação: "%s"?', + 'Remove an automatic action' => 'Remover uma ação automática', + 'Close the task' => 'Finalizar tarefa', 'Assign the task to a specific user' => 'Designar a tarefa para um usuário específico', 'Assign the task to the person who does the action' => 'Designar a tarefa para a pessoa que executa a ação', 'Duplicate the task to another project' => 'Duplicar a tarefa para um outro projeto', 'Move a task to another column' => 'Mover a tarefa para outra coluna', - 'Move a task to another position in the same column' => 'Mover a tarefa para outra posição, na mesma coluna', + 'Move a task to another position in the same column' => 'Mover a tarefa para outra posição na mesma coluna', 'Task modification' => 'Modificação de tarefa', 'Task creation' => 'Criação de tarefa', - 'Open a closed task' => 'Reabrir uma tarefa encerrada', - 'Closing a task' => 'Encerrando uma tarefa', + 'Open a closed task' => 'Reabrir uma tarefa finalizada', + 'Closing a task' => 'Finalizando uma tarefa', 'Assign a color to a specific user' => 'Designar uma cor para um usuário específico', 'Column title' => 'Título da coluna', 'Position' => 'Posição', @@ -253,26 +254,26 @@ return array( 'link' => 'link', 'Update this comment' => 'Atualizar este comentário', 'Comment updated successfully.' => 'Comentário atualizado com sucesso.', - 'Unable to update your comment.' => 'Impossível atualizar seu comentário.', + 'Unable to update your comment.' => 'Não é possível atualizar o seu comentário.', 'Remove a comment' => 'Remover um comentário', 'Comment removed successfully.' => 'Comentário removido com sucesso.', - 'Unable to remove this comment.' => 'Impossível remover este comentário.', - 'Do you really want to remove this comment?' => 'Você tem certeza de que quer remover este comentário?', - 'Only administrators or the creator of the comment can access to this page.' => 'Somente administradores ou o criator deste comentário tem acesso a esta página.', + 'Unable to remove this comment.' => 'Não é possível remover este comentário.', + 'Do you really want to remove this comment?' => 'Você realmente deseja remover este comentário?', + 'Only administrators or the creator of the comment can access to this page.' => 'Somente os administradores ou o criator deste comentário possuem acesso a esta página.', 'Details' => 'Detalhes', 'Current password for the user "%s"' => 'Senha atual para o usuário "%s"', 'The current password is required' => 'A senha atual é obrigatória', - 'Wrong password' => 'Senha errada', - 'Reset all tokens' => 'Reiniciar todos os tokens', + 'Wrong password' => 'Senha incorreta', + 'Reset all tokens' => 'Resetar todos os tokens', 'All tokens have been regenerated.' => 'Todos os tokens foram gerados novamente.', 'Unknown' => 'Desconhecido', 'Last logins' => 'Últimos logins', 'Login date' => 'Data de login', 'Authentication method' => 'Método de autenticação', 'IP address' => 'Endereço IP', - 'User agent' => 'Agente usuário', + 'User agent' => 'User Agent', 'Persistent connections' => 'Conexões persistentes', - 'No session.' => 'Sem sessão.', + 'No session.' => 'Nenhuma sessão.', 'Expiration date' => 'Data de expiração', 'Remember Me' => 'Lembre-se de mim', 'Creation date' => 'Data de criação', @@ -280,25 +281,25 @@ return array( 'Filter by due date' => 'Filtrar por data de vencimento', 'Everybody' => 'Todos', 'Open' => 'Abrir', - 'Closed' => 'Encerrado', + 'Closed' => 'Finalizado', 'Search' => 'Pesquisar', - 'Nothing found.' => 'Não encontrado.', - 'Search in the project "%s"' => 'Procure no projeto "%s"', + 'Nothing found.' => 'Nada foi encontrado.', + 'Search in the project "%s"' => 'Pesquisar no projeto "%s"', 'Due date' => 'Data de vencimento', 'Others formats accepted: %s and %s' => 'Outros formatos permitidos: %s e %s', 'Description' => 'Descrição', '%d comments' => '%d comentários', '%d comment' => '%d comentário', 'Email address invalid' => 'Endereço de e-mail inválido', - 'Your Google Account is not linked anymore to your profile.' => 'Sua conta Google não está mais associada ao seu perfil.', - 'Unable to unlink your Google Account.' => 'Impossível desassociar sua conta Google.', + 'Your Google Account is not linked anymore to your profile.' => 'Sua conta do Google não está mais associada ao seu perfil.', + 'Unable to unlink your Google Account.' => 'Não foi possível desassociar a sua Conta do Google.', 'Google authentication failed' => 'Autenticação do Google falhou.', - 'Unable to link your Google Account.' => 'Impossível associar a sua conta do Google.', - 'Your Google Account is linked to your profile successfully.' => 'Sua Conta do Google está ligada ao seu perfil com sucesso.', + 'Unable to link your Google Account.' => 'Não foi possível associar a sua Conta do Google.', + 'Your Google Account is linked to your profile successfully.' => 'Sua Conta do Google foi associada ao seu perfil com sucesso.', 'Email' => 'E-mail', - 'Link my Google Account' => 'Vincular minha conta Google', - 'Unlink my Google Account' => 'Desvincular minha conta do Google', - 'Login with my Google Account' => 'Entrar com minha conta do Google', + 'Link my Google Account' => 'Vincular minha Conta do Google', + 'Unlink my Google Account' => 'Desvincular minha Conta do Google', + 'Login with my Google Account' => 'Entrar com minha Conta do Google', 'Project not found.' => 'Projeto não encontrado.', 'Task #%d' => 'Tarefa #%d', 'Task removed successfully.' => 'Tarefa removida com sucesso.', @@ -312,8 +313,8 @@ return array( 'Category:' => 'Categoria:', 'Categories' => 'Categorias', 'Category not found.' => 'Categoria não encontrada.', - 'Your category have been created successfully.' => 'Seu categoria foi criada com sucesso.', - 'Unable to create your category.' => 'Não é possível criar sua categoria.', + 'Your category have been created successfully.' => 'Sua categoria foi criada com sucesso.', + 'Unable to create your category.' => 'Não foi possível criar a sua categoria.', 'Your category have been updated successfully.' => 'A sua categoria foi atualizada com sucesso.', 'Unable to update your category.' => 'Não foi possível atualizar a sua categoria.', 'Remove a category' => 'Remover uma categoria', @@ -326,7 +327,7 @@ return array( 'Do you really want to remove this category: "%s"?' => 'Você realmente deseja remover esta categoria: "%s"', 'Filter by category' => 'Filtrar por categoria', 'All categories' => 'Todas as categorias', - 'No category' => 'Sem categoria', + 'No category' => 'Nenhum categoria', 'The name is required' => 'O nome é obrigatório', 'Remove a file' => 'Remover um arquivo', 'Unable to remove this file.' => 'Não foi possível remover este arquivo.', @@ -343,60 +344,60 @@ return array( 'Time tracking' => 'Rastreamento de tempo', 'Estimate:' => 'Estimado:', 'Spent:' => 'Gasto:', - 'Do you really want to remove this sub-task?' => 'Você realmente deseja remover esta sub-tarefa?', + 'Do you really want to remove this sub-task?' => 'Você realmente deseja remover esta subtarefa?', 'Remaining:' => 'Restante:', 'hours' => 'horas', 'spent' => 'gasto', - 'estimated' => 'estimada', - 'Sub-Tasks' => 'Sub-tarefas', - 'Add a sub-task' => 'Adicionar uma sub-tarefa', + 'estimated' => 'estimado', + 'Sub-Tasks' => 'Subtarefas', + 'Add a sub-task' => 'Adicionar uma subtarefa', 'Original estimate' => 'Estimativa original', - 'Create another sub-task' => 'Criar uma outra sub-tarefa', + 'Create another sub-task' => 'Criar uma outra subtarefa', 'Time spent' => 'Tempo gasto', - 'Edit a sub-task' => 'Editar uma sub-tarefa', - 'Remove a sub-task' => 'Remover uma sub-tarefa', + 'Edit a sub-task' => 'Editar uma subtarefa', + 'Remove a sub-task' => 'Remover uma subtarefa', 'The time must be a numeric value' => 'O tempo deve ser um valor numérico', - 'Todo' => 'A fazer', + 'Todo' => 'À fazer', 'In progress' => 'Em andamento', - 'Sub-task removed successfully.' => 'Sub-tarefa removido com sucesso.', - 'Unable to remove this sub-task.' => 'Não foi possível remover esta sub-tarefa.', - 'Sub-task updated successfully.' => 'Sub-tarefa atualizada com sucesso.', - 'Unable to update your sub-task.' => 'Não foi possível atualizar sua sub-tarefa.', - 'Unable to create your sub-task.' => 'Não é possível criar sua sub-tarefa.', - 'Sub-task added successfully.' => 'Sub-tarefa adicionada com sucesso.', - 'Maximum size: ' => 'O tamanho máximo:', + 'Sub-task removed successfully.' => 'Subtarefa removida com sucesso.', + 'Unable to remove this sub-task.' => 'Não foi possível remover esta subtarefa.', + 'Sub-task updated successfully.' => 'Subtarefa atualizada com sucesso.', + 'Unable to update your sub-task.' => 'Não foi possível atualizar a sua subtarefa.', + 'Unable to create your sub-task.' => 'Não é possível criar a sua subtarefa.', + 'Sub-task added successfully.' => 'Subtarefa adicionada com sucesso.', + 'Maximum size: ' => 'Tamanho máximo:', 'Unable to upload the file.' => 'Não foi possível carregar o arquivo.', - 'Display another project' => 'Mostrar um outro projeto', - 'Your GitHub account was successfully linked to your profile.' => 'A sua conta GitHub foi ligada com sucesso ao seu perfil.', - 'Unable to link your GitHub Account.' => 'Não foi possível vincular sua conta GitHub.', - 'GitHub authentication failed' => 'Falhou autenticação GitHub', - 'Your GitHub account is no longer linked to your profile.' => 'A sua conta GitHub já não está ligada ao seu perfil.', - 'Unable to unlink your GitHub Account.' => 'Não foi possível desvincular sua conta GitHub.', - 'Login with my GitHub Account' => 'Entrar com minha conta do GitHub', - 'Link my GitHub Account' => 'Vincular minha conta GitHub', - 'Unlink my GitHub Account' => 'Desvincular minha conta do GitHub', + 'Display another project' => 'Exibir outro projeto', + 'Your GitHub account was successfully linked to your profile.' => 'A sua Conta do GitHub foi associada com sucesso ao seu perfil.', + 'Unable to link your GitHub Account.' => 'Não foi possível associar sua Conta do GitHub.', + 'GitHub authentication failed' => 'Autenticação do GitHub falhou', + 'Your GitHub account is no longer linked to your profile.' => 'A sua Conta do GitHub não está mais associada ao seu perfil.', + 'Unable to unlink your GitHub Account.' => 'Não foi possível desassociar a sua Conta do GitHub.', + 'Login with my GitHub Account' => 'Entrar com minha Conta do GitHub', + 'Link my GitHub Account' => 'Associar à minha Conta do GitHub', + 'Unlink my GitHub Account' => 'Desassociar a minha Conta do GitHub', 'Created by %s' => 'Criado por %s', 'Last modified on %B %e, %Y at %k:%M %p' => 'Última modificação em %B %e, %Y às %k: %M %p', - 'Tasks Export' => 'Tarefas Export', - 'Tasks exportation for "%s"' => 'Tarefas exportação para "%s"', + 'Tasks Export' => 'Exportar Tarefas', + 'Tasks exportation for "%s"' => 'As tarefas foram exportadas para "%s"', 'Start Date' => 'Data inicial', 'End Date' => 'Data final', 'Execute' => 'Executar', - 'Task Id' => 'Id da Tarefa', - 'Creator' => 'Criador', - 'Modification date' => 'Data de modificação', - 'Completion date' => 'Data de conclusão', + 'Task Id' => 'ID da Tarefa', + 'Creator' => 'Criado por', + 'Modification date' => 'Data da modificação', + 'Completion date' => 'Data da finalização', 'Webhook URL for task creation' => 'Webhook URL para criação de tarefas', - 'Webhook URL for task modification' => 'Webhook URL para modificação tarefa', - 'Clone' => 'Clone', + 'Webhook URL for task modification' => 'Webhook URL para modificação de tarefa', + 'Clone' => 'Clonar', 'Clone Project' => 'Clonar Projeto', 'Project cloned successfully.' => 'Projeto clonado com sucesso.', - 'Unable to clone this project.' => 'Impossível clonar este projeto.', + 'Unable to clone this project.' => 'Não foi possível clonar este projeto.', 'Email notifications' => 'Notificações por email', 'Enable email notifications' => 'Habilitar notificações por email', 'Task position:' => 'Posição da tarefa:', 'The task #%d have been opened.' => 'A tarefa #%d foi aberta.', - 'The task #%d have been closed.' => 'A tarefa #%d foi encerrada.', + 'The task #%d have been closed.' => 'A tarefa #%d foi finalizada.', 'Sub-task updated' => 'Subtarefa atualizada', 'Title:' => 'Título:', 'Status:' => 'Status:', @@ -407,18 +408,18 @@ 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][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]' => '', + '[%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)', + '[%s][Due tasks]' => '[%s][Tarefas pendentes]', '[Kanboard] Notification' => '[Kanboard] Notificação', - 'I want to receive notifications only for those projects:' => 'Quero receber notificações somente para estes projetos:', + 'I want to receive notifications only for those projects:' => 'Quero receber notificações apenas destes projetos:', 'view the task on Kanboard' => 'ver a tarefa no Kanboard', 'Public access' => 'Acesso público', 'Category management' => 'Gerenciamento de categorias', @@ -429,17 +430,17 @@ return array( 'Active projects' => 'Projetos ativos', 'Inactive projects' => 'Projetos inativos', 'Public access disabled' => 'Acesso público desabilitado', - 'Do you really want to disable this project: "%s"?' => 'Deseja ralmente desabilitar este projeto: "%s"?', - 'Do you really want to duplicate this project: "%s"?' => 'Deseja realmente duplicar este projeto: "%s"?', - 'Do you really want to enable this project: "%s"?' => 'Deseja realmente habilitar este projeto: "%s"?', - 'Project activation' => 'Avaliação do projeto', + 'Do you really want to disable this project: "%s"?' => 'Você realmente deseja desabilitar este projeto: "%s"?', + 'Do you really want to duplicate this project: "%s"?' => 'Você realmente deseja duplicar este projeto: "%s"?', + 'Do you really want to enable this project: "%s"?' => 'Você realmente deseja habilitar este projeto: "%s"?', + 'Project activation' => 'Ativação do projeto', 'Move the task to another project' => 'Mover a tarefa para outro projeto', 'Move to another project' => 'Mover para outro projeto', - 'Do you really want to duplicate this task?' => 'Deseja realmente duplicar esta tarefa?', - 'Duplicate a task' => 'Duplicar tarefa', + 'Do you really want to duplicate this task?' => 'Você realmente deseja duplicar esta tarefa?', + 'Duplicate a task' => 'Duplicar uma tarefa', 'External accounts' => 'Contas externas', 'Account type' => 'Tipo de conta', - // 'Local' => '', + 'Local' => 'Local', 'Remote' => 'Remoto', 'Enabled' => 'Habilitado', 'Disabled' => 'Desabilitado', @@ -447,12 +448,12 @@ return array( 'Github account linked' => 'Conta do Github associada', 'Username:' => 'Usuário:', 'Name:' => 'Nome:', - // 'Email:' => '', + 'Email:' => 'E-mail:', 'Default project:' => 'Projeto padrão:', 'Notifications:' => 'Notificações:', 'Notifications' => 'Notificações', - 'Group:' => 'Groupo:', - 'Regular user' => 'Usuário habitual', + 'Group:' => 'Grupo:', + 'Regular user' => 'Usuário comum', 'Account type:' => 'Tipo de conta:', 'Edit profile' => 'Editar perfil', 'Change password' => 'Alterar senha', @@ -463,33 +464,33 @@ return array( 'Never connected.' => 'Nunca conectado.', 'No account linked.' => 'Nenhuma conta associada.', 'Account linked.' => 'Conta associada.', - 'No external authentication enabled.' => 'Nenhuma autenticação externa permitida.', + 'No external authentication enabled.' => 'Nenhuma autenticação externa habilitada.', 'Password modified successfully.' => 'Senha alterada com sucesso.', 'Unable to change the password.' => 'Não foi possível alterar a senha.', - 'Change category for the task "%s"' => 'Mudar categoria para a tarefa "%s"', + 'Change category for the task "%s"' => 'Mudar categoria da tarefa "%s"', 'Change category' => 'Mudar categoria', - // '%s updated the task #%d' => '', - // '%s open the task #%d' => '', - // '%s moved the task #%d to the position #%d in the column "%s"' => '', - // '%s moved the task #%d to the column "%s"' => '', - // '%s created the task #%d' => '', - // '%s closed the task #%d' => '', - '%s created a subtask for the task #%d' => '%s criou uma sub-tarefa para a tarefa #%d', - '%s updated a subtask for the task #%d' => '%s atualizou uma sub-tarefa da tarefa #%d', + '%s updated the task %s' => '%s atualizou a tarefa %s', + '%s opened the task %s' => '%s abriu a tarefa %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s moveu a tarefa %s para a posição #%d na coluna "%s"', + '%s moved the task %s to the column "%s"' => '%s moveu a tarefa %s para a coluna "%s"', + '%s created the task %s' => '%s criou a tarefa %s', + '%s closed the task %s' => '%s finalizou a tarefa %s', + '%s created a subtask for the task %s' => '%s criou uma subtarefa para a tarefa %s', + '%s updated a subtask for the task %s' => '%s atualizou uma subtarefa da tarefa %s', 'Assigned to %s with an estimate of %s/%sh' => 'Designado para %s com tempo estimado de %s/%sh', 'Not assigned, estimate of %sh' => 'Não designado, estimado em %sh', - '%s updated a comment on the task #%d' => '%s atualizou o comentário na tarefa #%d', - '%s commented the task #%d' => '%s comentou a tarefa #%d', + '%s updated a comment on the task %s' => '%s atualizou o comentário na tarefa %s', + '%s commented the task %s' => '%s comentou a tarefa %s', '%s\'s activity' => 'Atividades de%s', 'No activity.' => 'Sem atividade.', - // 'RSS feed' => '', - '%s updated a comment on the task #%d' => '%s atualizou um comentário na tarefa #%d', - '%s commented on the task #%d' => '%s comentou na tarefa #%d', - '%s updated a subtask for the task #%d' => '%s atualizou uma sub-tarefa para a tarefa #%d', - '%s created a subtask for the task #%d' => '%s criou uma sub-tarefa para a tarefa #%d', + 'RSS feed' => 'Feed RSS', + '%s updated a comment on the task #%d' => '%s atualizou um comentário sobre a tarefa #%d', + '%s commented on the task #%d' => '%s comentou sobre a tarefa #%d', + '%s updated a subtask for the task #%d' => '%s atualizou uma subtarefa para a tarefa #%d', + '%s created a subtask for the task #%d' => '%s criou uma subtarefa para a tarefa #%d', '%s updated the task #%d' => '%s atualizou a tarefa #%d', '%s created the task #%d' => '%s criou a tarefa #%d', - '%s closed the task #%d' => '%s encerrou a tarefa #%d', + '%s closed the task #%d' => '%s finalizou a tarefa #%d', '%s open the task #%d' => '%s abriu a tarefa #%d', '%s moved the task #%d to the column "%s"' => '%s moveu a tarefa #%d para a coluna "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s moveu a tarefa #%d para a posição %d na coluna "%s"', @@ -498,108 +499,153 @@ return array( 'Default columns for new projects (Comma-separated)' => 'Colunas padrão para novos projetos (Separado por vírgula)', '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 change the assignee of the task #%d to %s' => '%s mudou a designação da tarefa #%d para %s', - // '[%s][Column Change] %s (#%d)' => '', - // '[%s][Position Change] %s (#%d)' => '', - // '[%s][Assignee Change] %s (#%d)' => '', - 'New password for the user "%s"' => 'Novo password para o usuário "%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)', + 'New password for the user "%s"' => 'Nova senha para o usuário "%s"', 'Choose an event' => 'Escolher um evento', - // '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' => 'Criar uma tarefa a partir de um provedor externo', - 'Change the assignee based on an external username' => 'Alterar designação com vase em um usuário externo!', + 'Github commit received' => 'Github commit received', + 'Github issue opened' => 'Github issue opened', + 'Github issue closed' => 'Github issue closed', + 'Github issue reopened' => 'Github issue reopened', + 'Github issue assignee change' => 'Github issue assignee change', + 'Github issue label change' => 'Github issue label change', + 'Create a task from an external provider' => 'Criar uma tarefa por meio de um serviço externo', + 'Change the assignee based on an external username' => 'Alterar designação com base em um usuário externo', 'Change the category based on an external label' => 'Alterar categoria com base em um rótulo externo', - 'Reference' => 'Referencia', - 'Reference: %s' => 'Referencia: %s', + 'Reference' => 'Referência', + 'Reference: %s' => 'Referência: %s', 'Label' => 'Rótulo', 'Database' => 'Banco de dados', 'About' => 'Sobre', - // '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)' => '', + 'Database driver:' => 'Driver do banco de dados:', + 'Board settings' => 'Configurações do Board', + 'URL and token' => 'URL e token', + 'Webhook settings' => 'Configurações do Webhook', + 'URL for task creation:' => 'URL para a criação da tarefa:', + 'Reset token' => 'Resetar token', + 'API endpoint:' => 'API endpoint:', + 'Refresh interval for private board' => 'Intervalo de atualização para um board privado', + 'Refresh interval for public board' => 'Intervalo de atualização para um board público', + 'Task highlight period' => 'Período de Tarefa em destaque', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Período (em segundos) para considerar que uma tarefa foi modificada recentemente (0 para desativar, 2 dias por padrão)', + 'Frequency in second (60 seconds by default)' => 'Frequência em segundos (60 segundos por padrão)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequência em segundos (0 para desativar este recurso, 10 segundos por padrão)', 'Application URL' => 'URL da Aplicação', - // 'Example: http://example.kanboard.net/ (used by email notifications)' => '', - // 'Token regenerated.' => '', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Exemplo: http://example.kanboard.net/ (utilizado nas notificações por e-mail)', + 'Token regenerated.' => 'Token ', 'Date format' => 'Formato de data', 'ISO format is always accepted, example: "%s" and "%s"' => 'O formato ISO é sempre aceito, exemplo: "%s" e "%s"', 'New private project' => 'Novo projeto privado', 'This project is private' => 'Este projeto é privado', - 'Type here to create a new sub-task' => 'Digite aqui para criar uma nova sub-tarefa', + 'Type here to create a new sub-task' => 'Digite aqui para criar uma nova subtarefa', 'Add' => 'Adicionar', 'Estimated time: %s hours' => 'Tempo estimado: %s horas', 'Time spent: %s hours' => 'Tempo gasto: %s horas', 'Started on %B %e, %Y' => 'Iniciado em %B %e, %Y', 'Start date' => 'Data de início', 'Time estimated' => 'Tempo estimado', - 'There is nothing assigned to you.' => 'Não há nada designado para você.', + 'There is nothing assigned to you.' => 'Não há nada designado à você.', 'My tasks' => 'Minhas tarefas', - // 'Activity stream' => '', - // 'Dashboard' => '', + 'Activity stream' => 'Atividades Recentes', + 'Dashboard' => 'Painel de Controle', 'Confirmation' => 'Confirmação', 'Allow everybody to access to this project' => 'Permitir que todos acessem este projeto', 'Everybody have access to this project.' => 'Todos possuem acesso a este projeto.', - // 'Webhooks' => '', - // 'API' => '', + 'Webhooks' => 'Webhooks', + 'API' => 'API', 'Integration' => 'Integração', - // 'Github webhook' => '', - 'Help on Github webhook' => 'Ajuda para o Github webhook', - 'Create a comment from an external provider' => 'Criar um comentário de um provedor externo', - // 'Github issue comment created' => '', + 'Github webhooks' => 'Github webhooks', + 'Help on Github webhooks' => 'Ajuda para o Github webhooks', + 'Create a comment from an external provider' => 'Criar um comentário por meio de um serviço externo', + 'Github issue comment created' => 'Github issue comment created', 'Configure' => 'Configurar', 'Project management' => 'Gerenciamento de projetos', 'My projects' => 'Meus projetos', 'Columns' => 'Colunas', 'Task' => 'Tarefas', - 'Your are not member of any project.' => 'Você não é menmbro de nenhum projeto.', + 'Your are not member of any project.' => 'Você não é membro de nenhum projeto.', 'Percentage' => 'Porcentagem', 'Number of tasks' => 'Número de tarefas', 'Task distribution' => 'Distribuição de tarefas', 'Reportings' => 'Relatórios', - // 'Task repartition for "%s"' => '', + 'Task repartition for "%s"' => 'Redistribuição da tarefa para "%s"', 'Analytics' => 'Estatísticas', - 'Subtask' => 'Sub-tarefa', - 'My subtasks' => 'Minhas sub-tarefas', - 'User repartition' => 'Repartição de usuário', - 'User repartition for "%s"' => 'Repartição de usuário para "%s"', - 'Clone this project' => 'Clonar o projeto', + 'Subtask' => 'Subtarefa', + 'My subtasks' => 'Minhas subtarefas', + 'User repartition' => 'Redistribuição de usuário', + 'User repartition for "%s"' => 'Redistribuição de usuário para "%s"', + 'Clone this project' => 'Clonar este projeto', 'Column removed successfully.' => 'Coluna removida com sucesso.', 'Edit Project' => 'Editar projeto', - // 'Github Issue' => '', - 'Not enough data to show the graph.' => 'Dados insuficientes para exibir o gráfico.', + 'Github Issue' => 'Github Issue', + 'Not enough data to show the graph.' => 'Não há dados suficientes para mostrar o gráfico.', 'Previous' => 'Anterior', - 'The id must be an integer' => 'A ID deve ser um inteiro', - 'The project id must be an integer' => 'A ID do projeto deve ser um inteiro', - 'The status must be an integer' => 'O status deve ser um inteiro', - 'The subtask id is required' => 'A ID da sub-tarefa é requerida', - 'The subtask id must be an integer' => 'A ID da sub-tarefa deve ser um inteiro', - 'The task id is required' => 'A ID da tarefa é requerida', - 'The task id must be an integer' => 'A ID da tarefa deve ser um inteiro', - 'The user id must be an integer' => 'A ID de usuário deve ser um inteiro', - 'This value is required' => 'Este valor é requerido', + 'The id must be an integer' => 'O ID deve ser um número inteiro', + 'The project id must be an integer' => 'O ID do projeto deve ser um inteiro', + 'The status must be an integer' => 'O status deve ser um número inteiro', + 'The subtask id is required' => 'O ID da subtarefa é obrigatório', + 'The subtask id must be an integer' => 'O ID da subtarefa deve ser um número inteiro', + 'The task id is required' => 'O ID da tarefa é obrigatório', + 'The task id must be an integer' => 'O ID da tarefa deve ser um número inteiro', + 'The user id must be an integer' => 'O ID do usuário deve ser um número inteiro', + 'This value is required' => 'Este valor é obrigatório', 'This value must be numeric' => 'Este valor deve ser numérico', 'Unable to create this task.' => 'Não foi possível criar esta tarefa.', - // '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.' => '', + 'Cumulative flow diagram' => 'Fluxograma cumulativo', + 'Cumulative flow diagram for "%s"' => 'Fluxograma cumulativo para "%s"', + 'Daily project summary' => 'Resumo diário do projeto', + 'Daily project summary export' => 'Exportação diária do resumo do projeto', + 'Daily project summary export for "%s"' => 'Exportação diária do resumo do projeto para "%s"', + 'Exports' => 'Exportar', + 'This export contains the number of tasks per column grouped per day.' => '', 'Nothing to preview...' => 'Nada para pré-visualizar...', 'Preview' => 'Pré-visualizar', - // 'Write' => '', + 'Write' => 'Escrever', + 'Active swimlanes' => 'Ativar swimlanes', + 'Add a new swimlane' => 'Adicionar novo swimlane', + 'Change default swimlane' => 'Alterar swimlane padrão', + 'Default swimlane' => 'Swimlane padrão', + 'Do you really want to remove this swimlane: "%s"?' => 'Você realmente deseja remover este swimlane: "%s"?', + 'Inactive swimlanes' => 'Desativar swimlanes', + 'Set project manager' => 'Definir gerente do projeto', + 'Set project member' => 'Definir membro do projeto', + 'Remove a swimlane' => 'Remover um swimlane', + 'Rename' => 'Renomear', + 'Show default swimlane' => 'Exibir swimlane padrão', + 'Swimlane modification for the project "%s"' => 'Modificação de swimlane para o projeto "%s"', + 'Swimlane not found.' => 'Swimlane não encontrado.', + 'Swimlane removed successfully.' => 'Swimlane removido com sucesso.', + 'Swimlanes' => 'Swimlanes', + 'Swimlane updated successfully.' => 'Swimlane atualizado com sucesso.', + 'The default swimlane have been updated successfully.' => 'O swimlane padrão foi atualizado com sucesso.', + 'Unable to create your swimlane.' => 'Não foi possível criar o seu swimlane.', + 'Unable to remove this swimlane.' => 'Não foi possível remover este swimlane.', + 'Unable to update this swimlane.' => 'Não foi possível atualizar este swimlane.', + 'Your swimlane have been created successfully.' => 'Seu swimlane foi criado com sucesso.', + 'Example: "Bug, Feature Request, Improvement"' => 'Exemplo: "Bug, Feature Request, Improvement"', + 'Default categories for new projects (Comma-separated)' => 'Categorias padrão para novos projetos (Separadas por vírgula)', + 'Gitlab commit received' => 'Gitlab commit received', + 'Gitlab issue opened' => 'Gitlab issue opened', + 'Gitlab issue closed' => 'Gitlab issue closed', + 'Gitlab webhooks' => 'Gitlab webhooks', + 'Help on Gitlab webhooks' => 'Ajuda sobre Gitlab webhooks', + 'Integrations' => 'Integrações', + 'Integration with third-party services' => 'Integração com serviços de terceiros', + 'Role for this project' => 'Função para este projeto', + 'Project manager' => 'Gerente do projeto', + 'Project member' => 'Membro do projeto', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Um gerente do projeto pode alterar as configurações do projeto e ter mais privilégios que um usuário padrão.', + 'Gitlab Issue' => 'Gitlab Issue', + 'Subtask Id' => 'ID da subtarefa', + 'Subtasks' => 'Subtarefas', + 'Subtasks Export' => 'Exportar subtarefas', + 'Subtasks exportation for "%s"' => 'Subtarefas exportadas para "%s"', + 'Task Title' => 'Título da Tarefa', + 'Untitled' => 'Sem título', + 'Application default' => 'Aplicação padrão', + 'Language:' => 'Idioma', + 'Timezone:' => 'Fuso horário', + 'Next' => 'Próximo', ); diff --git a/sources/app/Locale/ru_RU/translations.php b/sources/app/Locale/ru_RU/translations.php index a1cc7cf..a857de3 100644 --- a/sources/app/Locale/ru_RU/translations.php +++ b/sources/app/Locale/ru_RU/translations.php @@ -182,7 +182,7 @@ return array( 'Change assignee' => 'Сменить назначенного', 'Change assignee for the task "%s"' => 'Сменить назначенного для задачи « %s »', 'Timezone' => 'Часовой пояс', - 'Sorry, I didn\'t found this information in my database!' => 'К сожалению, информация в базе данных не найдена !', + 'Sorry, I didn\'t find this information in my database!' => 'К сожалению, информация в базе данных не найдена !', 'Page not found' => 'Страница не найдена', 'Complexity' => 'Сложность', 'limit' => 'лимит', @@ -194,7 +194,7 @@ return array( 'Allow this user' => 'Разрешить этого пользователя', 'Only those users have access to this project:' => 'Только эти пользователи имеют доступ к проекту :', 'Don\'t forget that administrators have access to everything.' => 'Помните, администратор имеет доступ ко всему.', - 'revoke' => 'отозвать', + 'Revoke' => 'отозвать', 'List of authorized users' => 'Список авторизованных пользователей', 'User' => 'Пользователь', 'Nobody have access to this project.' => 'Ни у кого нет доступа к этому проекту', @@ -213,6 +213,7 @@ return array( 'Invalid date' => 'Неверная дата', 'Must be done before %B %e, %Y' => 'Должно быть сделано до %d/%m/%Y', '%B %e, %Y' => '%d/%m/%Y', + // '%b %e, %Y' => '', 'Automatic actions' => 'Автоматические действия', 'Your automatic action have been created successfully.' => 'Автоматика настроена.', 'Unable to create your automatic action.' => 'Не удалось создать автоматизированное действие.', @@ -468,18 +469,18 @@ return array( 'Unable to change the password.' => 'Не удалось сменить пароль.', 'Change category for the task "%s"' => 'Сменить категорию для задачи "%s"', 'Change category' => 'Смена категории', - '%s updated the task #%d' => '%s обновил задачу #%d', - '%s open the task #%d' => '%s открыл задачу #%d', - '%s moved the task #%d to the position #%d in the column "%s"' => '%s перместил задачу #%d на позицию #%d в колонке "%s"', - '%s moved the task #%d to the column "%s"' => '%s переместил задачу #%d в колонку "%s"', - '%s created the task #%d' => '%s создал задачу #%d', - '%s closed the task #%d' => '%s закрыл задачу #%d', - '%s created a subtask for the task #%d' => '%s создал подзадачу для задачи #%d', - '%s updated a subtask for the task #%d' => '%s обновил подзадачу для задачи #%d', + '%s updated the task %s' => '%s обновил задачу %s', + '%s opened the task %s' => '%s открыл задачу %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s перместил задачу %s на позицию #%d в колонке "%s"', + '%s moved the task %s to the column "%s"' => '%s переместил задачу %s в колонку "%s"', + '%s created the task %s' => '%s создал задачу %s', + '%s closed the task %s' => '%s закрыл задачу %s', + '%s created a subtask for the task %s' => '%s создал подзадачу для задачи %s', + '%s updated a subtask for the task %s' => '%s обновил подзадачу для задачи %s', 'Assigned to %s with an estimate of %s/%sh' => 'Назначено %s с окончанием %s/%sh', 'Not assigned, estimate of %sh' => 'Не назначено, окончание %sh', - '%s updated a comment on the task #%d' => '%s обновил комментарий к задаче #%d', - '%s commented the task #%d' => '%s прокомментировал задачу #%d', + '%s updated a comment on the task %s' => '%s обновил комментарий к задаче %s', + '%s commented the task %s' => '%s прокомментировал задачу %s', '%s\'s activity' => '%s активность', 'No activity.' => 'Нет активности', 'RSS feed' => 'RSS лента', @@ -498,7 +499,7 @@ return array( 'Default columns for new projects (Comma-separated)' => 'Колонки по умолчанию для новых проектов (разделять запятой)', 'Task assignee change' => 'Изменен назначенный', '%s change the assignee of the task #%d to %s' => '%s сменил назначенного для задачи #%d на %s', - '%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)', @@ -555,8 +556,8 @@ return array( // 'Webhooks' => '', // 'API' => '', // 'Integration' => '', - // 'Github webhook' => '', - // 'Help on Github webhook' => '', + // 'Github webhooks' => '', + // 'Help on Github webhooks' => '', // 'Create a comment from an external provider' => '', // 'Github issue comment created' => '', // 'Configure' => '', @@ -602,4 +603,49 @@ return array( // '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)' => '', + // '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' => '', ); diff --git a/sources/app/Locale/sv_SE/translations.php b/sources/app/Locale/sv_SE/translations.php index ece0b2a..ae805d4 100644 --- a/sources/app/Locale/sv_SE/translations.php +++ b/sources/app/Locale/sv_SE/translations.php @@ -104,7 +104,7 @@ return array( 'Open a task' => 'Öppna en uppgift', 'Do you really want to open this task: "%s"?' => 'Vill du verkligen öppna denna uppgift: "%s"?', 'Back to the board' => 'Tillbaka till tavlan', - 'Created on %B %e, %Y at %k:%M %p' => 'Skapad %d %B %Y kl %H:%M', + 'Created on %B %e, %Y at %k:%M %p' => 'Skapad %Y-%m-%d kl %H:%M', 'There is nobody assigned' => 'Det finns ingen tilldelad', 'Column on the board:' => 'Kolumn på tavlan:', 'Status is open' => 'Statusen är öppen', @@ -166,8 +166,8 @@ return array( 'Work in progress' => 'Pågående', 'Done' => 'Slutfört', 'Application version:' => 'Version:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Slutfört %d %B %Y kl %H:%M', - '%B %e, %Y at %k:%M %p' => '%d %B %Y kl %H:%M', + 'Completed on %B %e, %Y at %k:%M %p' => 'Slutfört %Y-%m-%d kl %H:%M', + '%B %e, %Y at %k:%M %p' => '%Y-%m-%d kl %H:%M', 'Date created' => 'Skapat datum', 'Date completed' => 'Slutfört datum', 'Id' => 'ID', @@ -182,19 +182,19 @@ return array( 'Change assignee' => 'Ändra uppdragsinnehavare', 'Change assignee for the task "%s"' => 'Ändra uppdragsinnehavare för uppgiften "%s"', 'Timezone' => 'Tidszon', - 'Sorry, I didn\'t found this information in my database!' => 'Informationen kunde inte hittas i databasen.', + 'Sorry, I didn\'t find this information in my database!' => 'Informationen kunde inte hittas i databasen.', 'Page not found' => 'Sidan hittas inte', - 'Complexity' => 'Ungefärligt antal timmar', + 'Complexity' => 'Komplexitet', 'limit' => 'max', 'Task limit' => 'Uppgiftsbegränsning', - // 'Task count' => '', + 'Task count' => 'Antal uppgifter', 'This value must be greater than %d' => 'Värdet måste vara större än %d', 'Edit project access list' => 'Ändra projektåtkomst lista', 'Edit users access' => 'Användaråtkomst', 'Allow this user' => 'Tillåt användare', 'Only those users have access to this project:' => 'Bara de användarna har tillgång till detta projekt.', 'Don\'t forget that administrators have access to everything.' => 'Glöm inte att administratörerna har rätt att göra allt.', - 'revoke' => 'Dra tillbaka behörighet', + 'Revoke' => 'Dra tillbaka behörighet', 'List of authorized users' => 'Lista med behöriga användare', 'User' => 'Användare', 'Nobody have access to this project.' => 'Ingen har tillgång till detta projekt.', @@ -211,8 +211,9 @@ return array( 'Edit this task' => 'Ändra denna uppgift', 'Due Date' => 'Måldatum', 'Invalid date' => 'Ej tillåtet datum', - 'Must be done before %B %e, %Y' => 'Måste vara klart innan %B %e, %Y', - '%B %e, %Y' => '%d %B %Y', + 'Must be done before %B %e, %Y' => 'Måste vara klart innan %Y-%m-%d', + '%B %e, %Y' => '%Y-%m-%d', + '%b %e, %Y' => '%Y-%m-%d', 'Automatic actions' => 'Automatiska åtgärder', 'Your automatic action have been created successfully.' => 'Din automatiska åtgärd har skapats.', 'Unable to create your automatic action.' => 'Kunde inte skapa din automatiska åtgärd.', @@ -376,7 +377,7 @@ return array( 'Link my GitHub Account' => 'Anslut mitt GitHub-konto', 'Unlink my GitHub Account' => 'Koppla ifrån mitt GitHub-konto', 'Created by %s' => 'Skapad av %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Senaste ändring %B %e, %Y kl %k:%M %p', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Senaste ändring %Y-%m-%d kl %H:%M', 'Tasks Export' => 'Exportera uppgifter', 'Tasks exportation for "%s"' => 'Exportera uppgifter för "%s"', 'Start Date' => 'Startdatum', @@ -468,18 +469,18 @@ return array( 'Unable to change the password.' => 'Kunde inte byta lösenord.', 'Change category for the task "%s"' => 'Byt kategori för uppgiften "%s"', 'Change category' => 'Byt kategori', - '%s updated the task #%d' => '%s uppdaterade uppgiften #%d', - '%s open the task #%d' => '%s öppna uppgiften #%d', - '%s moved the task #%d to the position #%d in the column "%s"' => '%s flyttade uppgiften #%d till positionen #%d i kolumnen "%s"', - '%s moved the task #%d to the column "%s"' => '%s flyttade uppgiften #%d till kolumnen "%s"', - '%s created the task #%d' => '%s skapade uppgiften #%d', - '%s closed the task #%d' => '%s stängde uppgiften #%d', - '%s created a subtask for the task #%d' => '%s skapade en deluppgift för uppgiften #%d', - '%s updated a subtask for the task #%d' => '%s uppdaterade en deluppgift för uppgiften #%d', + '%s updated the task %s' => '%s uppdaterade uppgiften %s', + '%s opened the task %s' => '%s öppna uppgiften %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s flyttade uppgiften %s till positionen #%d i kolumnen "%s"', + '%s moved the task %s to the column "%s"' => '%s flyttade uppgiften %s till kolumnen "%s"', + '%s created the task %s' => '%s skapade uppgiften %s', + '%s closed the task %s' => '%s stängde uppgiften %s', + '%s created a subtask for the task %s' => '%s skapade en deluppgift för uppgiften %s', + '%s updated a subtask for the task %s' => '%s uppdaterade en deluppgift för uppgiften %s', 'Assigned to %s with an estimate of %s/%sh' => 'Tilldelades %s med en uppskattning på %s/%sh', 'Not assigned, estimate of %sh' => 'Inte tilldelade, uppskattat %sh', - '%s updated a comment on the task #%d' => '%s uppdaterade en kommentar till uppgiften #%d', - '%s commented the task #%d' => '%s kommenterade uppgiften #%d', + '%s updated a comment on the task %s' => '%s uppdaterade en kommentar till uppgiften %s', + '%s commented the task %s' => '%s kommenterade uppgiften %s', '%s\'s activity' => '%s\'s aktivitet', 'No activity.' => 'Ingen aktivitet.', 'RSS feed' => 'RSS flöde', @@ -498,7 +499,7 @@ return array( 'Default columns for new projects (Comma-separated)' => 'Standardkolumner för nya projekt (kommaseparerade)', '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 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)', @@ -542,64 +543,109 @@ return array( 'Add' => 'Lägg till', 'Estimated time: %s hours' => 'Uppskattad tid: %s timmar', 'Time spent: %s hours' => 'Nedlaggd tid: %s timmar', - 'Started on %B %e, %Y' => 'Startad den %B %e, %Y', + 'Started on %B %e, %Y' => 'Startad %Y-%m-%d', 'Start date' => 'Startdatum', 'Time estimated' => 'Uppskattad tid', 'There is nothing assigned to you.' => 'Du har inget tilldelat till dig.', - 'My tasks' => 'Mina uppgifte', + 'My tasks' => 'Mina uppgifter', 'Activity stream' => 'Aktivitetsström', 'Dashboard' => 'Instrumentpanel', 'Confirmation' => 'Bekräftelse', - // 'Allow everybody to access to this project' => '', - // 'Everybody have access to this project.' => '', - // 'Webhooks' => '', - // 'API' => '', - // 'Integration' => '', - // 'Github webhook' => '', - // 'Help on Github webhook' => '', - // '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' => '', + 'Allow everybody to access to this project' => 'Ge alla tillgång till projektet', + 'Everybody have access to this project.' => 'Alla har tillgång till projektet', + 'Webhooks' => 'Webhooks', + 'API' => 'API', + 'Integration' => 'Integration', + 'Github webhooks' => 'Github webhooks', + 'Help on Github webhooks' => 'Hjälp för Github webhooks', + 'Create a comment from an external provider' => 'Skapa en kommentar från en extern leverantör', + 'Github issue comment created' => 'Github frågekommentar skapad', + 'Configure' => 'Konfigurera', + 'Project management' => 'Projekthantering', + 'My projects' => 'Mina projekt', + 'Columns' => 'Kolumner', + 'Task' => 'Uppgift', + 'Your are not member of any project.' => 'Du är inte medlem i något projekt', + 'Percentage' => 'Procent', + 'Number of tasks' => 'Antal uppgifter', + 'Task distribution' => 'Uppgiftsfördelning', + 'Reportings' => 'Rapportering', + 'Task repartition for "%s"' => 'Uppgiftsdeltagande för "%s"', + 'Analytics' => 'Analyser', + 'Subtask' => 'Deluppgift', + 'My subtasks' => 'Mina deluppgifter', + 'User repartition' => 'Användardeltagande', + 'User repartition for "%s"' => 'Användardeltagande för "%s"', + 'Clone this project' => 'Klona projektet', + 'Column removed successfully.' => 'Kolumnen togs bort', + 'Edit Project' => 'Ändra Projekt', + 'Github Issue' => 'Github fråga', + 'Not enough data to show the graph.' => 'Inte tillräckligt med data för att visa graf', + 'Previous' => 'Föregående', + 'The id must be an integer' => 'ID måste vara ett heltal', + 'The project id must be an integer' => 'Projekt-ID måste vara ett heltal', + 'The status must be an integer' => 'Status måste vara ett heltal', + 'The subtask id is required' => 'Deluppgifts-ID behövs', + 'The subtask id must be an integer' => 'Deluppgifts-ID måste vara ett heltal', + 'The task id is required' => 'Uppgifts-ID behövs', + 'The task id must be an integer' => 'Uppgifts-ID måste vara ett heltal', + 'The user id must be an integer' => 'Användar-ID måste vara ett heltal', + 'This value is required' => 'Värdet behövs', + 'This value must be numeric' => 'Värdet måste vara numeriskt', + 'Unable to create this task.' => 'Kunde inte skapa uppgiften.', + 'Cumulative flow diagram' => 'Diagram med kumulativt flöde', + 'Cumulative flow diagram for "%s"' => 'Diagram med kumulativt flöde för "%s"', + 'Daily project summary' => 'Daglig projektsummering', + 'Daily project summary export' => 'Export av daglig projektsummering', + 'Daily project summary export for "%s"' => 'Export av daglig projektsummering för "%s"', + 'Exports' => 'Exporter', + 'This export contains the number of tasks per column grouped per day.' => 'Denna export innehåller antalet uppgifter per kolumn grupperade per dag.', + 'Nothing to preview...' => 'Inget att förhandsgrandska...', + 'Preview' => 'Förhandsgranska', + 'Write' => 'Skriva', + 'Active swimlanes' => 'Aktiva swimlanes', + 'Add a new swimlane' => 'Lägg till en nytt swimlane', + 'Change default swimlane' => 'Ändra standard swimlane', + 'Default swimlane' => 'Standard swimlane', + 'Do you really want to remove this swimlane: "%s"?' => 'Vill du verkligen ta bort denna swimlane: "%s"?', + 'Inactive swimlanes' => 'Inaktiv swimlane', + 'Set project manager' => 'Sätt Projektadministratör', + 'Set project member' => 'Sätt projektmedlem', + 'Remove a swimlane' => 'Ta bort en swimlane', + 'Rename' => 'Byt namn', + 'Show default swimlane' => 'Visa standard swimlane', + 'Swimlane modification for the project "%s"' => 'Ändra swimlane för projektet "%s"', + 'Swimlane not found.' => 'Swimlane kunde inte hittas', + 'Swimlane removed successfully.' => 'Swimlane togs bort', + 'Swimlanes' => 'Swimlanes', + 'Swimlane updated successfully.' => 'Swimlane uppdaterad', + 'The default swimlane have been updated successfully.' => 'Standardswimlane har uppdaterats', + 'Unable to create your swimlane.' => 'Kunde inte skapa din swimlane', + 'Unable to remove this swimlane.' => 'Kunde inte ta bort swimlane', + 'Unable to update this swimlane.' => 'Kunde inte uppdatera swimlane', + 'Your swimlane have been created successfully.' => 'Din swimlane har skapats', + 'Example: "Bug, Feature Request, Improvement"' => 'Exempel: "Bug, ny funktionalitet, förbättringar"', + 'Default categories for new projects (Comma-separated)' => 'Standardkategorier för nya projekt (komma-separerade)', + 'Gitlab commit received' => 'Gitlab bidrag mottaget', + 'Gitlab issue opened' => 'Gitlab fråga öppnad', + 'Gitlab issue closed' => 'Gitlab fråga stängd', + 'Gitlab webhooks' => 'Gitlab webhooks', + 'Help on Gitlab webhooks' => 'Hjälp för Gitlab webhooks', + 'Integrations' => 'Integrationer', + 'Integration with third-party services' => 'Integration med tjänst från tredjepart', + 'Role for this project' => 'Roll för detta projekt', + 'Project manager' => 'Projektadministratör', + 'Project member' => 'Projektmedlem', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'En projektadministratör kan ändra inställningar för projektet och har mer rättigheter än en standardanvändare.', + 'Gitlab Issue' => 'Gitlab fråga', + 'Subtask Id' => 'Deluppgifts-ID', + 'Subtasks' => 'Deluppgift', + 'Subtasks Export' => 'Export av deluppgifter', + 'Subtasks exportation for "%s"' => 'Export av deluppgifter för "%s"', + 'Task Title' => 'Uppgiftstitel', + 'Untitled' => 'Titel saknas', + 'Application default' => 'Applikationsstandard', + 'Language:' => 'Språk', + 'Timezone:' => 'Tidszon', + 'Next' => 'Nästa', ); diff --git a/sources/app/Locale/th_TH/translations.php b/sources/app/Locale/th_TH/translations.php index 42a1bcb..f091020 100644 --- a/sources/app/Locale/th_TH/translations.php +++ b/sources/app/Locale/th_TH/translations.php @@ -182,7 +182,7 @@ return array( 'Change assignee' => 'เปลี่ยนการกำหนด', 'Change assignee for the task "%s"' => 'เปลี่ยนการกำหนดสำหรับงาน « %s »', 'Timezone' => 'เขตเวลา', - 'Sorry, I didn\'t found this information in my database!' => 'เสียใจด้วย ไม่สามารถหาข้อมูลในฐานข้อมูลได้', + 'Sorry, I didn\'t find this information in my database!' => 'เสียใจด้วย ไม่สามารถหาข้อมูลในฐานข้อมูลได้', 'Page not found' => 'ไม่พบหน้า', 'Complexity' => 'ความซับซ้อน', 'limit' => 'จำกัด', @@ -194,7 +194,7 @@ return array( 'Allow this user' => 'อนุญาตผู้ใช้นี้', 'Only those users have access to this project:' => 'ผู้ใช้ที่สามารถเข้าถึงโปรเจคนี้:', 'Don\'t forget that administrators have access to everything.' => 'อย่าลืมผู้ดูแลระบบสามารถเข้าถึงได้ทุกอย่าง', - 'revoke' => 'ยกเลิก', + 'Revoke' => 'ยกเลิก', 'List of authorized users' => 'รายชื่อผู้ใช้ที่ได้รับการยืนยัน', 'User' => 'ผู้ใช้', // 'Nobody have access to this project.' => '', @@ -213,6 +213,7 @@ return array( 'Invalid date' => 'วันที่ผิด', 'Must be done before %B %e, %Y' => 'ต้องทำให้เสร็จก่อน %d/%m/%Y', '%B %e, %Y' => '%d/%m/%Y', + // '%b %e, %Y' => '', 'Automatic actions' => 'การกระทำอัตโนมัติ', 'Your automatic action have been created successfully.' => 'การกระทำอัตโนมัติสร้างเรียบร้อยแล้ว', 'Unable to create your automatic action.' => 'ไม่สามารถสร้างการกระทำอัตโนมัติได้', @@ -468,18 +469,18 @@ return array( 'Unable to change the password.' => 'ไม่สามารถเปลี่ยนรหัสผ่านได้', 'Change category for the task "%s"' => 'เปลี่ยนกลุ่มสำหรับงาน "%s"', 'Change category' => 'เปลี่ยนกลุ่ม', - '%s updated the task #%d' => '%s ปรับปรุงงานแล้ว #%d', - '%s open the task #%d' => '%s เปิดงานแล้ว #%d', - '%s moved the task #%d to the position #%d in the column "%s"' => '%s ย้ายงานแล้ว #%d ไปตำแหน่ง #%d ในคอลัมน์ "%s"', - '%s moved the task #%d to the column "%s"' => '%s ย้ายงานแล้ว #%d ไปคอลัมน์ "%s"', - '%s created the task #%d' => '%s สร้างงานแล้ว #%d', - '%s closed the task #%d' => '%s ปิดงานแล้ว #%d', - '%s created a subtask for the task #%d' => '%s สร้างงานย่อยสำหรับงานแล้ว #%d', - '%s updated a subtask for the task #%d' => '%s ปรับปรุงงานย่อยสำหรับงานแล้ว #%d', + '%s updated the task %s' => '%s ปรับปรุงงานแล้ว %s', + '%s opened the task %s' => '%s เปิดงานแล้ว %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s ย้ายงานแล้ว %s ไปตำแหน่ง #%d ในคอลัมน์ "%s"', + '%s moved the task %s to the column "%s"' => '%s ย้ายงานแล้ว %s ไปคอลัมน์ "%s"', + '%s created the task %s' => '%s สร้างงานแล้ว %s', + '%s closed the task %s' => '%s ปิดงานแล้ว %s', + '%s created a subtask for the task %s' => '%s สร้างงานย่อยสำหรับงานแล้ว %s', + '%s updated a subtask for the task %s' => '%s ปรับปรุงงานย่อยสำหรับงานแล้ว %s', 'Assigned to %s with an estimate of %s/%sh' => 'กำหนดให้ %s โดยประมาณแล้ว %s/%sh', 'Not assigned, estimate of %sh' => 'ไม่กำหนดแล้ว, ประมาณเวลาที่ใช้ %s ชั่วโมง', - '%s updated a comment on the task #%d' => '%s ปรับปรุงความคิดเห็นในงานแล้ว #%d', - '%s commented the task #%d' => '%s แสดงความคิดเห็นของงานแล้ว #%d', + '%s updated a comment on the task %s' => '%s ปรับปรุงความคิดเห็นในงานแล้ว %s', + '%s commented the task %s' => '%s แสดงความคิดเห็นของงานแล้ว %s', '%s\'s activity' => 'กิจกรรม %s', 'No activity.' => 'ไม่มีกิจกรรม', 'RSS feed' => 'RSS feed', @@ -498,7 +499,7 @@ return array( 'Default columns for new projects (Comma-separated)' => 'คอลัมน์เริ่มต้นสำหรับโปรเจคใหม่ (Comma-separated)', 'Task assignee change' => 'เปลี่ยนการกำหนดบุคคลของงาน', // '%s change the assignee of the task #%d to %s' => '', - // '%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)' => '', @@ -555,8 +556,8 @@ return array( // 'Webhooks' => '', // 'API' => '', // 'Integration' => '', - // 'Github webhook' => '', - // 'Help on Github webhook' => '', + // 'Github webhooks' => '', + // 'Help on Github webhooks' => '', // 'Create a comment from an external provider' => '', // 'Github issue comment created' => '', // 'Configure' => '', @@ -602,4 +603,49 @@ return array( // '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)' => '', + // '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' => '', ); diff --git a/sources/app/Locale/zh_CN/translations.php b/sources/app/Locale/zh_CN/translations.php index d146459..cd423d0 100644 --- a/sources/app/Locale/zh_CN/translations.php +++ b/sources/app/Locale/zh_CN/translations.php @@ -24,7 +24,7 @@ return array( 'Unassigned' => '未指定', 'View this task' => '查看该任务', 'Remove user' => '移除用户', - 'Do you really want to remove this user: "%s"?' => '你确定要移除这个用户吗:"%s"?', + 'Do you really want to remove this user: "%s"?' => '确定要删除用户"%s"吗?', 'New user' => '新建用户', 'All users' => '所有用户', 'Username' => '用户名', @@ -63,7 +63,7 @@ return array( 'Disable' => '停用', 'Enable' => '启用', 'New project' => '新建项目', - 'Do you really want to remove this project: "%s"?' => '你确定要移除该项目吗:"%s"?', + 'Do you really want to remove this project: "%s"?' => '确定要移除项目"%s"吗?', 'Remove project' => '移除项目', 'Boards' => '看板', 'Edit the board for "%s"' => '为"%s"修改看板', @@ -78,7 +78,7 @@ return array( 'Remove a column' => '移除一个栏目', 'Remove a column from a board' => '从看板移除一个栏目', 'Unable to remove this column.' => '无法移除该栏目。', - 'Do you really want to remove this column: "%s"?' => '你确定要移除该栏目:"%s"吗?', + 'Do you really want to remove this column: "%s"?' => '确定要移除栏目"%s"吗?', 'This action will REMOVE ALL TASKS associated to this column!' => '该动作将移除与该栏目相关的所有项目!', 'Settings' => '设置', 'Application settings' => '应用设置', @@ -182,19 +182,19 @@ return array( 'Change assignee' => '变更负责人', 'Change assignee for the task "%s"' => '更改任务"%s"的负责人', 'Timezone' => '时区', - 'Sorry, I didn\'t found this information in my database!' => '抱歉,无法在数据库中找到该信息!', + 'Sorry, I didn\'t find this information in my database!' => '抱歉,无法在数据库中找到该信息!', 'Page not found' => '页面未找到', 'Complexity' => '复杂度', 'limit' => '限制', 'Task limit' => '任务限制', - // 'Task count' => '', + 'Task count' => '任务数', 'This value must be greater than %d' => '该数值必须大于%d', 'Edit project access list' => '编辑项目存取列表', 'Edit users access' => '编辑用户存取权限', 'Allow this user' => '允许该用户', 'Only those users have access to this project:' => '只有这些用户有该项目的存取权限:', 'Don\'t forget that administrators have access to everything.' => '别忘了管理员有一切的权限。', - 'revoke' => '撤销', + 'Revoke' => '撤销', 'List of authorized users' => '已授权的用户列表', 'User' => '用户', 'Nobody have access to this project.' => '无用户可以访问此项目.', @@ -213,6 +213,7 @@ return array( 'Invalid date' => '无效日期', 'Must be done before %B %e, %Y' => '必须在%Y/%m/%d前完成', '%B %e, %Y' => '%Y/%m/%d', + // '%b %e, %Y' => '', 'Automatic actions' => '自动动作', 'Your automatic action have been created successfully.' => '您的自动动作已成功创建', 'Unable to create your automatic action.' => '无法为您创建自动动作。', @@ -231,7 +232,7 @@ return array( 'Next step' => '下一步', 'Define action parameters' => '定义动作参数', 'Save this action' => '保存该动作', - 'Do you really want to remove this action: "%s"?' => '确定要移除该动作"%s"吗?', + 'Do you really want to remove this action: "%s"?' => '确定要移除动作"%s"吗?', 'Remove an automatic action' => '移除一个自动动作', 'Close the task' => '关闭任务', 'Assign the task to a specific user' => '将该任务指派给一个用户', @@ -285,7 +286,7 @@ return array( 'Nothing found.' => '没找到。', 'Search in the project "%s"' => '在项目"%s"中查找', 'Due date' => '到期时间', - 'Others formats accepted: %s and %s' => '允许其他格式:%s 和 %s', + 'Others formats accepted: %s and %s' => '可以使用的其它格式:%s 和 %s', 'Description' => '描述', '%d comments' => '%d个评论', '%d comment' => '%d个评论', @@ -323,7 +324,7 @@ return array( 'Category Name' => '分类名称', 'Categories for the project "%s"' => '项目"%s"的分类', 'Add a new category' => '加入新分类', - 'Do you really want to remove this category: "%s"?' => '您确定要移除分类"%s"吗?', + 'Do you really want to remove this category: "%s"?' => '确定要移除分类"%s"吗?', 'Filter by category' => '按分类过滤', 'All categories' => '所有分类', 'No category' => '无分类', @@ -332,7 +333,7 @@ return array( 'Unable to remove this file.' => '无法移除该文件。', 'File removed successfully.' => '文件成功移除。', 'Attach a document' => '附加文档', - 'Do you really want to remove this file: "%s"?' => '您确定要移除文件"%s"吗?', + 'Do you really want to remove this file: "%s"?' => '确定要移除文件"%s"吗?', 'open' => '打开', 'Attachments' => '附件', 'Edit the task' => '修改任务', @@ -375,7 +376,7 @@ return array( 'Login with my GitHub Account' => '用Github账号登录', 'Link my GitHub Account' => '链接GitHub账号', 'Unlink my GitHub Account' => '取消GitHub账号链接', - 'Created by %s' => '创建者:', + 'Created by %s' => '创建者:%s', 'Last modified on %B %e, %Y at %k:%M %p' => '最后修改:%Y/%m/%d/ %H:%M', 'Tasks Export' => '任务导出', 'Tasks exportation for "%s"' => '导出任务"%s"', @@ -468,20 +469,20 @@ return array( 'Unable to change the password.' => '无法修改密码。', 'Change category for the task "%s"' => '变更任务 "%s" 的分类', 'Change category' => '变更分类', - '%s updated the task #%d' => '%s 更新了任务 #%d', - '%s open the task #%d' => '%s 开启了任务 #%d', - '%s moved the task #%d to the position #%d in the column "%s"' => '%s 将任务 #%d 移动到了"%s"的第#%d个位置', - '%s moved the task #%d to the column "%s"' => '%s 移动任务 #%d 到栏目 "%s"', - '%s created the task #%d' => '%s 创建了任务 #%d', - '%s closed the task #%d' => '%s 关闭了任务 #%d', - '%s created a subtask for the task #%d' => '%s 创建了 #%d的子任务', - '%s updated a subtask for the task #%d' => '%s 更新了 #%d的子任务', + '%s updated the task %s' => '%s 更新了任务 %s', + '%s opened the task %s' => '%s 开启了任务 %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s 将任务 %s 移动到了"%s"的第#%d个位置', + '%s moved the task %s to the column "%s"' => '%s 移动任务 %s 到栏目 "%s"', + '%s created the task %s' => '%s 创建了任务 %s', + '%s closed the task %s' => '%s 关闭了任务 %s', + '%s created a subtask for the task %s' => '%s 创建了 %s的子任务', + '%s updated a subtask for the task %s' => '%s 更新了 %s的子任务', 'Assigned to %s with an estimate of %s/%sh' => '分配给 %s,预估需要 %s/%s 小时', 'Not assigned, estimate of %sh' => '未分配,预估需要 %s 小时', - '%s updated a comment on the task #%d' => '%s 更新了任务 #%d的评论', - '%s commented the task #%d' => '%s 评论了任务 #%d', - '%s\'s activity' => '%s的活动', - 'No activity.' => '无活动', + '%s updated a comment on the task %s' => '%s 更新了任务 %s的评论', + '%s commented the task %s' => '%s 评论了任务 %s', + '%s\'s activity' => '%s的动态', + 'No activity.' => '无动态', 'RSS feed' => 'RSS 链接', '%s updated a comment on the task #%d' => '%s 更新了任务 #%d 的评论', '%s commented on the task #%d' => '%s 评论了任务 #%d', @@ -493,12 +494,12 @@ return array( '%s open the task #%d' => '%s 开启了任务 #%d', '%s moved the task #%d to the column "%s"' => '%s 将任务 #%d 移动到栏目 "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s将任务#%d移动到"%s"的第 %d 列', - 'Activity' => '活动', + 'Activity' => '动态', 'Default values are "%s"' => '默认值为 "%s"', 'Default columns for new projects (Comma-separated)' => '新建项目的默认栏目(用逗号分开)', 'Task assignee change' => '任务分配变更', '%s change the assignee of the task #%d to %s' => '%s 将任务 #%d 分配给了 %s', - '%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)', @@ -547,7 +548,7 @@ return array( 'Time estimated' => '预计时间', 'There is nothing assigned to you.' => '无任务指派给你。', 'My tasks' => '我的任务', - 'Activity stream' => '活动流', + 'Activity stream' => '动态记录', 'Dashboard' => '面板', 'Confirmation' => '确认', 'Allow everybody to access to this project' => '允许所有人访问此项目', @@ -555,8 +556,8 @@ return array( 'Webhooks' => '网络钩子', 'API' => '应用程序接口', 'Integration' => '整合', - 'Github webhook' => 'Github 网络钩子', - 'Help on Github webhook' => 'Github 网络钩子帮助', + 'Github webhooks' => 'Github 网络钩子', + 'Help on Github webhooks' => 'Github 网络钩子帮助', 'Create a comment from an external provider' => '从外部创建一个评论', 'Github issue comment created' => '已经创建了Github问题评论', 'Configure' => '配置', @@ -602,4 +603,49 @@ return array( '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"?' => '确定要删除泳道:"%s"?', + 'Inactive swimlanes' => '非活动泳道', + // 'Set project manager' => '', + // 'Set project member' => '', + 'Remove a swimlane' => '删除泳道', + 'Rename' => '重命名', + 'Show default swimlane' => '显示默认泳道', + 'Swimlane modification for the project "%s"' => '项目"%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' => '', ); diff --git a/sources/app/Model/Acl.php b/sources/app/Model/Acl.php index 4a07d11..599ff05 100644 --- a/sources/app/Model/Acl.php +++ b/sources/app/Model/Acl.php @@ -3,7 +3,7 @@ namespace Model; /** - * Acl model + * Access List * * @package model * @author Frederic Guillot @@ -16,36 +16,59 @@ class Acl extends Base * @access private * @var array */ - private $public_actions = array( + private $public_acl = array( 'user' => array('login', 'check', 'google', 'github'), 'task' => array('readonly'), 'board' => array('readonly'), 'project' => array('feed'), - 'webhook' => array('task', 'github'), + 'webhook' => '*', ); /** - * Controllers and actions allowed for regular users + * Controllers and actions for project members * * @access private * @var array */ - private $user_actions = array( - 'app' => array('index', 'preview', 'status'), - 'project' => array('index', 'show', 'exporttasks', 'exportdaily', 'share', 'edit', 'update', 'users', 'remove', 'duplicate', 'disable', 'enable', 'activity', 'search', 'tasks', 'create', 'save'), - 'board' => array('index', 'show', 'save', 'check', 'changeassignee', 'updateassignee', 'changecategory', 'updatecategory', 'movecolumn', 'edit', 'update', 'add', 'confirm', 'remove', 'subtasks', 'togglesubtask', 'attachments', 'comments', 'description'), - 'user' => array('edit', 'forbidden', 'logout', 'show', 'external', 'unlinkgoogle', 'unlinkgithub', 'sessions', 'removesession', 'last', 'notifications', 'password'), - 'comment' => array('create', 'save', 'confirm', 'remove', 'update', 'edit', 'forbidden'), - 'file' => array('create', 'save', 'download', 'confirm', 'remove', 'open', 'image'), - 'subtask' => array('create', 'save', 'edit', 'update', 'confirm', 'remove', 'togglestatus'), - 'task' => array('show', 'create', 'save', 'edit', 'update', 'close', 'open', 'duplicate', 'remove', 'description', 'move', 'copy', 'time'), - 'category' => array('index', 'save', 'edit', 'update', 'confirm', 'remove'), - 'action' => array('index', 'event', 'params', 'create', 'confirm', 'remove'), - 'analytic' => array('tasks', 'users', 'cfd'), + private $member_acl = array( + 'board' => '*', + 'comment' => '*', + 'file' => '*', + 'project' => array('show', 'tasks', 'search', 'activity'), + 'subtask' => '*', + 'task' => '*', ); /** - * Return true if the specified controller/action is allowed according to the given acl + * Controllers and actions for project managers + * + * @access private + * @var array + */ + private $manager_acl = array( + 'action' => '*', + 'analytic' => '*', + 'board' => array('movecolumn', 'edit', 'update', 'add', 'remove'), + 'category' => '*', + 'export' => array('tasks', 'subtasks', 'summary'), + 'project' => array('edit', 'update', 'share', 'integration', 'users', 'alloweverybody', 'allow', 'setowner', 'revoke', 'duplicate', 'disable', 'enable'), + 'swimlane' => '*', + ); + + /** + * Controllers and actions for admins + * + * @access private + * @var array + */ + private $admin_acl = array( + 'user' => array('index', 'create', 'save', 'remove'), + 'config' => '*', + 'project' => array('remove'), + ); + + /** + * Return true if the specified controller/action match the given acl * * @access public * @param array $acl Acl list @@ -53,13 +76,27 @@ class Acl extends Base * @param string $action Action name * @return bool */ - public function isAllowedAction(array $acl, $controller, $action) + public function matchAcl(array $acl, $controller, $action) { - if (isset($acl[$controller])) { - return in_array($action, $acl[$controller]); + $action = strtolower($action); + return isset($acl[$controller]) && $this->hasAction($action, $acl[$controller]); + } + + /** + * Return true if the specified action is inside the list of actions + * + * @access public + * @param string $action Action name + * @param mixed $action Actions list + * @return bool + */ + public function hasAction($action, $actions) + { + if (is_array($actions)) { + return in_array($action, $actions); } - return false; + return $actions === '*'; } /** @@ -72,94 +109,90 @@ class Acl extends Base */ public function isPublicAction($controller, $action) { - return $this->isAllowedAction($this->public_actions, $controller, $action); + return $this->matchAcl($this->public_acl, $controller, $action); } /** - * Return true if the given action is allowed for a regular user + * Return true if the given action is for admins * * @access public * @param string $controller Controller name * @param string $action Action name * @return bool */ - public function isUserAction($controller, $action) + public function isAdminAction($controller, $action) { - return $this->isAllowedAction($this->user_actions, $controller, $action); + return $this->matchAcl($this->admin_acl, $controller, $action); } /** - * Return true if the logged user is admin + * Return true if the given action is for project managers * * @access public + * @param string $controller Controller name + * @param string $action Action name * @return bool */ - public function isAdminUser() + public function isManagerAction($controller, $action) { - return isset($_SESSION['user']['is_admin']) && $_SESSION['user']['is_admin'] === true; + return $this->matchAcl($this->manager_acl, $controller, $action); } /** - * Return true if the logged user is not admin + * Return true if the given action is for project members * * @access public + * @param string $controller Controller name + * @param string $action Action name * @return bool */ - public function isRegularUser() + public function isMemberAction($controller, $action) { - return isset($_SESSION['user']['is_admin']) && $_SESSION['user']['is_admin'] === false; + return $this->matchAcl($this->member_acl, $controller, $action); } /** - * Get the connected user id - * - * @access public - * @return integer - */ - public function getUserId() - { - return isset($_SESSION['user']['id']) ? (int) $_SESSION['user']['id'] : 0; - } - - /** - * Check is the user is connected + * Return true if the visitor is allowed to access to the given page + * We suppose the user already authenticated * * @access public + * @param string $controller Controller name + * @param string $action Action name + * @param integer $project_id Project id * @return bool */ - public function isLogged() + public function isAllowed($controller, $action, $project_id = 0) { - return ! empty($_SESSION['user']); - } - - /** - * Check is the user was authenticated with the RememberMe or set the value - * - * @access public - * @param bool $value Set true if the user use the RememberMe - * @return bool - */ - public function isRememberMe($value = null) - { - if ($value !== null) { - $_SESSION['is_remember_me'] = $value; + // If you are admin you have access to everything + if ($this->userSession->isAdmin()) { + return true; } - return empty($_SESSION['is_remember_me']) ? false : $_SESSION['is_remember_me']; + // If you access to an admin action, your are not allowed + if ($this->isAdminAction($controller, $action)) { + return false; + } + + // Check project manager permissions + if ($this->isManagerAction($controller, $action)) { + return $this->isManagerActionAllowed($project_id); + } + + // Check project member permissions + if ($this->isMemberAction($controller, $action)) { + return $project_id > 0 && $this->projectPermission->isMember($project_id, $this->userSession->getId()); + } + + // Other applications actions are allowed + return true; } - /** - * Check if an action is allowed for the logged user - * - * @access public - * @param string $controller Controller name - * @param string $action Action name - * @return bool - */ - public function isPageAccessAllowed($controller, $action) + public function isManagerActionAllowed($project_id) { - return $this->isPublicAction($controller, $action) || - $this->isAdminUser() || - ($this->isRegularUser() && $this->isUserAction($controller, $action)); + if ($this->userSession->isAdmin()) { + return true; + } + + return $project_id > 0 && $this->projectPermission->isManager($project_id, $this->userSession->getId()); } } diff --git a/sources/app/Model/Action.php b/sources/app/Model/Action.php index f8dbd88..2204ad3 100644 --- a/sources/app/Model/Action.php +++ b/sources/app/Model/Action.php @@ -2,7 +2,8 @@ namespace Model; -use LogicException; +use Integration\GitlabWebhook; +use Integration\GithubWebhook; use SimpleValidator\Validator; use SimpleValidator\Validators; @@ -80,6 +81,9 @@ class Action extends Base GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE => t('Github issue assignee change'), GithubWebhook::EVENT_ISSUE_LABEL_CHANGE => t('Github issue label change'), GithubWebhook::EVENT_ISSUE_COMMENT => t('Github issue comment created'), + GitlabWebhook::EVENT_COMMIT => t('Gitlab commit received'), + GitlabWebhook::EVENT_ISSUE_OPENED => t('Gitlab issue opened'), + GitlabWebhook::EVENT_ISSUE_CLOSED => t('Gitlab issue closed'), ); asort($values); @@ -136,9 +140,17 @@ class Action extends Base public function getAll() { $actions = $this->db->table(self::TABLE)->findAll(); + $params = $this->db->table(self::TABLE_PARAMS)->findAll(); foreach ($actions as &$action) { - $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action['id'])->findAll(); + + $action['params'] = array(); + + foreach ($params as $param) { + if ($param['action_id'] === $action['id']) { + $action['params'][] = $param; + } + } } return $actions; @@ -187,6 +199,7 @@ class Action extends Base */ public function remove($action_id) { + // $this->container['fileCache']->remove('proxy_action_getAll'); return $this->db->table(self::TABLE)->eq('id', $action_id)->remove(); } @@ -230,6 +243,8 @@ class Action extends Base $this->db->closeTransaction(); + // $this->container['fileCache']->remove('proxy_action_getAll'); + return true; } @@ -240,7 +255,10 @@ class Action extends Base */ public function attachEvents() { - foreach ($this->getAll() as $action) { + //$actions = $this->container['fileCache']->proxy('action', 'getAll'); + $actions = $this->getAll(); + + foreach ($actions as $action) { $listener = $this->load($action['action_name'], $action['project_id'], $action['event_name']); @@ -248,7 +266,7 @@ class Action extends Base $listener->setParam($param['name'], $param['value']); } - $this->event->attach($action['event_name'], $listener); + $this->container['dispatcher']->addListener($action['event_name'], array($listener, 'execute')); } } @@ -303,6 +321,8 @@ class Action extends Base } } + // $this->container['fileCache']->remove('proxy_action_getAll'); + return true; } diff --git a/sources/app/Model/Authentication.php b/sources/app/Model/Authentication.php index a0e9684..92898cd 100644 --- a/sources/app/Model/Authentication.php +++ b/sources/app/Model/Authentication.php @@ -3,7 +3,6 @@ namespace Model; use Core\Request; -use Auth\Database; use SimpleValidator\Validator; use SimpleValidator\Validators; @@ -36,19 +35,12 @@ class Authentication extends Base * Check if the current user is authenticated * * @access public - * @param string $controller Controller - * @param string $action Action name * @return bool */ - public function isAuthenticated($controller, $action) + public function isAuthenticated() { - // If the action is public we don't need to do any checks - if ($this->acl->isPublicAction($controller, $action)) { - return true; - } - // If the user is already logged it's ok - if ($this->acl->isLogged()) { + if ($this->userSession->isLogged()) { // We update each time the RememberMe cookie tokens if ($this->backend('rememberMe')->hasCookie()) { @@ -118,7 +110,7 @@ class Authentication extends Base if (! empty($values['remember_me'])) { $credentials = $this->backend('rememberMe') - ->create($this->acl->getUserId(), Request::getIpAddress(), Request::getUserAgent()); + ->create($this->userSession->getId(), Request::getIpAddress(), Request::getUserAgent()); $this->backend('rememberMe')->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']); } diff --git a/sources/app/Model/Base.php b/sources/app/Model/Base.php index 56a4d8e..3f847c2 100644 --- a/sources/app/Model/Base.php +++ b/sources/app/Model/Base.php @@ -2,10 +2,7 @@ namespace Model; -use Core\Event; -use Core\Tool; use Pimple\Container; -use PicoDb\Database; /** * Base model class @@ -13,6 +10,8 @@ use PicoDb\Database; * @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 @@ -30,6 +29,7 @@ use PicoDb\Database; * @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 @@ -39,6 +39,7 @@ use PicoDb\Database; * @property \Model\TaskValidator $taskValidator * @property \Model\TimeTracking $timeTracking * @property \Model\User $user + * @property \Model\UserSession $userSession * @property \Model\Webhook $webhook */ abstract class Base @@ -51,14 +52,6 @@ abstract class Base */ protected $db; - /** - * Event dispatcher instance - * - * @access public - * @var \Core\Event - */ - public $event; - /** * Container instance * @@ -77,7 +70,6 @@ abstract class Base { $this->container = $container; $this->db = $this->container['db']; - $this->event = $this->container['event']; } /** @@ -89,7 +81,7 @@ abstract class Base */ public function __get($name) { - return Tool::loadModel($this->container, $name); + return $this->container[$name]; } /** @@ -117,7 +109,7 @@ abstract class Base * * @access public * @param array $values Input array - * @param array $keys List of keys to remove + * @param string[] $keys List of keys to remove */ public function removeFields(array &$values, array $keys) { @@ -132,8 +124,8 @@ abstract class Base * Force some fields to be at 0 if empty * * @access public - * @param array $values Input array - * @param array $keys List of keys + * @param array $values Input array + * @param string[] $keys List of keys */ public function resetFields(array &$values, array $keys) { @@ -148,8 +140,8 @@ abstract class Base * Force some fields to be integer * * @access public - * @param array $values Input array - * @param array $keys List of keys + * @param array $values Input array + * @param string[] $keys List of keys */ public function convertIntegerFields(array &$values, array $keys) { diff --git a/sources/app/Model/Board.php b/sources/app/Model/Board.php index 9ba2e06..550009f 100644 --- a/sources/app/Model/Board.php +++ b/sources/app/Model/Board.php @@ -24,7 +24,7 @@ class Board extends Base * Get Kanboard default columns * * @access public - * @return array + * @return string[] */ public function getDefaultColumns() { @@ -227,29 +227,47 @@ class Board extends Base } /** - * Get all columns and tasks for a given project + * Get all tasks sorted by columns and swimlanes * * @access public * @param integer $project_id Project id * @return array */ - public function get($project_id) + public function getBoard($project_id) { + $swimlanes = $this->swimlane->getSwimlanes($project_id); $columns = $this->getColumns($project_id); - $tasks = $this->taskFinder->getTasksOnBoard($project_id); + $nb_columns = count($columns); - foreach ($columns as &$column) { + for ($i = 0, $ilen = count($swimlanes); $i < $ilen; $i++) { - $column['tasks'] = array(); + $swimlanes[$i]['columns'] = $columns; + $swimlanes[$i]['nb_columns'] = $nb_columns; - foreach ($tasks as &$task) { - if ($task['column_id'] == $column['id']) { - $column['tasks'][] = $task; - } + 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']); } } - return $columns; + return $swimlanes; + } + + /** + * Get the total of tasks per column + * + * @access public + * @param integer $project_id + * @return array + */ + public function getColumnStats($project_id) + { + return $this->db + ->table(Task::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', 1) + ->groupBy('column_id') + ->listing('column_id', 'COUNT(*) AS total'); } /** diff --git a/sources/app/Model/Category.php b/sources/app/Model/Category.php index 54a0f55..cd60e7f 100644 --- a/sources/app/Model/Category.php +++ b/sources/app/Model/Category.php @@ -118,7 +118,30 @@ class Category extends Base } /** - * Create a category + * Create default cetegories during project creation (transaction already started in Project::create()) + * + * @access public + * @param integer $project_id + */ + public function createDefaultCategories($project_id) + { + $categories = explode(',', $this->config->get('project_categories')); + + foreach ($categories as $category) { + + $category = trim($category); + + if (! empty($category)) { + $this->db->table(self::TABLE)->insert(array( + 'project_id' => $project_id, + 'name' => $category, + )); + } + } + } + + /** + * Create a category (run inside a transaction) * * @access public * @param array $values Form values diff --git a/sources/app/Model/Comment.php b/sources/app/Model/Comment.php index 3b7dfbc..a36f2b4 100644 --- a/sources/app/Model/Comment.php +++ b/sources/app/Model/Comment.php @@ -2,6 +2,7 @@ namespace Model; +use Event\CommentEvent; use SimpleValidator\Validator; use SimpleValidator\Validators; @@ -107,7 +108,7 @@ class Comment extends Base $comment_id = $this->persist(self::TABLE, $values); if ($comment_id) { - $this->event->trigger(self::EVENT_CREATE, array('id' => $comment_id) + $values); + $this->container['dispatcher']->dispatch(self::EVENT_CREATE, new CommentEvent(array('id' => $comment_id) + $values)); } return $comment_id; @@ -127,7 +128,7 @@ class Comment extends Base ->eq('id', $values['id']) ->update(array('comment' => $values['comment'])); - $this->event->trigger(self::EVENT_UPDATE, $values); + $this->container['dispatcher']->dispatch(self::EVENT_UPDATE, new CommentEvent($values)); return $result; } diff --git a/sources/app/Model/Config.php b/sources/app/Model/Config.php index 1ad6dbd..e6d6673 100644 --- a/sources/app/Model/Config.php +++ b/sources/app/Model/Config.php @@ -2,8 +2,6 @@ namespace Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; use Core\Translator; use Core\Security; use Core\Session; @@ -27,30 +25,39 @@ class Config extends Base * Get available timezones * * @access public + * @param boolean $prepend Prepend a default value * @return array */ - public function getTimezones() + public function getTimezones($prepend = false) { $timezones = timezone_identifiers_list(); - return array_combine(array_values($timezones), $timezones); + $listing = array_combine(array_values($timezones), $timezones); + + if ($prepend) { + return array('' => t('Application default')) + $listing; + } + + return $listing; } /** * Get available languages * * @access public + * @param boolean $prepend Prepend a default value * @return array */ - public function getLanguages() + public function getLanguages($prepend = false) { // Sorted by value - return array( + $languages = array( 'da_DK' => 'Dansk', 'de_DE' => 'Deutsch', 'en_US' => 'English', 'es_ES' => 'Español', 'fr_FR' => 'Français', 'it_IT' => 'Italiano', + 'hu_HU' => 'Magyar', 'pl_PL' => 'Polski', 'pt_BR' => 'Português (Brasil)', 'ru_RU' => 'Русский', @@ -60,6 +67,12 @@ class Config extends Base 'ja_JP' => '日本語', 'th_TH' => 'ไทย', ); + + if ($prepend) { + return array('' => t('Application default')) + $languages; + } + + return $languages; } /** @@ -77,12 +90,13 @@ class Config extends Base return $value ?: $default_value; } - if (! isset($_SESSION['config'][$name])) { - $_SESSION['config'] = $this->getAll(); + // Cache config in session + if (! isset($this->session['config'][$name])) { + $this->session['config'] = $this->getAll(); } - if (! empty($_SESSION['config'][$name])) { - return $_SESSION['config'][$name]; + if (! empty($this->session['config'][$name])) { + return $this->session['config'][$name]; } return $default_value; @@ -127,7 +141,7 @@ class Config extends Base */ public function reload() { - $_SESSION['config'] = $this->getAll(); + $this->session['config'] = $this->getAll(); $this->setupTranslations(); } @@ -138,10 +152,11 @@ class Config extends Base */ public function setupTranslations() { - $language = $this->get('application_language', 'en_US'); - - if ($language !== 'en_US') { - Translator::load($language); + if ($this->userSession->isLogged() && ! empty($this->session['user']['language'])) { + Translator::load($this->session['user']['language']); + } + else { + Translator::load($this->get('application_language', 'en_US')); } } @@ -152,7 +167,12 @@ class Config extends Base */ public function setupTimezone() { - date_default_timezone_set($this->get('application_timezone', 'UTC')); + 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')); + } } /** diff --git a/sources/app/Model/DateParser.php b/sources/app/Model/DateParser.php index 38265f9..518a4f3 100644 --- a/sources/app/Model/DateParser.php +++ b/sources/app/Model/DateParser.php @@ -60,7 +60,7 @@ class DateParser extends Base * Return the list of supported date formats (for the parser) * * @access public - * @return array + * @return string[] */ public function getDateFormats() { @@ -103,7 +103,7 @@ class DateParser extends Base * * @access public * @param array $values Database values - * @param array $fields Date fields + * @param string[] $fields Date fields * @param string $format Date format */ public function format(array &$values, array $fields, $format = '') @@ -128,7 +128,7 @@ class DateParser extends Base * * @access public * @param array $values Database values - * @param array $fields Date fields + * @param string[] $fields Date fields */ public function convert(array &$values, array $fields) { diff --git a/sources/app/Model/File.php b/sources/app/Model/File.php index d5a0c7c..20fba9b 100644 --- a/sources/app/Model/File.php +++ b/sources/app/Model/File.php @@ -2,6 +2,8 @@ namespace Model; +use Event\FileEvent; + /** * File model * @@ -89,7 +91,10 @@ class File extends Base */ public function create($task_id, $name, $path, $is_image) { - $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id, 'name' => $name)); + $this->container['dispatcher']->dispatch( + self::EVENT_CREATE, + new FileEvent(array('task_id' => $task_id, 'name' => $name)) + ); return $this->db->table(self::TABLE)->save(array( 'task_id' => $task_id, diff --git a/sources/app/Model/LastLogin.php b/sources/app/Model/LastLogin.php index 3391db5..dd64284 100644 --- a/sources/app/Model/LastLogin.php +++ b/sources/app/Model/LastLogin.php @@ -32,7 +32,7 @@ class LastLogin extends Base * @param integer $user_id User id * @param string $ip IP Address * @param string $user_agent User Agent - * @return array + * @return boolean */ public function create($auth_type, $user_id, $ip, $user_agent) { diff --git a/sources/app/Model/Notification.php b/sources/app/Model/Notification.php index 8d1fca0..95306e8 100644 --- a/sources/app/Model/Notification.php +++ b/sources/app/Model/Notification.php @@ -3,9 +3,6 @@ namespace Model; use Core\Session; -use Core\Translator; -use Core\Template; -use Event\NotificationListener; use Swift_Message; use Swift_Mailer; use Swift_TransportException; @@ -29,8 +26,8 @@ class Notification extends Base * Get a list of people with notifications enabled * * @access public - * @param integer $project_id Project id - * @param array $exlude_users List of user_id to exclude + * @param integer $project_id Project id + * @param array $exclude_users List of user_id to exclude * @return array */ public function getUsersWithNotification($project_id, array $exclude_users = array()) @@ -61,15 +58,15 @@ class Notification extends Base * Get the list of users to send the notification for a given project * * @access public - * @param integer $project_id Project id - * @param array $exlude_users List of user_id to exclude + * @param integer $project_id Project id + * @param array $exclude_users List of user_id to exclude * @return array */ public function getUsersList($project_id, array $exclude_users = array()) { // Exclude the connected user if (Session::isOpen()) { - $exclude_users[] = $this->acl->getUserId(); + $exclude_users[] = $this->userSession->getId(); } $users = $this->getUsersWithNotification($project_id, $exclude_users); @@ -93,37 +90,6 @@ class Notification extends Base return $users; } - /** - * Attach events - * - * @access public - */ - public function attachEvents() - { - $events = array( - Task::EVENT_CREATE => 'task_creation', - Task::EVENT_UPDATE => 'task_update', - Task::EVENT_CLOSE => 'task_close', - Task::EVENT_OPEN => 'task_open', - 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', - Comment::EVENT_CREATE => 'comment_creation', - Comment::EVENT_UPDATE => 'comment_update', - File::EVENT_CREATE => 'file_creation', - ); - - foreach ($events as $event_name => $template_name) { - - $listener = new NotificationListener($this->container); - $listener->setTemplate($template_name); - - $this->event->attach($event_name, $listener); - } - } - /** * Send the email notifications * @@ -148,7 +114,7 @@ class Notification extends Base } } catch (Swift_TransportException $e) { - $this->container['logger']->addError($e->getMessage()); + $this->container['logger']->error($e->getMessage()); } } @@ -217,8 +183,10 @@ class Notification extends Base */ public function getMailContent($template, array $data) { - $tpl = new Template; - return $tpl->load('notification/'.$template, $data + array('application_url' => $this->config->get('application_url'))); + return $this->template->render( + 'notification/'.$template, + $data + array('application_url' => $this->config->get('application_url')) + ); } /** diff --git a/sources/app/Model/Project.php b/sources/app/Model/Project.php index c657e82..f9c5c39 100644 --- a/sources/app/Model/Project.php +++ b/sources/app/Model/Project.php @@ -4,7 +4,6 @@ namespace Model; use SimpleValidator\Validator; use SimpleValidator\Validators; -use Event\ProjectModificationDateListener; use Core\Security; /** @@ -52,12 +51,12 @@ class Project extends Base * Get a project by the name * * @access public - * @param string $project_name Project name + * @param string $name Project name * @return array */ - public function getByName($project_name) + public function getByName($name) { - return $this->db->table(self::TABLE)->eq('name', $project_name)->findOne(); + return $this->db->table(self::TABLE)->eq('name', $name)->findOne(); } /** @@ -110,7 +109,7 @@ class Project extends Base foreach ($projects as $key => $project) { - if (! $this->projectPermission->isUserAllowed($project['id'], $this->acl->getUserId())) { + if (! $this->projectPermission->isUserAllowed($project['id'], $this->userSession->getId())) { unset($projects[$key]); } } @@ -192,11 +191,12 @@ class Project extends Base public function getStats($project_id) { $stats = array(); - $columns = $this->board->getColumns($project_id); $stats['nb_active_tasks'] = 0; + $columns = $this->board->getColumns($project_id); + $column_stats = $this->board->getColumnStats($project_id); foreach ($columns as &$column) { - $column['nb_active_tasks'] = $this->taskFinder->countByColumnId($project_id, $column['id']); + $column['nb_active_tasks'] = isset($column_stats[$column['id']]) ? $column_stats[$column['id']] : 0; $stats['nb_active_tasks'] += $column['nb_active_tasks']; } @@ -228,7 +228,7 @@ class Project extends Base ); if (! $this->db->table(self::TABLE)->save($values)) { - return false; + return 0; } return $this->db->getConnection()->getLastId(); @@ -296,9 +296,11 @@ class Project extends Base } if ($add_user && $user_id) { - $this->projectPermission->allowUser($project_id, $user_id); + $this->projectPermission->addManager($project_id, $user_id); } + $this->category->createDefaultCategories($project_id); + $this->db->closeTransaction(); return (int) $project_id; @@ -489,34 +491,4 @@ class Project extends Base $v->getErrors() ); } - - /** - * Attach events - * - * @access public - */ - public function attachEvents() - { - $events = array( - Task::EVENT_CREATE_UPDATE, - Task::EVENT_CLOSE, - Task::EVENT_OPEN, - Task::EVENT_MOVE_COLUMN, - Task::EVENT_MOVE_POSITION, - Task::EVENT_ASSIGNEE_CHANGE, - GithubWebhook::EVENT_ISSUE_OPENED, - GithubWebhook::EVENT_ISSUE_CLOSED, - GithubWebhook::EVENT_ISSUE_REOPENED, - GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE, - GithubWebhook::EVENT_ISSUE_LABEL_CHANGE, - GithubWebhook::EVENT_ISSUE_COMMENT, - GithubWebhook::EVENT_COMMIT, - ); - - $listener = new ProjectModificationDateListener($this->container); - - foreach ($events as $event_name) { - $this->event->attach($event_name, $listener); - } - } } diff --git a/sources/app/Model/ProjectActivity.php b/sources/app/Model/ProjectActivity.php index 000dfa0..bbcb7f5 100644 --- a/sources/app/Model/ProjectActivity.php +++ b/sources/app/Model/ProjectActivity.php @@ -2,9 +2,6 @@ namespace Model; -use Core\Template; -use Event\ProjectActivityListener; - /** * Project activity model * @@ -25,7 +22,7 @@ class ProjectActivity extends Base * * @var integer */ - const MAX_EVENTS = 5000; + const MAX_EVENTS = 1000; /** * Add a new event for the project @@ -46,7 +43,7 @@ class ProjectActivity extends Base 'creator_id' => $creator_id, 'event_name' => $event_name, 'date_creation' => time(), - 'data' => serialize($data), + 'data' => json_encode($data), ); $this->cleanup(self::MAX_EVENTS - 1); @@ -70,13 +67,13 @@ class ProjectActivity extends Base * Get all events for the given projects list * * @access public - * @param integer $project_id Project id + * @param integer[] $project_ids Projects id * @param integer $limit Maximum events number * @return array */ - public function getProjects(array $projects, $limit = 50) + public function getProjects(array $project_ids, $limit = 50) { - if (empty($projects)) { + if (empty($project_ids)) { return array(); } @@ -86,7 +83,7 @@ class ProjectActivity extends Base User::TABLE.'.username AS author_username', User::TABLE.'.name AS author_name' ) - ->in('project_id', $projects) + ->in('project_id', $project_ids) ->join(User::TABLE, 'id', 'creator_id') ->desc('id') ->limit($limit) @@ -94,7 +91,7 @@ class ProjectActivity extends Base foreach ($events as &$event) { - $event += unserialize($event['data']); + $event += $this->decode($event['data']); unset($event['data']); $event['author'] = $event['author_name'] ?: $event['author_username']; @@ -126,34 +123,6 @@ class ProjectActivity extends Base } } - /** - * Attach events to be able to record the history - * - * @access public - */ - public function attachEvents() - { - $events = array( - Task::EVENT_ASSIGNEE_CHANGE, - Task::EVENT_UPDATE, - Task::EVENT_CREATE, - Task::EVENT_CLOSE, - Task::EVENT_OPEN, - Task::EVENT_MOVE_COLUMN, - Task::EVENT_MOVE_POSITION, - Comment::EVENT_UPDATE, - Comment::EVENT_CREATE, - SubTask::EVENT_UPDATE, - SubTask::EVENT_CREATE, - ); - - $listener = new ProjectActivityListener($this->container); - - foreach ($events as $event_name) { - $this->event->attach($event_name, $listener); - } - } - /** * Get the event html content * @@ -163,8 +132,10 @@ class ProjectActivity extends Base */ public function getContent(array $params) { - $tpl = new Template; - return $tpl->load('event/'.str_replace('.', '_', $params['event_name']), $params); + return $this->template->render( + 'event/'.str_replace('.', '_', $params['event_name']), + $params + ); } /** @@ -203,4 +174,20 @@ class ProjectActivity extends Base return ''; } } + + /** + * Decode event data, supports unserialize() and json_decode() + * + * @access public + * @param string $data Serialized data + * @return array + */ + public function decode($data) + { + if ($data{0} === 'a') { + return unserialize($data); + } + + return json_decode($data, true) ?: array(); + } } diff --git a/sources/app/Model/ProjectDailySummary.php b/sources/app/Model/ProjectDailySummary.php index 0ed3c02..0a06bbd 100644 --- a/sources/app/Model/ProjectDailySummary.php +++ b/sources/app/Model/ProjectDailySummary.php @@ -2,9 +2,6 @@ namespace Model; -use Core\Template; -use Event\ProjectDailySummaryListener; - /** * Project daily summary * @@ -157,25 +154,4 @@ class ProjectDailySummary extends Base return $metrics; } - - /** - * Attach events to be able to record the metrics - * - * @access public - */ - public function attachEvents() - { - $events = array( - Task::EVENT_CREATE, - Task::EVENT_CLOSE, - Task::EVENT_OPEN, - Task::EVENT_MOVE_COLUMN, - ); - - $listener = new ProjectDailySummaryListener($this->container); - - foreach ($events as $event_name) { - $this->event->attach($event_name, $listener); - } - } } diff --git a/sources/app/Model/ProjectPaginator.php b/sources/app/Model/ProjectPaginator.php index 9f1c39f..68b216b 100644 --- a/sources/app/Model/ProjectPaginator.php +++ b/sources/app/Model/ProjectPaginator.php @@ -38,9 +38,10 @@ class ProjectPaginator extends Base 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'] = $this->taskFinder->countByColumnId($project['id'], $column['id']); + $column['nb_tasks'] = isset($stats[$column['id']]) ? $stats[$column['id']] : 0; } } diff --git a/sources/app/Model/ProjectPermission.php b/sources/app/Model/ProjectPermission.php index 8984ef3..02f3b42 100644 --- a/sources/app/Model/ProjectPermission.php +++ b/sources/app/Model/ProjectPermission.php @@ -85,6 +85,27 @@ class ProjectPermission extends Base return $this->user->prepareList($users); } + /** + * Get a list of owners for a project + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getManagers($project_id) + { + $users = $this->db + ->table(self::TABLE) + ->join(User::TABLE, 'id', 'user_id') + ->eq('project_id', $project_id) + ->eq('is_owner', 1) + ->asc('username') + ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name') + ->findAll(); + + return $this->user->prepareList($users); + } + /** * Get allowed and not allowed users for a project * @@ -97,11 +118,13 @@ class ProjectPermission extends Base $users = array( 'allowed' => array(), 'not_allowed' => array(), + 'managers' => array(), ); $all_users = $this->user->getList(); $users['allowed'] = $this->getMembers($project_id); + $users['managers'] = $this->getManagers($project_id); foreach ($all_users as $user_id => $username) { @@ -114,14 +137,14 @@ class ProjectPermission extends Base } /** - * Allow a specific user for a given project + * Add a new project member * * @access public * @param integer $project_id Project id * @param integer $user_id User id * @return bool */ - public function allowUser($project_id, $user_id) + public function addMember($project_id, $user_id) { return $this->db ->table(self::TABLE) @@ -129,14 +152,14 @@ class ProjectPermission extends Base } /** - * Revoke a specific user for a given project + * Remove a member * * @access public * @param integer $project_id Project id * @param integer $user_id User id * @return bool */ - public function revokeUser($project_id, $user_id) + public function revokeMember($project_id, $user_id) { return $this->db ->table(self::TABLE) @@ -145,6 +168,39 @@ class ProjectPermission extends Base ->remove(); } + /** + * Add a project manager + * + * @access public + * @param integer $project_id Project id + * @param integer $user_id User id + * @return bool + */ + public function addManager($project_id, $user_id) + { + return $this->db + ->table(self::TABLE) + ->save(array('project_id' => $project_id, 'user_id' => $user_id, 'is_owner' => 1)); + } + + /** + * Change the role of a member + * + * @access public + * @param integer $project_id Project id + * @param integer $user_id User id + * @param integer $is_owner Is user owner of the project + * @return bool + */ + public function changeRole($project_id, $user_id, $is_owner) + { + return $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('user_id', $user_id) + ->update(array('is_owner' => (int) $is_owner)); + } + /** * Check if a specific user is member of a project * @@ -159,11 +215,29 @@ class ProjectPermission extends Base return true; } - return (bool) $this->db + return $this->db ->table(self::TABLE) ->eq('project_id', $project_id) ->eq('user_id', $user_id) - ->count(); + ->count() === 1; + } + + /** + * Check if a specific user is manager of a given project + * + * @access public + * @param integer $project_id Project id + * @param integer $user_id User id + * @return bool + */ + public function isManager($project_id, $user_id) + { + return $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('user_id', $user_id) + ->eq('is_owner', 1) + ->count() === 1; } /** @@ -188,28 +262,11 @@ class ProjectPermission extends Base */ public function isEverybodyAllowed($project_id) { - return (bool) $this->db + return $this->db ->table(Project::TABLE) ->eq('id', $project_id) ->eq('is_everybody_allowed', 1) - ->count(); - } - - /** - * Check if a specific user is allowed to manage a project - * - * @access public - * @param integer $project_id Project id - * @param integer $user_id User id - * @return bool - */ - public function adminAllowed($project_id, $user_id) - { - if ($this->isUserAllowed($project_id, $user_id) && $this->project->isPrivate($project_id)) { - return true; - } - - return false; + ->count() === 1; } /** @@ -241,7 +298,11 @@ class ProjectPermission extends Base */ public function getAllowedProjects($user_id) { - return $this->filterProjects($this->project->getListByStatus(Project::ACTIVE), $user_id, 'isUserAllowed'); + if ($this->user->isAdmin($user_id)) { + return $this->project->getListByStatus(Project::ACTIVE); + } + + return $this->getMemberProjects($user_id); } /** @@ -253,23 +314,39 @@ class ProjectPermission extends Base */ public function getMemberProjects($user_id) { - return $this->filterProjects($this->project->getListByStatus(Project::ACTIVE), $user_id, 'isMember'); + return $this->db + ->table(Project::TABLE) + ->eq('user_id', $user_id) + ->join(self::TABLE, 'project_id', 'id') + ->listing('projects.id', 'name'); } /** * Copy user access from a project to another one * - * @author Antonio Rabelo - * @param integer $project_from Project Template - * @return integer $project_to Project that receives the copy + * @param integer $project_src Project Template + * @return integer $project_dst Project that receives the copy * @return boolean */ - public function duplicate($project_from, $project_to) + public function duplicate($project_src, $project_dst) { - $users = $this->getMembers($project_from); + $rows = $this->db + ->table(self::TABLE) + ->columns('project_id', 'user_id', 'is_owner') + ->eq('project_id', $project_src) + ->findAll(); - foreach ($users as $user_id => $name) { - if (! $this->allowUser($project_to, $user_id)) { + foreach ($rows as $row) { + + $result = $this->db + ->table(self::TABLE) + ->save(array( + 'project_id' => $project_dst, + 'user_id' => $row['user_id'], + 'is_owner' => (int) $row['is_owner'], // (int) for postgres + )); + + if (! $result) { return false; } } @@ -291,6 +368,7 @@ class ProjectPermission extends Base new Validators\Integer('project_id', t('This value must be an integer')), new Validators\Required('user_id', t('The user id is required')), new Validators\Integer('user_id', t('This value must be an integer')), + new Validators\Integer('is_owner', t('This value must be an integer')), )); return array( diff --git a/sources/app/Model/SubTask.php b/sources/app/Model/SubTask.php index f301ad6..1c5d1bf 100644 --- a/sources/app/Model/SubTask.php +++ b/sources/app/Model/SubTask.php @@ -2,6 +2,7 @@ namespace Model; +use Event\SubtaskEvent; use SimpleValidator\Validator; use SimpleValidator\Validators; @@ -57,15 +58,11 @@ class SubTask extends Base */ public function getStatusList() { - $status = array( + return array( self::STATUS_TODO => t('Todo'), self::STATUS_INPROGRESS => t('In progress'), self::STATUS_DONE => t('Done'), ); - - asort($status); - - return $status; } /** @@ -146,7 +143,10 @@ class SubTask extends Base $subtask_id = $this->persist(self::TABLE, $values); if ($subtask_id) { - $this->event->trigger(self::EVENT_CREATE, array('id' => $subtask_id) + $values); + $this->container['dispatcher']->dispatch( + self::EVENT_CREATE, + new SubtaskEvent(array('id' => $subtask_id) + $values) + ); } return $subtask_id; @@ -165,7 +165,10 @@ class SubTask extends Base $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); if ($result) { - $this->event->trigger(self::EVENT_UPDATE, $values); + $this->container['dispatcher']->dispatch( + self::EVENT_UPDATE, + new SubtaskEvent($values) + ); } return $result; @@ -220,6 +223,7 @@ class SubTask extends Base $subtasks = $db->table(SubTask::TABLE) ->columns('title', 'time_estimated') ->eq('task_id', $src_task_id) + ->asc('id') // Explicit sorting for postgresql ->findAll(); foreach ($subtasks as &$subtask) { diff --git a/sources/app/Model/SubtaskExport.php b/sources/app/Model/SubtaskExport.php new file mode 100644 index 0000000..50b028e --- /dev/null +++ b/sources/app/Model/SubtaskExport.php @@ -0,0 +1,119 @@ +subtask_status = $this->subTask->getStatusList(); + $subtasks = $this->getSubtasks($project_id, $from, $to); + $results = array($this->getColumns()); + + foreach ($subtasks as $subtask) { + $results[] = $this->format($subtask); + } + + return $results; + } + + /** + * Get column titles + * + * @access public + * @return string[] + */ + public function getColumns() + { + return array( + e('Subtask Id'), + e('Title'), + e('Status'), + e('Assignee'), + e('Time estimated'), + e('Time spent'), + e('Task Id'), + e('Task Title'), + ); + } + + /** + * Format the output of a subtask array + * + * @access public + * @param array $subtask Subtask properties + * @return array + */ + public function format(array $subtask) + { + $values = array(); + $values[] = $subtask['id']; + $values[] = $subtask['title']; + $values[] = $this->subtask_status[$subtask['status']]; + $values[] = $subtask['assignee_name'] ?: $subtask['assignee_username']; + $values[] = $subtask['time_estimated']; + $values[] = $subtask['time_spent']; + $values[] = $subtask['task_id']; + $values[] = $subtask['task_title']; + + return $values; + } + + /** + * 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) + * @return array + */ + public function getSubtasks($project_id, $from, $to) + { + if (! is_numeric($from)) { + $from = $this->dateParser->resetDateToMidnight($this->dateParser->getTimestamp($from)); + } + + if (! is_numeric($to)) { + $to = $this->dateParser->resetDateToMidnight(strtotime('+1 day', $this->dateParser->getTimestamp($to))); + } + + return $this->db->table(SubTask::TABLE) + ->eq('project_id', $project_id) + ->columns( + SubTask::TABLE.'.*', + User::TABLE.'.username AS assignee_username', + User::TABLE.'.name AS assignee_name', + Task::TABLE.'.title AS task_title' + ) + ->gte('date_creation', $from) + ->lte('date_creation', $to) + ->join(Task::TABLE, 'id', 'task_id') + ->join(User::TABLE, 'id', 'user_id') + ->asc(SubTask::TABLE.'.id') + ->findAll(); + } +} diff --git a/sources/app/Model/Swimlane.php b/sources/app/Model/Swimlane.php new file mode 100644 index 0000000..069f14b --- /dev/null +++ b/sources/app/Model/Swimlane.php @@ -0,0 +1,495 @@ +db->table(self::TABLE)->eq('id', $swimlane_id)->findOne(); + } + + /** + * Get the swimlane name by the id + * + * @access public + * @param integer $swimlane_id Swimlane id + * @return string + */ + public function getNameById($swimlane_id) + { + return $this->db->table(self::TABLE)->eq('id', $swimlane_id)->findOneColumn('name') ?: ''; + } + + /** + * Get a swimlane id by the project and the name + * + * @access public + * @param integer $project_id Project id + * @param string $name Name + * @return integer + */ + public function getIdByName($project_id, $name) + { + return (int) $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('name', $name) + ->findOneColumn('id'); + } + + /** + * Get default swimlane properties + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getDefault($project_id) + { + return $this->db->table(Project::TABLE) + ->eq('id', $project_id) + ->columns('id', 'default_swimlane', 'show_default_swimlane') + ->findOne(); + } + + /** + * Get all swimlanes for a given project + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getAll($project_id) + { + return $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->orderBy('position', 'asc') + ->findAll(); + } + + /** + * Get the list of swimlanes by status + * + * @access public + * @param integer $project_id Project id + * @param integer $status Status + * @return array + */ + public function getAllByStatus($project_id, $status = self::ACTIVE) + { + $query = $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', $status); + + if ($status == self::ACTIVE) { + $query->asc('position'); + } + else { + $query->asc('name'); + } + + return $query->findAll(); + } + + /** + * Get active swimlanes + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getSwimlanes($project_id) + { + $swimlanes = $this->db->table(self::TABLE) + ->columns('id', 'name') + ->eq('project_id', $project_id) + ->eq('is_active', self::ACTIVE) + ->orderBy('position', 'asc') + ->findAll(); + + $default_swimlane = $this->db->table(Project::TABLE) + ->eq('id', $project_id) + ->eq('show_default_swimlane', 1) + ->findOneColumn('default_swimlane'); + + if ($default_swimlane) { + array_unshift($swimlanes, array('id' => 0, 'name' => $default_swimlane)); + } + + return $swimlanes; + } + + /** + * Get list of all swimlanes + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getSwimlanesList($project_id) + { + $swimlanes = $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->orderBy('position', 'asc') + ->listing('id', 'name'); + + $swimlanes[0] = $this->db->table(Project::TABLE) + ->eq('id', $project_id) + ->findOneColumn('default_swimlane'); + + return $swimlanes; + } + + /** + * Add a new swimlane + * + * @access public + * @param integer $project_id + * @param string $name + * @return bool + */ + public function create($project_id, $name) + { + return $this->persist(self::TABLE, array( + 'project_id' => $project_id, + 'name' => $name, + 'position' => $this->getLastPosition($project_id), + )); + } + + /** + * Rename a swimlane + * + * @access public + * @param integer $swimlane_id Swimlane id + * @param string $name Swimlane name + * @return bool + */ + public function rename($swimlane_id, $name) + { + return $this->db->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array('name' => $name)); + } + + /** + * Update the default swimlane + * + * @access public + * @param array $values Form values + * @return bool + */ + public function updateDefault(array $values) + { + return $this->db + ->table(Project::TABLE) + ->eq('id', $values['id']) + ->update(array( + 'default_swimlane' => $values['default_swimlane'], + 'show_default_swimlane' => $values['show_default_swimlane'], + )); + } + + /** + * Get the last position of a swimlane + * + * @access public + * @param integer $project_id + * @return integer + */ + public function getLastPosition($project_id) + { + return $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', 1) + ->count() + 1; + } + + /** + * Disable a swimlane + * + * @access public + * @param integer $project_id Project id + * @param integer $swimlane_id Swimlane id + * @return bool + */ + public function disable($project_id, $swimlane_id) + { + $result = $this->db + ->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array( + 'is_active' => self::INACTIVE, + 'position' => 0, + )); + + if ($result) { + // Re-order positions + $this->updatePositions($project_id); + } + + return $result; + } + + /** + * Enable a swimlane + * + * @access public + * @param integer $project_id Project id + * @param integer $swimlane_id Swimlane id + * @return bool + */ + public function enable($project_id, $swimlane_id) + { + return $this->db + ->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array( + 'is_active' => self::ACTIVE, + 'position' => $this->getLastPosition($project_id), + )); + } + + /** + * Remove a swimlane + * + * @access public + * @param integer $project_id Project id + * @param integer $swimlane_id Swimlane id + * @return bool + */ + public function remove($project_id, $swimlane_id) + { + $this->db->startTransaction(); + + // Tasks should not be assigned anymore to this swimlane + $this->db->table(Task::TABLE)->eq('swimlane_id', $swimlane_id)->update(array('swimlane_id' => 0)); + + if (! $this->db->table(self::TABLE)->eq('id', $swimlane_id)->remove()) { + $this->db->cancelTransaction(); + return false; + } + + // Re-order positions + $this->updatePositions($project_id); + + $this->db->closeTransaction(); + + return true; + } + + /** + * Update swimlane positions after disabling or removing a swimlane + * + * @access public + * @param integer $project_id Project id + * @return boolean + */ + public function updatePositions($project_id) + { + $position = 0; + $swimlanes = $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', 1) + ->asc('position') + ->findAllByColumn('id'); + + if (! $swimlanes) { + return false; + } + + foreach ($swimlanes as $swimlane_id) { + $this->db->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array('position' => ++$position)); + } + + return true; + } + + /** + * Move a swimlane down, increment the position value + * + * @access public + * @param integer $project_id Project id + * @param integer $swimlane_id Swimlane id + * @return boolean + */ + public function moveDown($project_id, $swimlane_id) + { + $swimlanes = $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', self::ACTIVE) + ->asc('position') + ->listing('id', 'position'); + + $positions = array_flip($swimlanes); + + if (isset($swimlanes[$swimlane_id]) && $swimlanes[$swimlane_id] < count($swimlanes)) { + + $position = ++$swimlanes[$swimlane_id]; + $swimlanes[$positions[$position]]--; + + $this->db->startTransaction(); + $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position)); + $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $swimlanes[$positions[$position]])); + $this->db->closeTransaction(); + + return true; + } + + return false; + } + + /** + * Move a swimlane up, decrement the position value + * + * @access public + * @param integer $project_id Project id + * @param integer $swimlane_id Swimlane id + * @return boolean + */ + public function moveUp($project_id, $swimlane_id) + { + $swimlanes = $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', self::ACTIVE) + ->asc('position') + ->listing('id', 'position'); + + $positions = array_flip($swimlanes); + + if (isset($swimlanes[$swimlane_id]) && $swimlanes[$swimlane_id] > 1) { + + $position = --$swimlanes[$swimlane_id]; + $swimlanes[$positions[$position]]++; + + $this->db->startTransaction(); + $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position)); + $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $swimlanes[$positions[$position]])); + $this->db->closeTransaction(); + + return true; + } + + return false; + } + + /** + * 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) + { + $rules = array( + new Validators\Required('project_id', t('The project id is required')), + new Validators\Required('name', t('The name is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + 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) + { + $rules = array( + new Validators\Required('id', t('The id is required')), + new Validators\Required('name', t('The name is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate default swimlane modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateDefaultModification(array $values) + { + $rules = array( + new Validators\Required('id', t('The id is required')), + new Validators\Required('default_swimlane', t('The name is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Common validation rules + * + * @access private + * @return array + */ + private function commonValidationRules() + { + return array( + new Validators\Integer('id', t('The id must be an integer')), + new Validators\Integer('project_id', t('The project id must be an integer')), + new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) + ); + } +} diff --git a/sources/app/Model/Task.php b/sources/app/Model/Task.php index a745f30..bc2913e 100644 --- a/sources/app/Model/Task.php +++ b/sources/app/Model/Task.php @@ -30,8 +30,10 @@ class Task extends Base * * @var string */ + const EVENT_MOVE_PROJECT = 'task.move.project'; const EVENT_MOVE_COLUMN = 'task.move.column'; const EVENT_MOVE_POSITION = 'task.move.position'; + const EVENT_MOVE_SWIMLANE = 'task.move.swimlane'; const EVENT_UPDATE = 'task.update'; const EVENT_CREATE = 'task.create'; const EVENT_CLOSE = 'task.close'; diff --git a/sources/app/Model/TaskCreation.php b/sources/app/Model/TaskCreation.php index 320bcb9..17e5ff7 100644 --- a/sources/app/Model/TaskCreation.php +++ b/sources/app/Model/TaskCreation.php @@ -2,6 +2,8 @@ namespace Model; +use Event\TaskEvent; + /** * Task Creation * @@ -19,10 +21,14 @@ class TaskCreation extends Base */ public function create(array $values) { + if (! $this->project->exists($values['project_id'])) { + return 0; + } + $this->prepare($values); $task_id = $this->persist(Task::TABLE, $values); - if ($task_id) { + if ($task_id !== false) { $this->fireEvents($task_id, $values); } @@ -39,7 +45,7 @@ class TaskCreation extends Base { $this->dateParser->convert($values, array('date_due', 'date_started')); $this->removeFields($values, array('another_task')); - $this->resetFields($values, array('owner_id', 'owner_id', 'date_due', 'score', 'category_id', 'time_estimated')); + $this->resetFields($values, array('owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated')); if (empty($values['column_id'])) { $values['column_id'] = $this->board->getFirstColumn($values['project_id']); @@ -49,9 +55,14 @@ class TaskCreation extends Base $values['color_id'] = $this->color->getDefaultColor(); } + if (empty($values['title'])) { + $values['title'] = t('Untitled'); + } + + $values['swimlane_id'] = empty($values['swimlane_id']) ? 0 : $values['swimlane_id']; $values['date_creation'] = time(); $values['date_modification'] = $values['date_creation']; - $values['position'] = $this->taskFinder->countByColumnId($values['project_id'], $values['column_id']) + 1; + $values['position'] = $this->taskFinder->countByColumnAndSwimlaneId($values['project_id'], $values['column_id'], $values['swimlane_id']) + 1; } /** @@ -64,7 +75,7 @@ class TaskCreation extends Base private function fireEvents($task_id, array $values) { $values['task_id'] = $task_id; - $this->event->trigger(Task::EVENT_CREATE_UPDATE, $values); - $this->event->trigger(Task::EVENT_CREATE, $values); + $this->container['dispatcher']->dispatch(Task::EVENT_CREATE_UPDATE, new TaskEvent($values)); + $this->container['dispatcher']->dispatch(Task::EVENT_CREATE, new TaskEvent($values)); } } diff --git a/sources/app/Model/TaskDuplication.php b/sources/app/Model/TaskDuplication.php index ab7a57f..172edb9 100644 --- a/sources/app/Model/TaskDuplication.php +++ b/sources/app/Model/TaskDuplication.php @@ -2,6 +2,8 @@ namespace Model; +use Event\TaskEvent; + /** * Task Duplication * @@ -27,6 +29,7 @@ class TaskDuplication extends Base 'score', 'category_id', 'time_estimated', + 'swimlane_id', ); /** @@ -79,10 +82,18 @@ class TaskDuplication extends Base $values['position'] = $this->taskFinder->countByColumnId($project_id, $values['column_id']) + 1; $values['owner_id'] = $task['owner_id']; $values['category_id'] = $task['category_id']; + $values['swimlane_id'] = $task['swimlane_id']; $this->checkDestinationProjectValues($values); - return $this->db->table(Task::TABLE)->eq('id', $task['id'])->update($values); + if ($this->db->table(Task::TABLE)->eq('id', $task['id'])->update($values)) { + $this->container['dispatcher']->dispatch( + Task::EVENT_MOVE_PROJECT, + new TaskEvent(array_merge($task, $values, array('task_id' => $task['id']))) + ); + } + + return true; } /** @@ -100,8 +111,18 @@ class TaskDuplication extends Base // Check if the category exists for the destination project if ($values['category_id'] > 0) { - $category_name = $this->category->getNameById($values['category_id']); - $values['category_id'] = $this->category->getIdByName($values['project_id'], $category_name); + $values['category_id'] = $this->category->getIdByName( + $values['project_id'], + $this->category->getNameById($values['category_id']) + ); + } + + // Check if the swimlane exists for the destination project + if ($values['swimlane_id'] > 0) { + $values['swimlane_id'] = $this->swimlane->getIdByName( + $values['project_id'], + $this->swimlane->getNameById($values['swimlane_id']) + ); } } diff --git a/sources/app/Model/TaskExport.php b/sources/app/Model/TaskExport.php index b929823..1592deb 100644 --- a/sources/app/Model/TaskExport.php +++ b/sources/app/Model/TaskExport.php @@ -24,10 +24,11 @@ class TaskExport extends Base public function export($project_id, $from, $to) { $tasks = $this->getTasks($project_id, $from, $to); + $swimlanes = $this->swimlane->getSwimlanesList($project_id); $results = array($this->getColumns()); foreach ($tasks as &$task) { - $results[] = array_values($this->format($task)); + $results[] = array_values($this->format($task, $swimlanes)); } return $results; @@ -50,6 +51,7 @@ class TaskExport extends Base projects.name AS project_name, tasks.is_active, project_has_categories.name AS category_name, + tasks.swimlane_id, columns.title AS column_title, tasks.position, tasks.color_id, @@ -71,6 +73,7 @@ class TaskExport extends Base LEFT JOIN columns ON columns.id = tasks.column_id LEFT JOIN projects ON projects.id = tasks.project_id WHERE tasks.date_creation >= ? AND tasks.date_creation <= ? AND tasks.project_id = ? + ORDER BY tasks.id ASC '; if (! is_numeric($from)) { @@ -89,15 +92,18 @@ class TaskExport extends Base * Format the output of a task array * * @access public - * @param array $task Task properties + * @param array $task Task properties + * @param array $swimlanes List of swimlanes * @return array */ - public function format(array &$task) + public function format(array &$task, array &$swimlanes) { $colors = $this->color->getList(); $task['is_active'] = $task['is_active'] == Task::STATUS_OPEN ? e('Open') : e('Closed'); $task['color_id'] = $colors[$task['color_id']]; + $task['score'] = $task['score'] ?: 0; + $task['swimlane_id'] = isset($swimlanes[$task['swimlane_id']]) ? $swimlanes[$task['swimlane_id']] : '?'; $this->dateParser->format($task, array('date_due', 'date_modification', 'date_creation', 'date_started', 'date_completed'), 'Y-m-d'); @@ -108,7 +114,7 @@ class TaskExport extends Base * Get column titles * * @access public - * @return array + * @return string[] */ public function getColumns() { @@ -117,6 +123,7 @@ class TaskExport extends Base e('Project'), e('Status'), e('Category'), + e('Swimlane'), e('Column'), e('Position'), e('Color'), diff --git a/sources/app/Model/TaskFinder.php b/sources/app/Model/TaskFinder.php index 0e58102..eb86fe3 100644 --- a/sources/app/Model/TaskFinder.php +++ b/sources/app/Model/TaskFinder.php @@ -38,6 +38,7 @@ class TaskFinder extends Base 'tasks.color_id', 'tasks.project_id', 'tasks.column_id', + 'tasks.swimlane_id', 'tasks.owner_id', 'tasks.creator_id', 'tasks.position', @@ -54,13 +55,17 @@ class TaskFinder extends Base * Get all tasks shown on the board (sorted by position) * * @access public - * @param integer $project_id Project id + * @param integer $project_id Project id + * @param integer $column_id Column id + * @param integer $swimlane_id Swimlane id * @return array */ - public function getTasksOnBoard($project_id) + public function getTasksByColumnAndSwimlane($project_id, $column_id, $swimlane_id = 0) { return $this->getQuery() ->eq('project_id', $project_id) + ->eq('column_id', $column_id) + ->eq('swimlane_id', $swimlane_id) ->eq('is_active', Task::STATUS_OPEN) ->asc('tasks.position') ->findAll(); @@ -167,6 +172,7 @@ class TaskFinder extends Base tasks.is_active, tasks.score, tasks.category_id, + tasks.swimlane_id, project_has_categories.name AS category_name, projects.name AS project_name, columns.title AS column_title, @@ -210,16 +216,35 @@ class TaskFinder extends Base * @access public * @param integer $project_id Project id * @param integer $column_id Column id - * @param array $status List of status id * @return integer */ - public function countByColumnId($project_id, $column_id, array $status = array(Task::STATUS_OPEN)) + public function countByColumnId($project_id, $column_id) { return $this->db ->table(Task::TABLE) ->eq('project_id', $project_id) ->eq('column_id', $column_id) - ->in('is_active', $status) + ->in('is_active', 1) + ->count(); + } + + /** + * Count the number of tasks for a given column and swimlane + * + * @access public + * @param integer $project_id Project id + * @param integer $column_id Column id + * @param integer $swimlane_id Swimlane id + * @return integer + */ + public function countByColumnAndSwimlaneId($project_id, $column_id, $swimlane_id) + { + return $this->db + ->table(Task::TABLE) + ->eq('project_id', $project_id) + ->eq('column_id', $column_id) + ->eq('swimlane_id', $swimlane_id) + ->in('is_active', 1) ->count(); } diff --git a/sources/app/Model/TaskModification.php b/sources/app/Model/TaskModification.php index b165ea2..dac5233 100644 --- a/sources/app/Model/TaskModification.php +++ b/sources/app/Model/TaskModification.php @@ -2,6 +2,8 @@ namespace Model; +use Event\TaskEvent; + /** * Task Modification * @@ -15,17 +17,16 @@ class TaskModification extends Base * * @access public * @param array $values - * @param boolean $fire_events * @return boolean */ - public function update(array $values, $fire_events = true) + public function update(array $values) { $original_task = $this->taskFinder->getById($values['id']); $this->prepare($values); $result = $this->db->table(Task::TABLE)->eq('id', $original_task['id'])->update($values); - if ($result && $fire_events) { + if ($result) { $this->fireEvents($original_task, $values); } @@ -51,7 +52,7 @@ class TaskModification extends Base } foreach ($events as $event) { - $this->event->trigger($event, $event_data); + $this->container['dispatcher']->dispatch($event, new TaskEvent($event_data)); } } diff --git a/sources/app/Model/TaskPaginator.php b/sources/app/Model/TaskPaginator.php index 4ae3566..e810922 100644 --- a/sources/app/Model/TaskPaginator.php +++ b/sources/app/Model/TaskPaginator.php @@ -26,7 +26,7 @@ class TaskPaginator extends Base { return $this->taskFinder->getQuery() ->eq('project_id', $project_id) - ->like('title', '%'.$search.'%') + ->ilike('title', '%'.$search.'%') ->offset($offset) ->limit($limit) ->orderBy($column, $direction) @@ -45,7 +45,7 @@ class TaskPaginator extends Base { return $this->db->table(Task::TABLE) ->eq('project_id', $project_id) - ->like('title', '%'.$search.'%') + ->ilike('title', '%'.$search.'%') ->count(); } @@ -76,7 +76,6 @@ class TaskPaginator extends Base * * @access public * @param integer $project_id Project id - * @param array $status List of status id * @return integer */ public function countClosedTasks($project_id) diff --git a/sources/app/Model/TaskPermission.php b/sources/app/Model/TaskPermission.php index 2ab154f..e2420e1 100644 --- a/sources/app/Model/TaskPermission.php +++ b/sources/app/Model/TaskPermission.php @@ -20,10 +20,10 @@ class TaskPermission extends Base */ public function canRemoveTask(array $task) { - if ($this->acl->isAdminUser()) { + if ($this->userSession->isAdmin() || $this->projectPermission->isManager($task['project_id'], $this->userSession->getId())) { return true; } - else if (isset($task['creator_id']) && $task['creator_id'] == $this->acl->getUserId()) { + else if (isset($task['creator_id']) && $task['creator_id'] == $this->userSession->getId()) { return true; } diff --git a/sources/app/Model/TaskPosition.php b/sources/app/Model/TaskPosition.php index c23bc3b..2c271de 100644 --- a/sources/app/Model/TaskPosition.php +++ b/sources/app/Model/TaskPosition.php @@ -2,6 +2,8 @@ namespace Model; +use Event\TaskEvent; + /** * Task Position * @@ -18,20 +20,25 @@ class TaskPosition extends Base * @param integer $task_id Task id * @param integer $column_id Column id * @param integer $position Position (must be >= 1) + * @param integer $swimlane_id Swimlane id * @return boolean */ - public function movePosition($project_id, $task_id, $column_id, $position) + public function movePosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0) { $original_task = $this->taskFinder->getById($task_id); - $positions = $this->calculatePositions($project_id, $task_id, $column_id, $position); - if ($positions === false || ! $this->savePositions($positions)) { - return false; + $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']); + } + + $this->fireEvents($original_task, $column_id, $position, $swimlane_id); } - $this->fireEvents($original_task, $column_id, $position); - - return true; + return $result; } /** @@ -42,9 +49,10 @@ class TaskPosition extends Base * @param integer $task_id Task id * @param integer $column_id Column id * @param integer $position Position (must be >= 1) + * @param integer $swimlane_id Swimlane id * @return array|boolean */ - public function calculatePositions($project_id, $task_id, $column_id, $position) + public function calculatePositions($project_id, $task_id, $column_id, $position, $swimlane_id = 0) { // The position can't be lower than 1 if ($position < 1) { @@ -59,10 +67,12 @@ class TaskPosition extends Base $columns[$board_column_id] = $this->db->table(Task::TABLE) ->eq('is_active', 1) + ->eq('swimlane_id', $swimlane_id) ->eq('project_id', $project_id) ->eq('column_id', $board_column_id) ->neq('id', $task_id) ->asc('position') + ->asc('id') // Fix Postgresql unit test ->findAllByColumn('id'); } @@ -72,7 +82,9 @@ class TaskPosition extends Base } // We put our task to the new position - array_splice($columns[$column_id], $position - 1, 0, $task_id); + if ($task_id) { + array_splice($columns[$column_id], $position - 1, 0, $task_id); + } return $columns; } @@ -82,11 +94,12 @@ class TaskPosition extends Base * * @access private * @param array $columns Sorted tasks + * @param integer $swimlane_id Swimlane id * @return boolean */ - private function savePositions(array $columns) + private function savePositions(array $columns, $swimlane_id) { - return $this->db->transaction(function ($db) use ($columns) { + return $this->db->transaction(function ($db) use ($columns, $swimlane_id) { foreach ($columns as $column_id => $column) { @@ -96,7 +109,8 @@ class TaskPosition extends Base $result = $db->table(Task::TABLE)->eq('id', $task_id)->update(array( 'position' => $position, - 'column_id' => $column_id + 'column_id' => $column_id, + 'swimlane_id' => $swimlane_id, )); if (! $result) { @@ -112,25 +126,52 @@ class TaskPosition extends Base /** * Fire events * - * @access public + * @access private * @param array $task * @param integer $new_column_id * @param integer $new_position + * @param integer $new_swimlane_id */ - public function fireEvents(array $task, $new_column_id, $new_position) + private function fireEvents(array $task, $new_column_id, $new_position, $new_swimlane_id) { $event_data = array( 'task_id' => $task['id'], 'project_id' => $task['project_id'], 'position' => $new_position, 'column_id' => $new_column_id, + 'swimlane_id' => $new_swimlane_id, ); - if ($task['column_id'] != $new_column_id) { - $this->event->trigger(Task::EVENT_MOVE_COLUMN, $event_data); + if ($task['swimlane_id'] != $new_swimlane_id) { + $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data)); + } + else if ($task['column_id'] != $new_column_id) { + $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_COLUMN, new TaskEvent($event_data)); } else if ($task['position'] != $new_position) { - $this->event->trigger(Task::EVENT_MOVE_POSITION, $event_data); + $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_POSITION, new TaskEvent($event_data)); } } + + /** + * Calculate the new position of all tasks + * + * @access private + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param integer $column_id Column id + * @param integer $position Position (must be >= 1) + * @param integer $swimlane_id Swimlane id + * @return boolean + */ + private function calculateAndSave($project_id, $task_id, $column_id, $position, $swimlane_id) + { + $positions = $this->calculatePositions($project_id, $task_id, $column_id, $position, $swimlane_id); + + if ($positions === false || ! $this->savePositions($positions, $swimlane_id)) { + return false; + } + + return true; + } } diff --git a/sources/app/Model/TaskStatus.php b/sources/app/Model/TaskStatus.php index 99faffd..225b393 100644 --- a/sources/app/Model/TaskStatus.php +++ b/sources/app/Model/TaskStatus.php @@ -2,6 +2,8 @@ namespace Model; +use Event\TaskEvent; + /** * Task Status * @@ -84,9 +86,9 @@ class TaskStatus extends Base )); if ($result) { - $this->event->trigger( + $this->container['dispatcher']->dispatch( $event, - array('task_id' => $task_id) + $this->taskFinder->getById($task_id) + new TaskEvent(array('task_id' => $task_id) + $this->taskFinder->getById($task_id)) ); } diff --git a/sources/app/Model/TaskValidator.php b/sources/app/Model/TaskValidator.php index ecaf090..ae21ca2 100644 --- a/sources/app/Model/TaskValidator.php +++ b/sources/app/Model/TaskValidator.php @@ -29,6 +29,7 @@ class TaskValidator extends Base new Validators\Integer('creator_id', t('This value must be an integer')), new Validators\Integer('score', t('This value must be an integer')), new Validators\Integer('category_id', t('This value must be an integer')), + new Validators\Integer('swimlane_id', t('This value must be an integer')), new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200), new Validators\Date('date_due', t('Invalid date'), $this->dateParser->getDateFormats()), new Validators\Date('date_started', t('Invalid date'), $this->dateParser->getDateFormats()), diff --git a/sources/app/Model/User.php b/sources/app/Model/User.php index 8fdfa81..1bcc82b 100644 --- a/sources/app/Model/User.php +++ b/sources/app/Model/User.php @@ -28,6 +28,17 @@ class User extends Base */ const EVERYBODY_ID = -1; + /** + * Return the full name + * + * @param array $user User properties + * @return string + */ + public function getFullname(array $user) + { + return $user['name'] ?: $user['username']; + } + /** * Return true is the given user id is administrator * @@ -37,46 +48,12 @@ class User extends Base */ public function isAdmin($user_id) { - $result = $this->db + return $this->userSession->isAdmin() || // Avoid SQL query if connected + $this->db ->table(User::TABLE) ->eq('id', $user_id) ->eq('is_admin', 1) - ->count(); - - return $result > 0; - } - - /** - * Get the default project from the session - * - * @access public - * @return integer - */ - public function getFavoriteProjectId() - { - return isset($_SESSION['user']['default_project_id']) ? $_SESSION['user']['default_project_id'] : 0; - } - - /** - * Get the last seen project from the session - * - * @access public - * @return integer - */ - public function getLastSeenProjectId() - { - return empty($_SESSION['user']['last_show_project_id']) ? 0 : $_SESSION['user']['last_show_project_id']; - } - - /** - * Set the last seen project from the session - * - * @access public - * @@param integer $project_id Project id - */ - public function storeLastSeenProjectId($project_id) - { - $_SESSION['user']['last_show_project_id'] = (int) $project_id; + ->count() === 1; } /** @@ -276,8 +253,8 @@ class User extends Base $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); // If the user is connected refresh his session - if (Session::isOpen() && $_SESSION['user']['id'] == $values['id']) { - $this->updateSession(); + if (Session::isOpen() && $this->userSession->getId() == $values['id']) { + $this->userSession->refresh(); } return $result; @@ -317,30 +294,6 @@ class User extends Base }); } - /** - * Update user session information - * - * @access public - * @param array $user User data - */ - public function updateSession(array $user = array()) - { - if (empty($user)) { - $user = $this->getById($_SESSION['user']['id']); - } - - if (isset($user['password'])) { - unset($user['password']); - } - - $user['id'] = (int) $user['id']; - $user['default_project_id'] = (int) $user['default_project_id']; - $user['is_admin'] = (bool) $user['is_admin']; - $user['is_ldap_user'] = (bool) $user['is_ldap_user']; - - $_SESSION['user'] = $user; - } - /** * Common validation rules * @@ -457,7 +410,7 @@ class User extends Base if ($v->execute()) { // Check password - if ($this->authentication->authenticate($_SESSION['user']['username'], $values['current_password'])) { + if ($this->authentication->authenticate($this->session['user']['username'], $values['current_password'])) { return array(true, array()); } else { diff --git a/sources/app/Model/UserSession.php b/sources/app/Model/UserSession.php new file mode 100644 index 0000000..6d9a2eb --- /dev/null +++ b/sources/app/Model/UserSession.php @@ -0,0 +1,115 @@ +user->getById($this->userSession->getId()); + } + + if (isset($user['password'])) { + unset($user['password']); + } + + $user['id'] = (int) $user['id']; + $user['default_project_id'] = (int) $user['default_project_id']; + $user['is_admin'] = (bool) $user['is_admin']; + $user['is_ldap_user'] = (bool) $user['is_ldap_user']; + + $this->session['user'] = $user; + } + + /** + * Return true if the logged user is admin + * + * @access public + * @return bool + */ + public function isAdmin() + { + return isset($this->session['user']['is_admin']) && $this->session['user']['is_admin'] === true; + } + + /** + * Get the connected user id + * + * @access public + * @return integer + */ + public function getId() + { + return isset($this->session['user']['id']) ? (int) $this->session['user']['id'] : 0; + } + + /** + * Check if the given user_id is the connected user + * + * @param integer $user_id User id + * @return boolean + */ + public function isCurrentUser($user_id) + { + return $this->getId() == $user_id; + } + + /** + * Check is the user is connected + * + * @access public + * @return bool + */ + public function isLogged() + { + return ! empty($this->session['user']); + } + + /** + * Get the last seen project from the session + * + * @access public + * @return integer + */ + public function getLastSeenProjectId() + { + return empty($this->session['last_show_project_id']) ? 0 : $this->session['last_show_project_id']; + } + + /** + * Get the default project from the session + * + * @access public + * @return integer + */ + public function getFavoriteProjectId() + { + return isset($this->session['user']['default_project_id']) ? $this->session['user']['default_project_id'] : 0; + } + + /** + * Set the last seen project from the session + * + * @access public + * @param integer $project_id Project id + */ + public function storeLastSeenProjectId($project_id) + { + $this->session['last_show_project_id'] = (int) $project_id; + } +} diff --git a/sources/app/Model/Webhook.php b/sources/app/Model/Webhook.php index 14d5068..7edffa6 100644 --- a/sources/app/Model/Webhook.php +++ b/sources/app/Model/Webhook.php @@ -2,8 +2,6 @@ namespace Model; -use Event\WebhookListener; - /** * Webhook model * @@ -33,87 +31,6 @@ class Webhook extends Base */ const HTTP_USER_AGENT = 'Kanboard Webhook'; - /** - * URL to call for task creation - * - * @access private - * @var string - */ - private $url_task_creation = ''; - - /** - * URL to call for task modification - * - * @access private - * @var string - */ - private $url_task_modification = ''; - - /** - * Webook token - * - * @access private - * @var string - */ - private $token = ''; - - /** - * Attach events - * - * @access public - */ - public function attachEvents() - { - $this->url_task_creation = $this->config->get('webhook_url_task_creation'); - $this->url_task_modification = $this->config->get('webhook_url_task_modification'); - $this->token = $this->config->get('webhook_token'); - - if ($this->url_task_creation) { - $this->attachCreateEvents(); - } - - if ($this->url_task_modification) { - $this->attachUpdateEvents(); - } - } - - /** - * Attach events for task modification - * - * @access public - */ - public function attachUpdateEvents() - { - $events = array( - Task::EVENT_UPDATE, - Task::EVENT_CLOSE, - Task::EVENT_OPEN, - Task::EVENT_MOVE_COLUMN, - Task::EVENT_MOVE_POSITION, - Task::EVENT_ASSIGNEE_CHANGE, - ); - - $listener = new WebhookListener($this->container); - $listener->setUrl($this->url_task_modification); - - foreach ($events as $event_name) { - $this->event->attach($event_name, $listener); - } - } - - /** - * Attach events for task creation - * - * @access public - */ - public function attachCreateEvents() - { - $listener = new WebhookListener($this->container); - $listener->setUrl($this->url_task_creation); - - $this->event->attach(Task::EVENT_CREATE, $listener); - } - /** * Call the external URL * @@ -123,6 +40,8 @@ class Webhook extends Base */ public function notify($url, array $task) { + $token = $this->config->get('webhook_token'); + $headers = array( 'Connection: close', 'User-Agent: '.self::HTTP_USER_AGENT, @@ -140,10 +59,10 @@ class Webhook extends Base )); if (strpos($url, '?') !== false) { - $url .= '&token='.$this->token; + $url .= '&token='.$token; } else { - $url .= '?token='.$this->token; + $url .= '?token='.$token; } @file_get_contents($url, false, $context); diff --git a/sources/app/Schema/Mysql.php b/sources/app/Schema/Mysql.php index 52dbea5..05e8f14 100644 --- a/sources/app/Schema/Mysql.php +++ b/sources/app/Schema/Mysql.php @@ -5,7 +5,68 @@ namespace Schema; use PDO; use Core\Security; -const VERSION = 36; +const VERSION = 41; + +function version_41($pdo) +{ + $pdo->exec('ALTER TABLE users ADD COLUMN timezone VARCHAR(50)'); + $pdo->exec('ALTER TABLE users ADD COLUMN language CHAR(5)'); +} + +function version_40($pdo) +{ + // Avoid some full table scans + $pdo->exec('CREATE INDEX users_admin_idx ON users(is_admin)'); + $pdo->exec('CREATE INDEX columns_project_idx ON columns(project_id)'); + $pdo->exec('CREATE INDEX tasks_project_idx ON tasks(project_id)'); + $pdo->exec('CREATE INDEX swimlanes_project_idx ON swimlanes(project_id)'); + $pdo->exec('CREATE INDEX categories_project_idx ON project_has_categories(project_id)'); + $pdo->exec('CREATE INDEX subtasks_task_idx ON task_has_subtasks(task_id)'); + $pdo->exec('CREATE INDEX files_task_idx ON task_has_files(task_id)'); + $pdo->exec('CREATE INDEX comments_task_idx ON comments(task_id)'); + + // Set the ownership for all private projects + $rq = $pdo->prepare('SELECT id FROM projects WHERE is_private=1'); + $rq->execute(); + $project_ids = $rq->fetchAll(PDO::FETCH_COLUMN, 0); + + $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)); + } +} + +function version_39($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('project_categories', '')); +} + +function version_38($pdo) +{ + $pdo->exec(" + CREATE TABLE swimlanes ( + id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(200) NOT NULL, + position INT DEFAULT 1, + is_active INT DEFAULT 1, + project_id INT, + PRIMARY KEY(id), + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE (name, project_id) + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $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 show_default_swimlane INT DEFAULT 1"); +} + +function version_37($pdo) +{ + $pdo->exec("ALTER TABLE project_has_users ADD COLUMN is_owner TINYINT(1) DEFAULT '0'"); +} function version_36($pdo) { diff --git a/sources/app/Schema/Postgres.php b/sources/app/Schema/Postgres.php index 9493e60..9d5aa7a 100644 --- a/sources/app/Schema/Postgres.php +++ b/sources/app/Schema/Postgres.php @@ -5,7 +5,67 @@ namespace Schema; use PDO; use Core\Security; -const VERSION = 17; +const VERSION = 22; + +function version_22($pdo) +{ + $pdo->exec('ALTER TABLE users ADD COLUMN timezone VARCHAR(50)'); + $pdo->exec('ALTER TABLE users ADD COLUMN language CHAR(5)'); +} + +function version_21($pdo) +{ + // Avoid some full table scans + $pdo->exec('CREATE INDEX users_admin_idx ON users(is_admin)'); + $pdo->exec('CREATE INDEX columns_project_idx ON columns(project_id)'); + $pdo->exec('CREATE INDEX tasks_project_idx ON tasks(project_id)'); + $pdo->exec('CREATE INDEX swimlanes_project_idx ON swimlanes(project_id)'); + $pdo->exec('CREATE INDEX categories_project_idx ON project_has_categories(project_id)'); + $pdo->exec('CREATE INDEX subtasks_task_idx ON task_has_subtasks(task_id)'); + $pdo->exec('CREATE INDEX files_task_idx ON task_has_files(task_id)'); + $pdo->exec('CREATE INDEX comments_task_idx ON comments(task_id)'); + + // Set the ownership for all private projects + $rq = $pdo->prepare("SELECT id FROM projects WHERE is_private='1'"); + $rq->execute(); + $project_ids = $rq->fetchAll(PDO::FETCH_COLUMN, 0); + + $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)); + } +} + +function version_20($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('project_categories', '')); +} + +function version_19($pdo) +{ + $pdo->exec(" + CREATE TABLE swimlanes ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) NOT NULL, + position INTEGER DEFAULT 1, + is_active BOOLEAN DEFAULT '1', + project_id INTEGER, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE (name, project_id) + ) + "); + + $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 show_default_swimlane BOOLEAN DEFAULT '1'"); +} + +function version_18($pdo) +{ + $pdo->exec("ALTER TABLE project_has_users ADD COLUMN is_owner BOOLEAN DEFAULT '0'"); +} function version_17($pdo) { diff --git a/sources/app/Schema/Sqlite.php b/sources/app/Schema/Sqlite.php index 82c2f41..c615606 100644 --- a/sources/app/Schema/Sqlite.php +++ b/sources/app/Schema/Sqlite.php @@ -5,7 +5,67 @@ namespace Schema; use Core\Security; use PDO; -const VERSION = 35; +const VERSION = 40; + +function version_40($pdo) +{ + $pdo->exec('ALTER TABLE users ADD COLUMN timezone TEXT'); + $pdo->exec('ALTER TABLE users ADD COLUMN language TEXT'); +} + +function version_39($pdo) +{ + // Avoid some full table scans + $pdo->exec('CREATE INDEX users_admin_idx ON users(is_admin)'); + $pdo->exec('CREATE INDEX columns_project_idx ON columns(project_id)'); + $pdo->exec('CREATE INDEX tasks_project_idx ON tasks(project_id)'); + $pdo->exec('CREATE INDEX swimlanes_project_idx ON swimlanes(project_id)'); + $pdo->exec('CREATE INDEX categories_project_idx ON project_has_categories(project_id)'); + $pdo->exec('CREATE INDEX subtasks_task_idx ON task_has_subtasks(task_id)'); + $pdo->exec('CREATE INDEX files_task_idx ON task_has_files(task_id)'); + $pdo->exec('CREATE INDEX comments_task_idx ON comments(task_id)'); + + // Set the ownership for all private projects + $rq = $pdo->prepare('SELECT id FROM projects WHERE is_private=1'); + $rq->execute(); + $project_ids = $rq->fetchAll(PDO::FETCH_COLUMN, 0); + + $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)); + } +} + +function version_38($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('project_categories', '')); +} + +function version_37($pdo) +{ + $pdo->exec(" + CREATE TABLE swimlanes ( + id INTEGER PRIMARY KEY, + name TEXT, + position INTEGER DEFAULT 1, + is_active INTEGER DEFAULT 1, + project_id INTEGER, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE (name, project_id) + ) + "); + + $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 show_default_swimlane INTEGER DEFAULT 1"); +} + +function version_36($pdo) +{ + $pdo->exec('ALTER TABLE project_has_users ADD COLUMN is_owner INTEGER DEFAULT "0"'); +} function version_35($pdo) { @@ -457,7 +517,7 @@ function version_1($pdo) $pdo->exec(" CREATE TABLE tasks ( id INTEGER PRIMARY KEY, - title TEXT NOT NULL, + title TEXT NOCASE NOT NULL, description TEXT, date_creation INTEGER, color_id TEXT, diff --git a/sources/app/ServiceProvider/ClassProvider.php b/sources/app/ServiceProvider/ClassProvider.php new file mode 100644 index 0000000..3177276 --- /dev/null +++ b/sources/app/ServiceProvider/ClassProvider.php @@ -0,0 +1,79 @@ + array( + 'Acl', + 'Action', + 'Authentication', + 'Board', + 'Category', + 'Color', + 'Comment', + 'Config', + 'DateParser', + 'File', + 'LastLogin', + 'Notification', + 'Project', + 'ProjectActivity', + 'ProjectAnalytic', + 'ProjectDailySummary', + 'ProjectPaginator', + 'ProjectPermission', + 'SubTask', + 'SubtaskPaginator', + 'SubtaskExport', + 'Swimlane', + 'Task', + 'TaskCreation', + 'TaskDuplication', + 'TaskExport', + 'TaskFinder', + 'TaskModification', + 'TaskPaginator', + 'TaskPermission', + 'TaskPosition', + 'TaskStatus', + 'TaskValidator', + 'TimeTracking', + 'User', + 'UserSession', + 'Webhook', + ), + 'Core' => array( + 'Template', + 'Session', + 'MemoryCache', + 'FileCache', + ), + 'Integration' => array( + 'GitlabWebhook', + 'GithubWebhook', + ) + ); + + public function register(Container $container) + { + foreach ($this->classes as $namespace => $classes) { + + foreach ($classes as $name) { + + $class = '\\'.$namespace.'\\'.$name; + + $container[lcfirst($name)] = function ($c) use ($class) { + return new $class($c); + }; + } + } + } +} diff --git a/sources/app/ServiceProvider/Database.php b/sources/app/ServiceProvider/DatabaseProvider.php similarity index 83% rename from sources/app/ServiceProvider/Database.php rename to sources/app/ServiceProvider/DatabaseProvider.php index 75e1f73..4218f5f 100644 --- a/sources/app/ServiceProvider/Database.php +++ b/sources/app/ServiceProvider/DatabaseProvider.php @@ -4,19 +4,21 @@ namespace ServiceProvider; use Pimple\Container; use Pimple\ServiceProviderInterface; -use PicoDb\Database as Dbal; +use PicoDb\Database; -class Database implements ServiceProviderInterface +class DatabaseProvider implements ServiceProviderInterface { public function register(Container $container) { $container['db'] = $this->getInstance(); + $container['db']->stopwatch = DEBUG; + $container['db']->log_queries = DEBUG; } /** * Setup the database driver and execute schema migration * - * @return PicoDb\Database + * @return \PicoDb\Database */ public function getInstance() { @@ -49,13 +51,13 @@ class Database implements ServiceProviderInterface /** * Setup the Sqlite database driver * - * @return PicoDb\Database + * @return \PicoDb\Database */ function getSqliteInstance() { require_once __DIR__.'/../Schema/Sqlite.php'; - return new Dbal(array( + return new Database(array( 'driver' => 'sqlite', 'filename' => DB_FILENAME )); @@ -64,13 +66,13 @@ class Database implements ServiceProviderInterface /** * Setup the Mysql database driver * - * @return PicoDb\Database + * @return \PicoDb\Database */ function getMysqlInstance() { require_once __DIR__.'/../Schema/Mysql.php'; - return new Dbal(array( + return new Database(array( 'driver' => 'mysql', 'hostname' => DB_HOSTNAME, 'username' => DB_USERNAME, @@ -83,13 +85,13 @@ class Database implements ServiceProviderInterface /** * Setup the Postgres database driver * - * @return PicoDb\Database + * @return \PicoDb\Database */ public function getPostgresInstance() { require_once __DIR__.'/../Schema/Postgres.php'; - return new Dbal(array( + return new Database(array( 'driver' => 'postgres', 'hostname' => DB_HOSTNAME, 'username' => DB_USERNAME, diff --git a/sources/app/ServiceProvider/Event.php b/sources/app/ServiceProvider/Event.php deleted file mode 100644 index 0436aa7..0000000 --- a/sources/app/ServiceProvider/Event.php +++ /dev/null @@ -1,15 +0,0 @@ -addSubscriber(new BootstrapSubscriber($container)); + $container['dispatcher']->addSubscriber(new AuthSubscriber($container)); + $container['dispatcher']->addSubscriber(new ProjectActivitySubscriber($container)); + $container['dispatcher']->addSubscriber(new ProjectDailySummarySubscriber($container)); + $container['dispatcher']->addSubscriber(new ProjectModificationDateSubscriber($container)); + $container['dispatcher']->addSubscriber(new WebhookSubscriber($container)); + $container['dispatcher']->addSubscriber(new NotificationSubscriber($container)); + + // Automatic actions + $container['action']->attachEvents(); + } +} diff --git a/sources/app/ServiceProvider/Logging.php b/sources/app/ServiceProvider/Logging.php deleted file mode 100644 index 9737cad..0000000 --- a/sources/app/ServiceProvider/Logging.php +++ /dev/null @@ -1,21 +0,0 @@ -pushHandler(new StreamHandler(__DIR__.'/../../data/debug.log', Logger::DEBUG)); - $logger->pushHandler(new SyslogHandler('kanboard', LOG_USER, Logger::DEBUG)); - - $container['logger'] = $logger; - } -} diff --git a/sources/app/ServiceProvider/LoggingProvider.php b/sources/app/ServiceProvider/LoggingProvider.php new file mode 100644 index 0000000..5b2cf56 --- /dev/null +++ b/sources/app/ServiceProvider/LoggingProvider.php @@ -0,0 +1,24 @@ +setLogger(new Syslog('kanboard')); + + if (DEBUG) { + $logger->setLogger(new File(__DIR__.'/../../data/debug.log')); + } + + $container['logger'] = $logger; + } +} diff --git a/sources/app/ServiceProvider/Mailer.php b/sources/app/ServiceProvider/Mailer.php deleted file mode 100644 index c82c16f..0000000 --- a/sources/app/ServiceProvider/Mailer.php +++ /dev/null @@ -1,36 +0,0 @@ -getInstance(); - } - - public function getInstance() - { - switch (MAIL_TRANSPORT) { - case 'smtp': - $transport = Swift_SmtpTransport::newInstance(MAIL_SMTP_HOSTNAME, MAIL_SMTP_PORT); - $transport->setUsername(MAIL_SMTP_USERNAME); - $transport->setPassword(MAIL_SMTP_PASSWORD); - $transport->setEncryption(MAIL_SMTP_ENCRYPTION); - break; - case 'sendmail': - $transport = Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND); - break; - default: - $transport = Swift_MailTransport::newInstance(); - } - - return $transport; - } -} diff --git a/sources/app/ServiceProvider/MailerProvider.php b/sources/app/ServiceProvider/MailerProvider.php new file mode 100644 index 0000000..6469a73 --- /dev/null +++ b/sources/app/ServiceProvider/MailerProvider.php @@ -0,0 +1,33 @@ +setUsername(MAIL_SMTP_USERNAME); + $transport->setPassword(MAIL_SMTP_PASSWORD); + $transport->setEncryption(MAIL_SMTP_ENCRYPTION); + break; + case 'sendmail': + $transport = Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND); + break; + default: + $transport = Swift_MailTransport::newInstance(); + } + + return $transport; + }; + } +} diff --git a/sources/app/Subscriber/AuthSubscriber.php b/sources/app/Subscriber/AuthSubscriber.php new file mode 100644 index 0000000..161a7af --- /dev/null +++ b/sources/app/Subscriber/AuthSubscriber.php @@ -0,0 +1,27 @@ + array('onSuccess', 0), + ); + } + + public function onSuccess(AuthEvent $event) + { + $this->lastLogin->create( + $event->getAuthType(), + $event->getUserId(), + Request::getIpAddress(), + Request::getUserAgent() + ); + } +} diff --git a/sources/app/Subscriber/Base.php b/sources/app/Subscriber/Base.php new file mode 100644 index 0000000..abc051b --- /dev/null +++ b/sources/app/Subscriber/Base.php @@ -0,0 +1,56 @@ +container = $container; + } + + /** + * Load automatically models + * + * @access public + * @param string $name Model name + * @return mixed + */ + public function __get($name) + { + return $this->container[$name]; + } +} diff --git a/sources/app/Subscriber/BootstrapSubscriber.php b/sources/app/Subscriber/BootstrapSubscriber.php new file mode 100644 index 0000000..35d0eff --- /dev/null +++ b/sources/app/Subscriber/BootstrapSubscriber.php @@ -0,0 +1,23 @@ + array('setup', 0), + 'api.bootstrap' => array('setup', 0), + 'console.bootstrap' => array('setup', 0), + ); + } + + public function setup() + { + $this->config->setupTranslations(); + $this->config->setupTimezone(); + } +} diff --git a/sources/app/Subscriber/NotificationSubscriber.php b/sources/app/Subscriber/NotificationSubscriber.php new file mode 100644 index 0000000..1580f6d --- /dev/null +++ b/sources/app/Subscriber/NotificationSubscriber.php @@ -0,0 +1,81 @@ + 'task_creation', + Task::EVENT_UPDATE => 'task_update', + Task::EVENT_CLOSE => 'task_close', + Task::EVENT_OPEN => 'task_open', + 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', + Comment::EVENT_CREATE => 'comment_creation', + Comment::EVENT_UPDATE => 'comment_update', + File::EVENT_CREATE => 'file_creation', + ); + + public static function getSubscribedEvents() + { + return array( + Task::EVENT_CREATE => array('execute', 0), + Task::EVENT_UPDATE => array('execute', 0), + Task::EVENT_CLOSE => array('execute', 0), + Task::EVENT_OPEN => array('execute', 0), + 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), + Comment::EVENT_CREATE => array('execute', 0), + Comment::EVENT_UPDATE => array('execute', 0), + File::EVENT_CREATE => array('execute', 0), + ); + } + + public function execute(GenericEvent $event, $event_name) + { + $values = $this->getTemplateData($event); + $users = $this->notification->getUsersList($values['task']['project_id']); + + if ($users) { + $this->notification->sendEmails($this->templates[$event_name], $users, $values); + } + } + + public function getTemplateData(GenericEvent $event) + { + $values = array(); + + switch (get_class($event)) { + case 'Event\TaskEvent': + $values['task'] = $this->taskFinder->getDetails($event['task_id']); + break; + case 'Event\SubtaskEvent': + $values['subtask'] = $this->subTask->getById($event['id'], true); + $values['task'] = $this->taskFinder->getDetails($event['task_id']); + break; + case 'Event\FileEvent': + $values['file'] = $event->getAll(); + $values['task'] = $this->taskFinder->getDetails($event['task_id']); + break; + case 'Event\CommentEvent': + $values['comment'] = $this->comment->getById($event['id']); + $values['task'] = $this->taskFinder->getDetails($values['comment']['task_id']); + break; + } + + return $values; + } +} diff --git a/sources/app/Subscriber/ProjectActivitySubscriber.php b/sources/app/Subscriber/ProjectActivitySubscriber.php new file mode 100644 index 0000000..aae09ae --- /dev/null +++ b/sources/app/Subscriber/ProjectActivitySubscriber.php @@ -0,0 +1,63 @@ + array('execute', 0), + Task::EVENT_UPDATE => array('execute', 0), + Task::EVENT_CREATE => array('execute', 0), + Task::EVENT_CLOSE => array('execute', 0), + Task::EVENT_OPEN => array('execute', 0), + Task::EVENT_MOVE_COLUMN => array('execute', 0), + 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), + ); + } + + public function execute(GenericEvent $event, $event_name) + { + // Executed only when someone is logged + if ($this->userSession->isLogged() && isset($event['task_id'])) { + + $values = $this->getValues($event); + + $this->projectActivity->createEvent( + $values['task']['project_id'], + $values['task']['id'], + $this->userSession->getId(), + $event_name, + $values + ); + } + } + + private function getValues(GenericEvent $event) + { + $values = array(); + $values['task'] = $this->taskFinder->getDetails($event['task_id']); + + switch (get_class($event)) { + case 'Event\SubtaskEvent': + $values['subtask'] = $this->subTask->getById($event['id'], true); + break; + case 'Event\CommentEvent': + $values['comment'] = $this->comment->getById($event['id']); + break; + } + + return $values; + } +} diff --git a/sources/app/Subscriber/ProjectDailySummarySubscriber.php b/sources/app/Subscriber/ProjectDailySummarySubscriber.php new file mode 100644 index 0000000..6d73773 --- /dev/null +++ b/sources/app/Subscriber/ProjectDailySummarySubscriber.php @@ -0,0 +1,27 @@ + array('execute', 0), + Task::EVENT_CLOSE => array('execute', 0), + Task::EVENT_OPEN => array('execute', 0), + Task::EVENT_MOVE_COLUMN => array('execute', 0), + ); + } + + public function execute(TaskEvent $event) + { + if (isset($event['project_id'])) { + $this->projectDailySummary->updateTotals($event['project_id'], date('Y-m-d')); + } + } +} diff --git a/sources/app/Subscriber/ProjectModificationDateSubscriber.php b/sources/app/Subscriber/ProjectModificationDateSubscriber.php new file mode 100644 index 0000000..4c5380f --- /dev/null +++ b/sources/app/Subscriber/ProjectModificationDateSubscriber.php @@ -0,0 +1,31 @@ + array('execute', 0), + Task::EVENT_CLOSE => array('execute', 0), + Task::EVENT_OPEN => array('execute', 0), + Task::EVENT_MOVE_SWIMLANE => array('execute', 0), + Task::EVENT_MOVE_COLUMN => array('execute', 0), + Task::EVENT_MOVE_POSITION => array('execute', 0), + Task::EVENT_MOVE_PROJECT => array('execute', 0), + Task::EVENT_ASSIGNEE_CHANGE => array('execute', 0), + ); + } + + public function execute(GenericEvent $event) + { + if (isset($event['project_id'])) { + $this->project->updateModificationDate($event['project_id']); + } + } +} diff --git a/sources/app/Subscriber/WebhookSubscriber.php b/sources/app/Subscriber/WebhookSubscriber.php new file mode 100644 index 0000000..6b5abf1 --- /dev/null +++ b/sources/app/Subscriber/WebhookSubscriber.php @@ -0,0 +1,42 @@ + array('onTaskCreation', 0), + Task::EVENT_UPDATE => array('onTaskModification', 0), + Task::EVENT_CLOSE => array('onTaskModification', 0), + Task::EVENT_OPEN => array('onTaskModification', 0), + Task::EVENT_MOVE_COLUMN => array('onTaskModification', 0), + Task::EVENT_MOVE_POSITION => array('onTaskModification', 0), + Task::EVENT_ASSIGNEE_CHANGE => array('onTaskModification', 0), + ); + } + + public function onTaskCreation(TaskEvent $event) + { + $this->executeRequest('webhook_url_task_creation', $event); + } + + public function onTaskModification(TaskEvent $event) + { + $this->executeRequest('webhook_url_task_modification', $event); + } + + public function executeRequest($parameter, TaskEvent $event) + { + $url = $this->config->get($parameter); + + if (! empty($url)) { + $this->webhook->notify($url, $event->getAll()); + } + } +} diff --git a/sources/app/Template/action/event.php b/sources/app/Template/action/event.php index 565e900..67a65c1 100644 --- a/sources/app/Template/action/event.php +++ b/sources/app/Template/action/event.php @@ -3,15 +3,15 @@

-
+ - + formCsrf() ?> - - + formHidden('project_id', $values) ?> + formHidden('action_name', $values) ?> - -
+ formLabel(t('Event'), 'event_name') ?> + formSelect('event_name', $events, $values) ?>
@@ -20,6 +20,6 @@
- $project['id'])) ?> + a(t('cancel'), 'action', 'index', array('project_id' => $project['id'])) ?>
\ No newline at end of file diff --git a/sources/app/Template/action/index.php b/sources/app/Template/action/index.php index 54f1369..e388fbc 100644 --- a/sources/app/Template/action/index.php +++ b/sources/app/Template/action/index.php @@ -15,26 +15,26 @@ - - + inList($action['event_name'], $available_events) ?> + inList($action['action_name'], $available_actions) ?> - $project['id'], 'action_id' => $action['id'])) ?> + a(t('Remove'), 'action', 'confirm', array('project_id' => $project['id'], 'action_id' => $action['id'])) ?> @@ -51,12 +51,12 @@

-
- - + + formCsrf() ?> + formHidden('project_id', $values) ?> - -
+ formLabel(t('Action'), 'action_name') ?> + formSelect('action_name', $available_actions, $values) ?>
diff --git a/sources/app/Template/action/params.php b/sources/app/Template/action/params.php index f221d92..3a5ecb8 100644 --- a/sources/app/Template/action/params.php +++ b/sources/app/Template/action/params.php @@ -3,34 +3,34 @@

- + - + formCsrf() ?> - - - + formHidden('project_id', $values) ?> + formHidden('event_name', $values) ?> + formHidden('action_name', $values) ?> $param_desc): ?> - - -
- - -
- - -
- - -
- - -
- - - + contains($param_name, 'column_id')): ?> + formLabel($param_desc, $param_name) ?> + formSelect('params['.$param_name.']', $columns_list, $values) ?>
+ contains($param_name, 'user_id')): ?> + formLabel($param_desc, $param_name) ?> + formSelect('params['.$param_name.']', $users_list, $values) ?>
+ contains($param_name, 'project_id')): ?> + formLabel($param_desc, $param_name) ?> + formSelect('params['.$param_name.']', $projects_list, $values) ?>
+ contains($param_name, 'color_id')): ?> + formLabel($param_desc, $param_name) ?> + formSelect('params['.$param_name.']', $colors_list, $values) ?>
+ contains($param_name, 'category_id')): ?> + formLabel($param_desc, $param_name) ?> + formSelect('params['.$param_name.']', $categories_list, $values) ?>
+ contains($param_name, 'label')): ?> + formLabel($param_desc, $param_name) ?> + formText('params['.$param_name.']', $values) ?> @@ -38,6 +38,6 @@
- $project['id'])) ?> + a(t('cancel'), 'action', 'index', array('project_id' => $project['id'])) ?>
\ No newline at end of file diff --git a/sources/app/Template/action/remove.php b/sources/app/Template/action/remove.php index 131c2b5..672c08a 100644 --- a/sources/app/Template/action/remove.php +++ b/sources/app/Template/action/remove.php @@ -4,12 +4,12 @@

- + inList($action['event_name'], $available_events).'/'.$this->inList($action['action_name'], $available_actions)) ?>

- $project['id'], 'action_id' => $action['id']), true, 'btn btn-red') ?> + a(t('Yes'), 'action', 'remove', array('project_id' => $project['id'], 'action_id' => $action['id']), true, 'btn btn-red') ?> - $project['id'])) ?> + a(t('cancel'), 'action', 'index', array('project_id' => $project['id'])) ?>
\ No newline at end of file diff --git a/sources/app/Template/analytic/cfd.php b/sources/app/Template/analytic/cfd.php index d8dd551..58115e7 100644 --- a/sources/app/Template/analytic/cfd.php +++ b/sources/app/Template/analytic/cfd.php @@ -6,24 +6,24 @@

-
+

-
+ - + formCsrf() ?>
- - + formLabel(t('Start Date'), 'from') ?> + formText('from', $values, array(), array('required', 'placeholder="'.$this->inList($date_format, $date_formats).'"'), 'form-date') ?>
- - + formLabel(t('End Date'), 'to') ?> + formText('to', $values, array(), array('required', 'placeholder="'.$this->inList($date_format, $date_formats).'"'), 'form-date') ?>
diff --git a/sources/app/Template/analytic/layout.php b/sources/app/Template/analytic/layout.php index 2dd7a4f..f250691 100644 --- a/sources/app/Template/analytic/layout.php +++ b/sources/app/Template/analytic/layout.php @@ -1,15 +1,15 @@ - - +js('assets/js/d3.v3.4.8.min.js') ?> +js('assets/js/dimple.v2.1.0.min.js') ?>
    -
  • -
  • -
  • -
  • +
  • e($user['username']) ?>
  • +
  • e($user['name']) ?: t('None') ?>
  • +
  • e($user['email']) ?: t('None') ?>
  • +
  • e($projects[$user['default_project_id']]) : t('None') ?>
  • +
  • inList($user['timezone'], $timezones) ?>
  • +
  • inList($user['language'], $languages) ?>
  • diff --git a/sources/app/Template/user/sidebar.php b/sources/app/Template/user/sidebar.php index 0a1a076..f74c8b0 100644 --- a/sources/app/Template/user/sidebar.php +++ b/sources/app/Template/user/sidebar.php @@ -2,37 +2,37 @@

    • - $user['id'])) ?> + a(t('Summary'), 'user', 'show', array('user_id' => $user['id'])) ?>
    • - + userSession->isAdmin() || $this->userSession->isCurrentUser($user['id'])): ?>
    • - $user['id'])) ?> + a(t('Edit profile'), 'user', 'edit', array('user_id' => $user['id'])) ?>
    • - $user['id'])) ?> + a(t('Change password'), 'user', 'password', array('user_id' => $user['id'])) ?>
    • - $user['id'])) ?> + a(t('Email notifications'), 'user', 'notifications', array('user_id' => $user['id'])) ?>
    • - $user['id'])) ?> + a(t('External accounts'), 'user', 'external', array('user_id' => $user['id'])) ?>
    • - $user['id'])) ?> + a(t('Last logins'), 'user', 'last', array('user_id' => $user['id'])) ?>
    • - $user['id'])) ?> + a(t('Persistent connections'), 'user', 'sessions', array('user_id' => $user['id'])) ?>
    • - + userSession->isAdmin() && ! $this->userSession->isCurrentUser($user['id'])): ?>
    • - $user['id'])) ?> + a(t('Remove'), 'user', 'remove', array('user_id' => $user['id'])) ?>
    diff --git a/sources/app/common.php b/sources/app/common.php index addfe87..bd336d9 100644 --- a/sources/app/common.php +++ b/sources/app/common.php @@ -10,7 +10,8 @@ if (file_exists('config.php')) { require __DIR__.'/constants.php'; $container = new Pimple\Container; -$container->register(new ServiceProvider\Logging); -$container->register(new ServiceProvider\Database); -$container->register(new ServiceProvider\Event); -$container->register(new ServiceProvider\Mailer); +$container->register(new ServiceProvider\LoggingProvider); +$container->register(new ServiceProvider\DatabaseProvider); +$container->register(new ServiceProvider\ClassProvider); +$container->register(new ServiceProvider\EventDispatcherProvider); +$container->register(new ServiceProvider\MailerProvider); diff --git a/sources/app/constants.php b/sources/app/constants.php index 9283a02..4a74119 100644 --- a/sources/app/constants.php +++ b/sources/app/constants.php @@ -1,7 +1,10 @@ '; -} - -/** - * Add a stylesheet asset - * - * @param string $filename Filename - * @return string - */ -function css($filename) -{ - return ''; -} - -/** - * Load a template - * - * @param string $name Template name - * @param array $args Template parameters - * @return string - */ -function template($name, array $args = array()) -{ - $tpl = new Template; - return $tpl->load($name, $args); -} - -/** - * Check if the given user_id is the connected user - * - * @param integer $user_id User id - * @return boolean - */ -function is_current_user($user_id) -{ - return $_SESSION['user']['id'] == $user_id; -} - -/** - * Check if the current user is administrator - * - * @return boolean - */ -function is_admin() -{ - return $_SESSION['user']['is_admin'] == 1; -} - -/** - * Return true if the user can configure the project (project are previously filtered) - * - * @return boolean - */ -function is_project_admin(array $project) -{ - return is_admin() || $project['is_private'] == 1; -} - -/** - * Return the username - * - * @param array $user User properties (optional) - * @return string - */ -function get_username(array $user = array()) -{ - return ! empty($user) ? ($user['name'] ?: $user['username']) - : ($_SESSION['user']['name'] ?: $_SESSION['user']['username']); -} - -/** - * Get the current user id - * - * @return integer - */ -function get_user_id() -{ - return $_SESSION['user']['id']; -} - -/** - * Markdown transformation - * - * @param string $text Markdown content - * @param array $link Link parameters for replacement - * @return string - */ -function markdown($text, array $link = array('controller' => 'task', 'action' => 'show', 'params' => array())) -{ - $html = Parsedown::instance() - ->setMarkupEscaped(true) # escapes markup (HTML) - ->text($text); - - // Replace task #123 by a link to the task - $html = preg_replace_callback('!#(\d+)!i', function($matches) use ($link) { - return a( - $matches[0], - $link['controller'], - $link['action'], - $link['params'] + array('task_id' => $matches[1]) - ); - }, $html); - - return $html; -} - -/** - * Get the current URL without the querystring - * - * @return string - */ -function get_current_base_url() -{ - $url = Request::isHTTPS() ? 'https://' : 'http://'; - $url .= $_SERVER['SERVER_NAME']; - $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT']; - $url .= dirname($_SERVER['PHP_SELF']) !== '/' ? dirname($_SERVER['PHP_SELF']).'/' : '/'; - - return $url; -} - -/** - * HTML escaping - * - * @param string $value Value to escape - * @return string - */ -function escape($value) -{ - return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false); -} - -/** - * Dispplay the flash session message - * - * @param string $html HTML wrapper - * @return string - */ -function flash($html) -{ - $data = ''; - - if (isset($_SESSION['flash_message'])) { - $data = sprintf($html, escape($_SESSION['flash_message'])); - unset($_SESSION['flash_message']); - } - - return $data; -} - -/** - * Display the flash session error message - * - * @param string $html HTML wrapper - * @return string - */ -function flash_error($html) -{ - $data = ''; - - if (isset($_SESSION['flash_error_message'])) { - $data = sprintf($html, escape($_SESSION['flash_error_message'])); - unset($_SESSION['flash_error_message']); - } - - return $data; -} - -/** - * Format a file size - * - * @param integer $size Size in bytes - * @param integer $precision Precision - * @return string - */ -function format_bytes($size, $precision = 2) -{ - $base = log($size) / log(1024); - $suffixes = array('', 'k', 'M', 'G', 'T'); - - return round(pow(1024, $base - floor($base)), $precision).$suffixes[(int)floor($base)]; -} - -/** - * Truncate a long text - * - * @param string $value Text - * @param integer $max_length Max Length - * @param string $end Text end - * @return string - */ -function summary($value, $max_length = 85, $end = '[...]') -{ - $length = strlen($value); - - if ($length > $max_length) { - return substr($value, 0, $max_length).' '.$end; - } - - return $value; -} - -/** - * Return true if needle is contained in the haystack - * - * @param string $haystack Haystack - * @param string $needle Needle - * @return boolean - */ -function contains($haystack, $needle) -{ - return strpos($haystack, $needle) !== false; -} - -/** - * Return a value from a dictionary - * - * @param mixed $id Key - * @param array $listing Dictionary - * @param string $default_value Value displayed when the key doesn't exists - * @return string - */ -function in_list($id, array $listing, $default_value = '?') -{ - if (isset($listing[$id])) { - return escape($listing[$id]); - } - - return $default_value; -} - -/** - * Display the form error class - * - * @param array $errors Error list - * @param string $name Field name - * @return string - */ -function error_class(array $errors, $name) -{ - return ! isset($errors[$name]) ? '' : ' form-error'; -} - -/** - * Display a list of form errors - * - * @param array $errors List of errors - * @param string $name Field name - * @return string - */ -function error_list(array $errors, $name) -{ - $html = ''; - - if (isset($errors[$name])) { - - $html .= '
      '; - - foreach ($errors[$name] as $error) { - $html .= '
    • '.escape($error).'
    • '; - } - - $html .= '
    '; - } - - return $html; -} - -/** - * Get an escaped form value - * - * @param mixed $values Values - * @param string $name Field name - * @return string - */ -function form_value($values, $name) -{ - if (isset($values->$name)) { - return 'value="'.escape($values->$name).'"'; - } - - return isset($values[$name]) ? 'value="'.escape($values[$name]).'"' : ''; -} - -/** - * Hidden CSRF token field - * - * @return string - */ -function form_csrf() -{ - return ''; -} - -/** - * Display a hidden form field - * - * @param string $name Field name - * @param array $values Form values - * @return string - */ -function form_hidden($name, array $values = array()) -{ - return ''; -} - -/** - * Display a select field - * - * @param string $name Field name - * @param array $options Options - * @param array $values Form values - * @param array $errors Form errors - * @param string $class CSS class - * @return string - */ -function form_select($name, array $options, array $values = array(), array $errors = array(), $class = '') -{ - $html = ''; - $html .= error_list($errors, $name); - - return $html; -} - -/** - * Display a radio field group - * - * @param string $name Field name - * @param array $options Options - * @param array $values Form values - * @return string - */ -function form_radios($name, array $options, array $values = array()) -{ - $html = ''; - - foreach ($options as $value => $label) { - $html .= form_radio($name, $label, $value, isset($values[$name]) && $values[$name] == $value); - } - - return $html; -} - -/** - * Display a radio field - * - * @param string $name Field name - * @param string $label Form label - * @param string $value Form value - * @param boolean $selected Field selected or not - * @param string $class CSS class - * @return string - */ -function form_radio($name, $label, $value, $selected = false, $class = '') -{ - return ''; -} - -/** - * Display a checkbox field - * - * @param string $name Field name - * @param string $label Form label - * @param string $value Form value - * @param boolean $checked Field selected or not - * @param string $class CSS class - * @return string - */ -function form_checkbox($name, $label, $value, $checked = false, $class = '') -{ - return ''; -} - -/** - * Display a form label - * - * @param string $name Field name - * @param string $label Form label - * @param array $attributes HTML attributes - * @return string - */ -function form_label($label, $name, array $attributes = array()) -{ - return ''; -} - -/** - * Display a textarea - * - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_textarea($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - $class .= error_class($errors, $name); - - $html = ''; - $html .= error_list($errors, $name); - - return $html; -} - -/** - * Display a input field - * - * @param string $type HMTL input tag type - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_input($type, $name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - $class .= error_class($errors, $name); - - $html = ''; - if (in_array('required', $attributes)) $html .= '*'; - $html .= error_list($errors, $name); - - return $html; -} - -/** - * Display a text field - * - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('text', $name, $values, $errors, $attributes, $class); -} - -/** - * Display a password field - * - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_password($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('password', $name, $values, $errors, $attributes, $class); -} - -/** - * Display an email field - * - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_email($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('email', $name, $values, $errors, $attributes, $class); -} - -/** - * Display a date field - * - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_date($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('date', $name, $values, $errors, $attributes, $class); -} - -/** - * Display a number field - * - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_number($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('number', $name, $values, $errors, $attributes, $class); -} - -/** - * Display a numeric field (allow decimal number) - * - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_numeric($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('text', $name, $values, $errors, $attributes, $class.' form-numeric'); -} - -/** - * Link - * - * a('link', 'task', 'show', array('task_id' => $task_id)) - * - * @param string $label Link label - * @param string $controller Controller name - * @param string $action Action name - * @param array $params Url parameters - * @param boolean $csrf Add a CSRF token - * @param string $class CSS class attribute - * @param boolean $new_tab Open the link in a new tab - * @return string - */ -function a($label, $controller, $action, array $params = array(), $csrf = false, $class = '', $title = '', $new_tab = false) -{ - return ''.$label.''; -} - -/** - * URL query string - * - * u('task', 'show', array('task_id' => $task_id)) - * - * @param string $controller Controller name - * @param string $action Action name - * @param array $params Url parameters - * @param boolean $csrf Add a CSRF token - * @return string - */ -function u($controller, $action, array $params = array(), $csrf = false) -{ - $html = '?controller='.$controller.'&action='.$action; - - if ($csrf) { - $params['csrf_token'] = Security::getCSRFToken(); - } - - foreach ($params as $key => $value) { - $html .= '&'.$key.'='.$value; - } - - return $html; -} - -/** - * Pagination links - * - * @param array $pagination Pagination information - * @return string - */ -function paginate(array $pagination) -{ - extract($pagination); - - if ($pagination['offset'] === 0 && ($total - $pagination['offset']) <= $limit) { - return ''; - } - - $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 - */ -function order($label, $column, array $pagination) -{ - extract($pagination); - - $prefix = ''; - - if ($order === $column) { - $prefix = $direction === 'DESC' ? '▼ ' : '▲ '; - $direction = $direction === 'DESC' ? 'ASC' : 'DESC'; - } - - $order = $column; - - return $prefix.a($label, $controller, $action, $params + compact('offset', 'order', 'direction')); -} diff --git a/sources/assets/css/alert.css b/sources/assets/css/alert.css index 14e26c9..99dc417 100644 --- a/sources/assets/css/alert.css +++ b/sources/assets/css/alert.css @@ -74,6 +74,7 @@ } } +#main .alert, .page .alert { margin-top: 10px; -} \ No newline at end of file +} diff --git a/sources/assets/css/app.css b/sources/assets/css/app.css index 2ce277c..3e57991 100644 --- a/sources/assets/css/app.css +++ b/sources/assets/css/app.css @@ -24,6 +24,7 @@ body { -webkit-font-smoothing: antialiased; font-smoothing: antialiased; text-rendering: optimizeLegibility; + -webkit-text-size-adjust: 100%; } ul.no-bullet li { @@ -155,6 +156,10 @@ th a:hover { width: 30%; } +.column-35 { + width: 35%; +} + .column-40 { width: 40%; } @@ -183,7 +188,6 @@ label { input[type="number"], input[type="date"], input[type="email"], -input[type="tel"], input[type="password"], input[type="text"] { color: #888; @@ -202,7 +206,6 @@ input[type="text"] { input[type="number"]:focus, input[type="date"]:focus, input[type="email"]:focus, -input[type="tel"]:focus, input[type="password"]:focus, input[type="text"]:focus, textarea:focus { @@ -520,9 +523,11 @@ a.btn-blue:focus, } } +#main .alert, .page .alert { margin-top: 10px; -}/* tooltip */ +} +/* tooltip */ .tooltip-arrow:after { background: #fff; border: 1px solid #aaaaaa; @@ -734,7 +739,12 @@ a.filter-on { height: 70px; margin-bottom: 10px; } -/* project view */ + +/* swimlanes */ +th.board-swimlane-title { + vertical-align: top; + text-align: right; +}/* project view */ .project-listing { max-width: 500px; margin-left: 30px; @@ -804,6 +814,15 @@ div.task-board-recent { font-size: 80%; } +.task-board-current-user a { + text-decoration: underline; +} + +.task-board-current-user a:focus, +.task-board-current-user a:hover { + text-decoration: none; +} + a.task-board-nobody { font-weight: normal; font-style: italic; @@ -1149,6 +1168,7 @@ tr td.task-orange, border: 1px solid #ddd; color: #333; background-color: #fefefe; + overflow: auto; } .listing li { @@ -1202,19 +1222,23 @@ tr td.task-orange, margin-top: 10px; color: #555; }/* dashboard */ -#dashboard table { - font-size: 0.9em; -} -.dashboard-left-column { - width: 55%; - float: left; -} +@media only screen and (min-width: 1280px) { -.dashboard-right-column { - margin-left: 5%; - width: 40%; - float: left; + #dashboard table { + font-size: 0.9em; + } + + .dashboard-left-column { + width: 55%; + float: left; + } + + .dashboard-right-column { + margin-left: 5%; + width: 40%; + float: left; + } } .dashboard-project-stats span { @@ -1224,7 +1248,7 @@ tr td.task-orange, } .dashboard-project-stats strong { - font-size: 1.1em; + font-size: 1.2em; } .dashboard-table-link { @@ -1261,17 +1285,21 @@ tr td.task-orange, width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); + overflow: auto; } #popover-content { - position: fixed; + position: absolute; width: 70%; margin: 0 0 0 -35%; left: 50%; top: 5%; padding: 15px; background: #fff; -}/* confirmation box */ + overflow: scroll; + max-height: 83%; +} +/* confirmation box */ .confirm { max-width: 700px; font-size: 1.1em; @@ -1282,14 +1310,14 @@ tr td.task-orange, } .sidebar-content { - margin-left: 330px; + margin-left: 280px; } .sidebar { position: absolute; left: 0px; top: 0; - width: 250px; + max-width: 250px; padding: 10px; padding-top: 0; border: 1px solid #ddd; @@ -1313,6 +1341,10 @@ tr td.task-orange, font-size: 0.85em; } + .sidebar-content { + margin-left: 255px; + } + .form-tab { max-width: 404px; } @@ -1325,6 +1357,50 @@ tr td.task-orange, .form-inline-group input[type="submit"] { margin-top: 20px; } + + td > input[type="text"] { + max-width: 150px; + } + + .task-time-form label { + display: block; + } + + .task-time-form input[type="submit"] { + margin-top: 10px; + display: block; + } +} + +@media only screen and (max-width: 1024px) and (orientation: landscape) { + + body { + font-size: 0.7em; + } + + header { + padding-bottom: 4px; + } + + .sidebar { + max-width: 220px; + } + + .sidebar-content { + margin-left: 225px; + } + + div.chosen-container { + font-size: 0.9em; + } + + input[type="number"], + input[type="date"], + input[type="email"], + input[type="password"], + input[type="text"] { + height: 18px; + } } /*! * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome diff --git a/sources/assets/css/base.css b/sources/assets/css/base.css index f4c438a..a8c7d73 100644 --- a/sources/assets/css/base.css +++ b/sources/assets/css/base.css @@ -23,6 +23,7 @@ body { -webkit-font-smoothing: antialiased; font-smoothing: antialiased; text-rendering: optimizeLegibility; + -webkit-text-size-adjust: 100%; } ul.no-bullet li { diff --git a/sources/assets/css/board.css b/sources/assets/css/board.css index c939b34..37dba97 100644 --- a/sources/assets/css/board.css +++ b/sources/assets/css/board.css @@ -60,3 +60,9 @@ a.filter-on { height: 70px; margin-bottom: 10px; } + +/* swimlanes */ +th.board-swimlane-title { + vertical-align: top; + text-align: right; +} \ No newline at end of file diff --git a/sources/assets/css/dashboard.css b/sources/assets/css/dashboard.css index dc07718..aed805d 100644 --- a/sources/assets/css/dashboard.css +++ b/sources/assets/css/dashboard.css @@ -1,17 +1,21 @@ /* dashboard */ -#dashboard table { - font-size: 0.9em; -} -.dashboard-left-column { - width: 55%; - float: left; -} +@media only screen and (min-width: 1280px) { -.dashboard-right-column { - margin-left: 5%; - width: 40%; - float: left; + #dashboard table { + font-size: 0.9em; + } + + .dashboard-left-column { + width: 55%; + float: left; + } + + .dashboard-right-column { + margin-left: 5%; + width: 40%; + float: left; + } } .dashboard-project-stats span { @@ -21,7 +25,7 @@ } .dashboard-project-stats strong { - font-size: 1.1em; + font-size: 1.2em; } .dashboard-table-link { diff --git a/sources/assets/css/form.css b/sources/assets/css/form.css index ec56668..b6cb891 100644 --- a/sources/assets/css/form.css +++ b/sources/assets/css/form.css @@ -12,7 +12,6 @@ label { input[type="number"], input[type="date"], input[type="email"], -input[type="tel"], input[type="password"], input[type="text"] { color: #888; @@ -31,7 +30,6 @@ input[type="text"] { input[type="number"]:focus, input[type="date"]:focus, input[type="email"]:focus, -input[type="tel"]:focus, input[type="password"]:focus, input[type="text"]:focus, textarea:focus { diff --git a/sources/assets/css/listing.css b/sources/assets/css/listing.css index c71b8a7..c40c482 100644 --- a/sources/assets/css/listing.css +++ b/sources/assets/css/listing.css @@ -6,6 +6,7 @@ border: 1px solid #ddd; color: #333; background-color: #fefefe; + overflow: auto; } .listing li { diff --git a/sources/assets/css/popover.css b/sources/assets/css/popover.css index 74d472a..30ef094 100644 --- a/sources/assets/css/popover.css +++ b/sources/assets/css/popover.css @@ -6,14 +6,17 @@ width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); + overflow: auto; } #popover-content { - position: fixed; + position: absolute; width: 70%; margin: 0 0 0 -35%; left: 50%; top: 5%; padding: 15px; background: #fff; -} \ No newline at end of file + overflow: scroll; + max-height: 83%; +} diff --git a/sources/assets/css/responsive.css b/sources/assets/css/responsive.css index 38a5759..f319c34 100644 --- a/sources/assets/css/responsive.css +++ b/sources/assets/css/responsive.css @@ -10,6 +10,10 @@ font-size: 0.85em; } + .sidebar-content { + margin-left: 255px; + } + .form-tab { max-width: 404px; } @@ -22,4 +26,48 @@ .form-inline-group input[type="submit"] { margin-top: 20px; } + + td > input[type="text"] { + max-width: 150px; + } + + .task-time-form label { + display: block; + } + + .task-time-form input[type="submit"] { + margin-top: 10px; + display: block; + } +} + +@media only screen and (max-width: 1024px) and (orientation: landscape) { + + body { + font-size: 0.7em; + } + + header { + padding-bottom: 4px; + } + + .sidebar { + max-width: 220px; + } + + .sidebar-content { + margin-left: 225px; + } + + div.chosen-container { + font-size: 0.9em; + } + + input[type="number"], + input[type="date"], + input[type="email"], + input[type="password"], + input[type="text"] { + height: 18px; + } } diff --git a/sources/assets/css/sidebar.css b/sources/assets/css/sidebar.css index b8a9483..7618ad2 100644 --- a/sources/assets/css/sidebar.css +++ b/sources/assets/css/sidebar.css @@ -5,14 +5,14 @@ } .sidebar-content { - margin-left: 330px; + margin-left: 280px; } .sidebar { position: absolute; left: 0px; top: 0; - width: 250px; + max-width: 250px; padding: 10px; padding-top: 0; border: 1px solid #ddd; diff --git a/sources/assets/css/table.css b/sources/assets/css/table.css index 7c7be56..9bc0c71 100644 --- a/sources/assets/css/table.css +++ b/sources/assets/css/table.css @@ -82,6 +82,10 @@ th a:hover { width: 30%; } +.column-35 { + width: 35%; +} + .column-40 { width: 40%; } diff --git a/sources/assets/css/task.css b/sources/assets/css/task.css index a7a3ee5..9d88520 100644 --- a/sources/assets/css/task.css +++ b/sources/assets/css/task.css @@ -40,6 +40,15 @@ div.task-board-recent { font-size: 80%; } +.task-board-current-user a { + text-decoration: underline; +} + +.task-board-current-user a:focus, +.task-board-current-user a:hover { + text-decoration: none; +} + a.task-board-nobody { font-weight: normal; font-style: italic; diff --git a/sources/assets/js/app.js b/sources/assets/js/app.js index 0be4fca..6a8b910 100644 --- a/sources/assets/js/app.js +++ b/sources/assets/js/app.js @@ -1,4 +1,3 @@ -/* DO NOT EDIT: auto-generated file */ /*! jQuery v1.11.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ !function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.1",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
    ",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h; if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
    a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function ab(){return!0}function bb(){return!1}function cb(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),hb=/^\s+/,ib=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,jb=/<([\w:]+)/,kb=/\s*$/g,rb={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:k.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]},sb=db(y),tb=sb.appendChild(y.createElement("div"));rb.optgroup=rb.option,rb.tbody=rb.tfoot=rb.colgroup=rb.caption=rb.thead,rb.th=rb.td;function ub(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ub(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function vb(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wb(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xb(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function yb(a){var b=pb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function zb(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Ab(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Bb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xb(b).text=a.text,yb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!gb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(tb.innerHTML=a.outerHTML,tb.removeChild(f=tb.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ub(f),h=ub(a),g=0;null!=(e=h[g]);++g)d[g]&&Bb(e,d[g]);if(b)if(c)for(h=h||ub(a),d=d||ub(f),g=0;null!=(e=h[g]);g++)Ab(e,d[g]);else Ab(a,f);return d=ub(f,"script"),d.length>0&&zb(d,!i&&ub(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=db(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(lb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(jb.exec(f)||["",""])[1].toLowerCase(),l=rb[i]||rb._default,h.innerHTML=l[1]+f.replace(ib,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&hb.test(f)&&p.push(b.createTextNode(hb.exec(f)[0])),!k.tbody){f="table"!==i||kb.test(f)?""!==l[1]||kb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ub(p,"input"),vb),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ub(o.appendChild(f),"script"),g&&zb(h),c)){e=0;while(f=h[e++])ob.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ub(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&zb(ub(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ub(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fb,""):void 0;if(!("string"!=typeof a||mb.test(a)||!k.htmlSerialize&&gb.test(a)||!k.leadingWhitespace&&hb.test(a)||rb[(jb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ib,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ub(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ub(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&nb.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ub(i,"script"),xb),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ub(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,yb),j=0;f>j;j++)d=g[j],ob.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qb,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Cb,Db={};function Eb(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fb(a){var b=y,c=Db[a];return c||(c=Eb(a,b),"none"!==c&&c||(Cb=(Cb||m("