diff options
Diffstat (limited to 'app')
213 files changed, 6947 insertions, 2855 deletions
diff --git a/app/Api/Me.php b/app/Api/Me.php index ccc809ed..3d08626a 100644 --- a/app/Api/Me.php +++ b/app/Api/Me.php @@ -33,7 +33,7 @@ class Me extends Base public function getMyActivityStream() { $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); - return $this->projectActivity->getProjects($project_ids, 100); + return $this->helper->projectActivity->getProjectsEvents($project_ids, 100); } public function createMyPrivateProject($name, $description = null) diff --git a/app/Api/Project.php b/app/Api/Project.php index 8e311f7f..846d7046 100644 --- a/app/Api/Project.php +++ b/app/Api/Project.php @@ -53,13 +53,13 @@ class Project extends Base public function getProjectActivities(array $project_ids) { - return $this->projectActivity->getProjects($project_ids); + return $this->helper->projectActivity->getProjectsEvents($project_ids); } public function getProjectActivity($project_id) { $this->checkProjectPermission($project_id); - return $this->projectActivity->getProject($project_id); + return $this->helper->projectActivity->getProjectEvents($project_id); } public function createProject($name, $description = null) diff --git a/app/Api/User.php b/app/Api/User.php index 9b8081d6..6ee935a3 100644 --- a/app/Api/User.php +++ b/app/Api/User.php @@ -87,6 +87,7 @@ class User extends \Kanboard\Core\Base try { $ldap = LdapClient::connect(); + $ldap->setLogger($this->logger); $user = LdapUser::getUser($ldap, $username); if ($user === null) { diff --git a/app/Auth/LdapAuth.php b/app/Auth/LdapAuth.php index b4efbb55..c9423580 100644 --- a/app/Auth/LdapAuth.php +++ b/app/Auth/LdapAuth.php @@ -63,10 +63,12 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface try { $client = LdapClient::connect($this->getLdapUsername(), $this->getLdapPassword()); + $client->setLogger($this->logger); + $user = LdapUser::getUser($client, $this->username); if ($user === null) { - $this->logger->info('User not found in LDAP server'); + $this->logger->info('User ('.$this->username.') not found in LDAP server'); return false; } @@ -74,6 +76,8 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME'); } + $this->logger->info('Authenticate user: '.$user->getDn()); + if ($client->authenticate($user->getDn(), $this->password)) { $this->userInfo = $user; return true; diff --git a/app/Console/Base.php b/app/Console/BaseCommand.php index 25d48e44..23cdcc9c 100644 --- a/app/Console/Base.php +++ b/app/Console/BaseCommand.php @@ -11,6 +11,7 @@ use Symfony\Component\Console\Command\Command; * @package console * @author Frederic Guillot * + * @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator * @property \Kanboard\Export\SubtaskExport $subtaskExport * @property \Kanboard\Export\TaskExport $taskExport * @property \Kanboard\Export\TransitionExport $transitionExport @@ -21,11 +22,12 @@ use Symfony\Component\Console\Command\Command; * @property \Kanboard\Model\ProjectDailyStats $projectDailyStats * @property \Kanboard\Model\Task $task * @property \Kanboard\Model\TaskFinder $taskFinder + * @property \Kanboard\Model\User $user * @property \Kanboard\Model\UserNotification $userNotification * @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher */ -abstract class Base extends Command +abstract class BaseCommand extends Command { /** * Container instance diff --git a/app/Console/Cronjob.php b/app/Console/CronjobCommand.php index 3a5c5596..dae13af9 100644 --- a/app/Console/Cronjob.php +++ b/app/Console/CronjobCommand.php @@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\NullOutput; -class Cronjob extends Base +class CronjobCommand extends BaseCommand { private $commands = array( 'projects:daily-stats', diff --git a/app/Console/LocaleComparator.php b/app/Console/LocaleComparatorCommand.php index 8e5e0904..de83714f 100644 --- a/app/Console/LocaleComparator.php +++ b/app/Console/LocaleComparatorCommand.php @@ -7,7 +7,7 @@ use RecursiveDirectoryIterator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class LocaleComparator extends Base +class LocaleComparatorCommand extends BaseCommand { const REF_LOCALE = 'fr_FR'; diff --git a/app/Console/LocaleSync.php b/app/Console/LocaleSyncCommand.php index d62b40b5..11cfbde0 100644 --- a/app/Console/LocaleSync.php +++ b/app/Console/LocaleSyncCommand.php @@ -6,7 +6,7 @@ use DirectoryIterator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class LocaleSync extends Base +class LocaleSyncCommand extends BaseCommand { const REF_LOCALE = 'fr_FR'; diff --git a/app/Console/ProjectDailyColumnStatsExport.php b/app/Console/ProjectDailyColumnStatsExportCommand.php index 2513fbf1..ced1a374 100644 --- a/app/Console/ProjectDailyColumnStatsExport.php +++ b/app/Console/ProjectDailyColumnStatsExportCommand.php @@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class ProjectDailyColumnStatsExport extends Base +class ProjectDailyColumnStatsExportCommand extends BaseCommand { protected function configure() { diff --git a/app/Console/ProjectDailyStatsCalculation.php b/app/Console/ProjectDailyStatsCalculationCommand.php index 9884cc1c..5b898f02 100644 --- a/app/Console/ProjectDailyStatsCalculation.php +++ b/app/Console/ProjectDailyStatsCalculationCommand.php @@ -6,7 +6,7 @@ use Kanboard\Model\Project; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class ProjectDailyStatsCalculation extends Base +class ProjectDailyStatsCalculationCommand extends BaseCommand { protected function configure() { diff --git a/app/Console/ResetPasswordCommand.php b/app/Console/ResetPasswordCommand.php new file mode 100644 index 00000000..93dc3761 --- /dev/null +++ b/app/Console/ResetPasswordCommand.php @@ -0,0 +1,79 @@ +<?php + +namespace Kanboard\Console; + +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; + +class ResetPasswordCommand extends BaseCommand +{ + protected function configure() + { + $this + ->setName('user:reset-password') + ->setDescription('Change user password') + ->addArgument('username', InputArgument::REQUIRED, 'Username') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $helper = $this->getHelper('question'); + $username = $input->getArgument('username'); + + $passwordQuestion = new Question('What is the new password for '.$username.'? (characters are not printed)'.PHP_EOL); + $passwordQuestion->setHidden(true); + $passwordQuestion->setHiddenFallback(false); + + $password = $helper->ask($input, $output, $passwordQuestion); + + $confirmationQuestion = new Question('Confirmation:'.PHP_EOL); + $confirmationQuestion->setHidden(true); + $confirmationQuestion->setHiddenFallback(false); + + $confirmation = $helper->ask($input, $output, $confirmationQuestion); + + if ($this->validatePassword($output, $password, $confirmation)) { + $this->resetPassword($output, $username, $password); + } + } + + private function validatePassword(OutputInterface $output, $password, $confirmation) + { + list($valid, $errors) = $this->passwordResetValidator->validateModification(array( + 'password' => $password, + 'confirmation' => $confirmation, + )); + + if (!$valid) { + foreach ($errors as $error_list) { + foreach ($error_list as $error) { + $output->writeln('<error>'.$error.'</error>'); + } + } + } + + return $valid; + } + + private function resetPassword(OutputInterface $output, $username, $password) + { + $userId = $this->user->getIdByUsername($username); + + if (empty($userId)) { + $output->writeln('<error>User not found</error>'); + return false; + } + + if (!$this->user->update(array('id' => $userId, 'password' => $password))) { + $output->writeln('<error>Unable to update password</error>'); + return false; + } + + $output->writeln('<info>Password updated successfully</info>'); + + return true; + } +} diff --git a/app/Console/ResetTwoFactorCommand.php b/app/Console/ResetTwoFactorCommand.php new file mode 100644 index 00000000..3bf01e81 --- /dev/null +++ b/app/Console/ResetTwoFactorCommand.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Console; + +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ResetTwoFactorCommand extends BaseCommand +{ + protected function configure() + { + $this + ->setName('user:reset-2fa') + ->setDescription('Remove two-factor authentication for a user') + ->addArgument('username', InputArgument::REQUIRED, 'Username'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $username = $input->getArgument('username'); + $userId = $this->user->getIdByUsername($username); + + if (empty($userId)) { + $output->writeln('<error>User not found</error>'); + return false; + } + + if (!$this->user->update(array('id' => $userId, 'twofactor_activated' => 0, 'twofactor_secret' => ''))) { + $output->writeln('<error>Unable to update user profile</error>'); + return false; + } + + $output->writeln('<info>Two-factor authentication disabled</info>'); + + return true; + } +} diff --git a/app/Console/SubtaskExport.php b/app/Console/SubtaskExportCommand.php index aaa95276..986af1a4 100644 --- a/app/Console/SubtaskExport.php +++ b/app/Console/SubtaskExportCommand.php @@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class SubtaskExport extends Base +class SubtaskExportCommand extends BaseCommand { protected function configure() { diff --git a/app/Console/TaskExport.php b/app/Console/TaskExportCommand.php index 4515bf95..789245bc 100644 --- a/app/Console/TaskExport.php +++ b/app/Console/TaskExportCommand.php @@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class TaskExport extends Base +class TaskExportCommand extends BaseCommand { protected function configure() { diff --git a/app/Console/TaskOverdueNotification.php b/app/Console/TaskOverdueNotificationCommand.php index 43be4df8..7d176ab1 100644 --- a/app/Console/TaskOverdueNotification.php +++ b/app/Console/TaskOverdueNotificationCommand.php @@ -8,7 +8,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class TaskOverdueNotification extends Base +class TaskOverdueNotificationCommand extends BaseCommand { protected function configure() { diff --git a/app/Console/TaskTrigger.php b/app/Console/TaskTriggerCommand.php index 8d707211..9e9554f9 100644 --- a/app/Console/TaskTrigger.php +++ b/app/Console/TaskTriggerCommand.php @@ -7,7 +7,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Kanboard\Model\Task; use Kanboard\Event\TaskListEvent; -class TaskTrigger extends Base +class TaskTriggerCommand extends BaseCommand { protected function configure() { diff --git a/app/Console/TransitionExport.php b/app/Console/TransitionExportCommand.php index d9f805a4..265757b3 100644 --- a/app/Console/TransitionExport.php +++ b/app/Console/TransitionExportCommand.php @@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class TransitionExport extends Base +class TransitionExportCommand extends BaseCommand { protected function configure() { diff --git a/app/Controller/Activity.php b/app/Controller/Activity.php index db520ebe..47a66e0a 100644 --- a/app/Controller/Activity.php +++ b/app/Controller/Activity.php @@ -20,7 +20,7 @@ class Activity extends Base $project = $this->getProject(); $this->response->html($this->helper->layout->app('activity/project', array( - 'events' => $this->projectActivity->getProject($project['id']), + 'events' => $this->helper->projectActivity->getProjectEvents($project['id']), 'project' => $project, 'title' => t('%s\'s activity', $project['name']) ))); @@ -38,7 +38,8 @@ class Activity extends Base $this->response->html($this->helper->layout->task('activity/task', array( 'title' => $task['title'], 'task' => $task, - 'events' => $this->projectActivity->getTask($task['id']), + 'project' => $this->project->getById($task['project_id']), + 'events' => $this->helper->projectActivity->getTaskEvents($task['id']), ))); } } diff --git a/app/Controller/Analytic.php b/app/Controller/Analytic.php index 6ce062c4..35bc3048 100644 --- a/app/Controller/Analytic.php +++ b/app/Controller/Analytic.php @@ -2,6 +2,7 @@ namespace Kanboard\Controller; +use Kanboard\Filter\TaskProjectFilter; use Kanboard\Model\Task as TaskModel; /** @@ -44,14 +45,15 @@ class Analytic extends Base public function compareHours() { $project = $this->getProject(); - $params = $this->getProjectFilters('analytic', 'compareHours'); - $query = $this->taskFilter->create()->filterByProject($params['project']['id'])->getQuery(); $paginator = $this->paginator ->setUrl('analytic', 'compareHours', array('project_id' => $project['id'])) ->setMax(30) ->setOrder(TaskModel::TABLE.'.id') - ->setQuery($query) + ->setQuery($this->taskQuery + ->withFilter(new TaskProjectFilter($project['id'])) + ->getQuery() + ) ->calculate(); $this->response->html($this->helper->layout->analytic('analytic/compare_hours', array( diff --git a/app/Controller/App.php b/app/Controller/App.php index df1d3c90..01f733ff 100644 --- a/app/Controller/App.php +++ b/app/Controller/App.php @@ -157,7 +157,7 @@ class App extends Base $this->response->html($this->helper->layout->dashboard('app/activity', array( 'title' => t('My activity stream'), - 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id']), 100), + 'events' => $this->helper->projectActivity->getProjectsEvents($this->projectPermission->getActiveProjectIds($user['id']), 100), 'user' => $user, ))); } diff --git a/app/Controller/AvatarFile.php b/app/Controller/AvatarFile.php new file mode 100644 index 00000000..a47cca66 --- /dev/null +++ b/app/Controller/AvatarFile.php @@ -0,0 +1,92 @@ +<?php + +namespace Kanboard\Controller; + +use Kanboard\Core\ObjectStorage\ObjectStorageException; +use Kanboard\Core\Thumbnail; + +/** + * Avatar File Controller + * + * @package controller + * @author Frederic Guillot + */ +class AvatarFile extends Base +{ + /** + * Display avatar page + */ + public function show() + { + $user = $this->getUser(); + + $this->response->html($this->helper->layout->user('avatar_file/show', array( + 'user' => $user, + ))); + } + + /** + * Upload Avatar + */ + public function upload() + { + $user = $this->getUser(); + + if (! $this->avatarFile->uploadFile($user['id'], $this->request->getFileInfo('avatar'))) { + $this->flash->failure(t('Unable to upload the file.')); + } + + $this->response->redirect($this->helper->url->to('AvatarFile', 'show', array('user_id' => $user['id']))); + } + + /** + * Remove Avatar image + */ + public function remove() + { + $this->checkCSRFParam(); + $user = $this->getUser(); + $this->avatarFile->remove($user['id']); + $this->response->redirect($this->helper->url->to('AvatarFile', 'show', array('user_id' => $user['id']))); + } + + /** + * Show Avatar image (public) + */ + public function image() + { + $user_id = $this->request->getIntegerParam('user_id'); + $size = $this->request->getStringParam('size', 48); + $filename = $this->avatarFile->getFilename($user_id); + $etag = md5($filename.$size); + + $this->response->cache(365 * 86400, $etag); + $this->response->contentType('image/jpeg'); + + if ($this->request->getHeader('If-None-Match') !== '"'.$etag.'"') { + $this->render($filename, $size); + } else { + $this->response->status(304); + } + } + + /** + * Render thumbnail from object storage + * + * @access private + * @param string $filename + * @param integer $size + */ + private function render($filename, $size) + { + try { + $blob = $this->objectStorage->get($filename); + + Thumbnail::createFromString($blob) + ->resize($size, $size) + ->toOutput(); + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + } + } +} diff --git a/app/Controller/Base.php b/app/Controller/Base.php index 2453be18..beb56909 100644 --- a/app/Controller/Base.php +++ b/app/Controller/Base.php @@ -287,60 +287,4 @@ abstract class Base extends \Kanboard\Core\Base return $subtask; } - - /** - * Common method to get project filters - * - * @access protected - * @param string $controller - * @param string $action - * @return array - */ - protected function getProjectFilters($controller, $action) - { - $project = $this->getProject(); - $search = $this->request->getStringParam('search', $this->userSession->getFilters($project['id'])); - $board_selector = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); - unset($board_selector[$project['id']]); - - $filters = array( - 'controller' => $controller, - 'action' => $action, - 'project_id' => $project['id'], - 'search' => urldecode($search), - ); - - $this->userSession->setFilters($project['id'], $filters['search']); - - return array( - 'project' => $project, - 'board_selector' => $board_selector, - 'filters' => $filters, - 'title' => $project['name'], - 'description' => $this->getProjectDescription($project), - ); - } - - /** - * Get project description - * - * @access protected - * @param array &$project - * @return string - */ - protected function getProjectDescription(array &$project) - { - if ($project['owner_id'] > 0) { - $description = t('Project owner: ').'**'.$this->helper->text->e($project['owner_name'] ?: $project['owner_username']).'**'.PHP_EOL.PHP_EOL; - - if (! empty($project['description'])) { - $description .= '***'.PHP_EOL.PHP_EOL; - $description .= $project['description']; - } - } else { - $description = $project['description']; - } - - return $description; - } } diff --git a/app/Controller/Board.php b/app/Controller/Board.php index 199f1703..67e99b81 100644 --- a/app/Controller/Board.php +++ b/app/Controller/Board.php @@ -2,6 +2,8 @@ namespace Kanboard\Controller; +use Kanboard\Formatter\BoardFormatter; + /** * Board controller * @@ -47,16 +49,19 @@ class Board extends Base */ public function show() { - $params = $this->getProjectFilters('board', 'show'); + $project = $this->getProject(); + $search = $this->helper->projectHeader->getSearchQuery($project); $this->response->html($this->helper->layout->app('board/view_private', array( - 'categories_list' => $this->category->getList($params['project']['id'], false), - 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false), - 'custom_filters_list' => $this->customFilter->getAll($params['project']['id'], $this->userSession->getId()), - 'swimlanes' => $this->taskFilter->search($params['filters']['search'])->getBoard($params['project']['id']), + 'project' => $project, + 'title' => $project['name'], + 'description' => $this->helper->projectHeader->getDescription($project), 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), 'board_highlight_period' => $this->config->get('board_highlight_period'), - ) + $params)); + 'swimlanes' => $this->taskLexer + ->build($search) + ->format(BoardFormatter::getInstance($this->container)->setProjectId($project['id'])) + ))); } /** @@ -177,9 +182,11 @@ class Board extends Base { return $this->template->render('board/table_container', array( 'project' => $this->project->getById($project_id), - 'swimlanes' => $this->taskFilter->search($this->userSession->getFilters($project_id))->getBoard($project_id), 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), 'board_highlight_period' => $this->config->get('board_highlight_period'), + 'swimlanes' => $this->taskLexer + ->build($this->userSession->getFilters($project_id)) + ->format(BoardFormatter::getInstance($this->container)->setProjectId($project_id)) )); } } diff --git a/app/Controller/Calendar.php b/app/Controller/Calendar.php index a0a25e41..2517286d 100644 --- a/app/Controller/Calendar.php +++ b/app/Controller/Calendar.php @@ -2,6 +2,9 @@ namespace Kanboard\Controller; +use Kanboard\Filter\TaskAssigneeFilter; +use Kanboard\Filter\TaskProjectFilter; +use Kanboard\Filter\TaskStatusFilter; use Kanboard\Model\Task as TaskModel; /** @@ -20,9 +23,14 @@ class Calendar extends Base */ public function show() { + $project = $this->getProject(); + $this->response->html($this->helper->layout->app('calendar/show', array( + 'project' => $project, + 'title' => $project['name'], + 'description' => $this->helper->projectHeader->getDescription($project), 'check_interval' => $this->config->get('board_private_refresh_interval'), - ) + $this->getProjectFilters('calendar', 'show'))); + ))); } /** @@ -35,21 +43,11 @@ class Calendar extends Base $project_id = $this->request->getIntegerParam('project_id'); $start = $this->request->getStringParam('start'); $end = $this->request->getStringParam('end'); + $search = $this->userSession->getFilters($project_id); + $queryBuilder = $this->taskLexer->build($search)->withFilter(new TaskProjectFilter($project_id)); - // Common filter - $filter = $this->taskFilterCalendarFormatter - ->search($this->userSession->getFilters($project_id)) - ->filterByProject($project_id); - - // Tasks - if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') { - $events = $filter->copy()->filterByCreationDateRange($start, $end)->setColumns('date_creation', 'date_completed')->format(); - } else { - $events = $filter->copy()->filterByStartDateRange($start, $end)->setColumns('date_started', 'date_completed')->format(); - } - - // Tasks with due date - $events = array_merge($events, $filter->copy()->filterByDueDateRange($start, $end)->setColumns('date_due')->setFullDay()->format()); + $events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end); + $events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end)); $events = $this->hook->merge('controller:calendar:project:events', $events, array( 'project_id' => $project_id, @@ -70,21 +68,15 @@ class Calendar extends Base $user_id = $this->request->getIntegerParam('user_id'); $start = $this->request->getStringParam('start'); $end = $this->request->getStringParam('end'); - $filter = $this->taskFilterCalendarFormatter->create()->filterByOwner($user_id)->filterByStatus(TaskModel::STATUS_OPEN); - - // Task with due date - $events = $filter->copy()->filterByDueDateRange($start, $end)->setColumns('date_due')->setFullDay()->format(); + $queryBuilder = $this->taskQuery + ->withFilter(new TaskAssigneeFilter($user_id)) + ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN)); - // Tasks - if ($this->config->get('calendar_user_tasks', 'date_started') === 'date_creation') { - $events = array_merge($events, $filter->copy()->filterByCreationDateRange($start, $end)->setColumns('date_creation', 'date_completed')->format()); - } else { - $events = array_merge($events, $filter->copy()->filterByStartDateRange($start, $end)->setColumns('date_started', 'date_completed')->format()); - } + $events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end); + $events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end)); - // Subtasks time tracking if ($this->config->get('calendar_user_subtasks_time_tracking') == 1) { - $events = array_merge($events, $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end)); + $events = array_merge($events, $this->helper->calendar->getSubtaskTimeTrackingEvents($user_id, $start, $end)); } $events = $this->hook->merge('controller:calendar:user:events', $events, array( diff --git a/app/Controller/Doc.php b/app/Controller/Doc.php index f85326ac..00b9e585 100644 --- a/app/Controller/Doc.php +++ b/app/Controller/Doc.php @@ -5,54 +5,32 @@ namespace Kanboard\Controller; use Parsedown; /** - * Documentation controller + * Documentation Viewer * * @package controller * @author Frederic Guillot */ class Doc extends Base { - private function readFile($filename) - { - $url = $this->helper->url; - $data = file_get_contents($filename); - list($title, ) = explode("\n", $data, 2); - - $replaceUrl = function (array $matches) use ($url) { - return '('.$url->to('doc', 'show', array('file' => str_replace('.markdown', '', $matches[1]))).')'; - }; - - $content = preg_replace_callback('/\((.*.markdown)\)/', $replaceUrl, $data); - - return array( - 'content' => Parsedown::instance()->text($content), - 'title' => $title !== 'Documentation' ? t('Documentation: %s', $title) : $title, - ); - } - public function show() { $page = $this->request->getStringParam('file', 'index'); - if (! preg_match('/^[a-z0-9\-]+/', $page)) { + if (!preg_match('/^[a-z0-9\-]+/', $page)) { $page = 'index'; } - $filenames = array(__DIR__.'/../../doc/'.$page.'.markdown'); - $filename = __DIR__.'/../../doc/index.markdown'; - if ($this->config->getCurrentLanguage() === 'fr_FR') { - array_unshift($filenames, __DIR__.'/../../doc/fr/'.$page.'.markdown'); + $filename = __DIR__.'/../../doc/fr/' . $page . '.markdown'; + } else { + $filename = __DIR__ . '/../../doc/' . $page . '.markdown'; } - foreach ($filenames as $file) { - if (file_exists($file)) { - $filename = $file; - break; - } + if (!file_exists($filename)) { + $filename = __DIR__.'/../../doc/index.markdown'; } - $this->response->html($this->helper->layout->app('doc/show', $this->readFile($filename))); + $this->response->html($this->helper->layout->app('doc/show', $this->render($filename))); } /** @@ -62,4 +40,53 @@ class Doc extends Base { $this->response->html($this->template->render('config/keyboard_shortcuts')); } + + /** + * Prepare Markdown file + * + * @access private + * @param string $filename + * @return array + */ + private function render($filename) + { + $data = file_get_contents($filename); + $content = preg_replace_callback('/\((.*.markdown)\)/', array($this, 'replaceMarkdownUrl'), $data); + $content = preg_replace_callback('/\((screenshots.*\.png)\)/', array($this, 'replaceImageUrl'), $content); + + list($title, ) = explode("\n", $data, 2); + + return array( + 'content' => Parsedown::instance()->text($content), + 'title' => $title !== 'Documentation' ? t('Documentation: %s', $title) : $title, + ); + } + + /** + * Regex callback to replace Markdown links + * + * @access public + * @param array $matches + * @return string + */ + public function replaceMarkdownUrl(array $matches) + { + return '('.$this->helper->url->to('doc', 'show', array('file' => str_replace('.markdown', '', $matches[1]))).')'; + } + + /** + * Regex callback to replace image links + * + * @access public + * @param array $matches + * @return string + */ + public function replaceImageUrl(array $matches) + { + if ($this->config->getCurrentLanguage() === 'fr_FR') { + return '('.$this->helper->url->base().'doc/fr/'.$matches[1].')'; + } + + return '('.$this->helper->url->base().'doc/'.$matches[1].')'; + } } diff --git a/app/Controller/Feed.php b/app/Controller/Feed.php index 8457c383..f8b3d320 100644 --- a/app/Controller/Feed.php +++ b/app/Controller/Feed.php @@ -26,7 +26,7 @@ class Feed extends Base } $this->response->xml($this->template->render('feed/user', array( - 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id'])), + 'events' => $this->helper->projectActivity->getProjectsEvents($this->projectPermission->getActiveProjectIds($user['id'])), 'user' => $user, ))); } @@ -47,7 +47,7 @@ class Feed extends Base } $this->response->xml($this->template->render('feed/project', array( - 'events' => $this->projectActivity->getProject($project['id']), + 'events' => $this->helper->projectActivity->getProjectEvents($project['id']), 'project' => $project, ))); } diff --git a/app/Controller/FileViewer.php b/app/Controller/FileViewer.php index bc91c3d8..3be4ea14 100644 --- a/app/Controller/FileViewer.php +++ b/app/Controller/FileViewer.php @@ -66,9 +66,16 @@ class FileViewer extends Base */ public function image() { + $file = $this->getFile(); + $etag = md5($file['path']); + $this->response->contentType($this->helper->file->getImageMimeType($file['name'])); + $this->response->cache(5 * 86400, $etag); + + if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') { + return $this->response->status(304); + } + try { - $file = $this->getFile(); - $this->response->contentType($this->helper->file->getImageMimeType($file['name'])); $this->objectStorage->output($file['path']); } catch (ObjectStorageException $e) { $this->logger->error($e->getMessage()); @@ -82,12 +89,21 @@ class FileViewer extends Base */ public function thumbnail() { + $file = $this->getFile(); + $model = $file['model']; + $filename = $this->$model->getThumbnailPath($file['path']); + $etag = md5($filename); + + $this->response->cache(5 * 86400, $etag); $this->response->contentType('image/jpeg'); + if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') { + return $this->response->status(304); + } + try { - $file = $this->getFile(); - $model = $file['model']; - $this->objectStorage->output($this->$model->getThumbnailPath($file['path'])); + + $this->objectStorage->output($filename); } catch (ObjectStorageException $e) { $this->logger->error($e->getMessage()); diff --git a/app/Controller/Gantt.php b/app/Controller/Gantt.php index 9ffa277f..5e9ad55e 100644 --- a/app/Controller/Gantt.php +++ b/app/Controller/Gantt.php @@ -2,7 +2,14 @@ namespace Kanboard\Controller; +use Kanboard\Filter\ProjectIdsFilter; +use Kanboard\Filter\ProjectStatusFilter; +use Kanboard\Filter\ProjectTypeFilter; +use Kanboard\Filter\TaskProjectFilter; +use Kanboard\Formatter\ProjectGanttFormatter; +use Kanboard\Formatter\TaskGanttFormatter; use Kanboard\Model\Task as TaskModel; +use Kanboard\Model\Project as ProjectModel; /** * Gantt controller @@ -17,14 +24,16 @@ class Gantt extends Base */ public function projects() { - if ($this->userSession->isAdmin()) { - $project_ids = $this->project->getAllIds(); - } else { - $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); - } + $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); + $filter = $this->projectQuery + ->withFilter(new ProjectTypeFilter(ProjectModel::TYPE_TEAM)) + ->withFilter(new ProjectStatusFilter(ProjectModel::ACTIVE)) + ->withFilter(new ProjectIdsFilter($project_ids)); + + $filter->getQuery()->asc(ProjectModel::TABLE.'.start_date'); $this->response->html($this->helper->layout->app('gantt/projects', array( - 'projects' => $this->projectGanttFormatter->filter($project_ids)->format(), + 'projects' => $filter->format(new ProjectGanttFormatter($this->container)), 'title' => t('Gantt chart for all projects'), ))); } @@ -54,9 +63,10 @@ class Gantt extends Base */ public function project() { - $params = $this->getProjectFilters('gantt', 'project'); - $filter = $this->taskFilterGanttFormatter->search($params['filters']['search'])->filterByProject($params['project']['id']); + $project = $this->getProject(); + $search = $this->helper->projectHeader->getSearchQuery($project); $sorting = $this->request->getStringParam('sorting', 'board'); + $filter = $this->taskLexer->build($search)->withFilter(new TaskProjectFilter($project['id'])); if ($sorting === 'date') { $filter->getQuery()->asc(TaskModel::TABLE.'.date_started')->asc(TaskModel::TABLE.'.date_creation'); @@ -64,10 +74,12 @@ class Gantt extends Base $filter->getQuery()->asc('column_position')->asc(TaskModel::TABLE.'.position'); } - $this->response->html($this->helper->layout->app('gantt/project', $params + array( - 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false), + $this->response->html($this->helper->layout->app('gantt/project', array( + 'project' => $project, + 'title' => $project['name'], + 'description' => $this->helper->projectHeader->getDescription($project), 'sorting' => $sorting, - 'tasks' => $filter->format(), + 'tasks' => $filter->format(new TaskGanttFormatter($this->container)), ))); } diff --git a/app/Controller/GroupHelper.php b/app/Controller/GroupHelper.php index 34f522a6..429614c2 100644 --- a/app/Controller/GroupHelper.php +++ b/app/Controller/GroupHelper.php @@ -2,6 +2,8 @@ namespace Kanboard\Controller; +use Kanboard\Formatter\GroupAutoCompleteFormatter; + /** * Group Helper * @@ -11,14 +13,14 @@ namespace Kanboard\Controller; class GroupHelper extends Base { /** - * Group autocompletion (Ajax) + * Group auto-completion (Ajax) * * @access public */ public function autocomplete() { $search = $this->request->getStringParam('term'); - $groups = $this->groupManager->find($search); - $this->response->json($this->groupAutoCompleteFormatter->setGroups($groups)->format()); + $formatter = new GroupAutoCompleteFormatter($this->groupManager->find($search)); + $this->response->json($formatter->format()); } } diff --git a/app/Controller/Ical.php b/app/Controller/Ical.php index f1ea6d8f..8fe97b46 100644 --- a/app/Controller/Ical.php +++ b/app/Controller/Ical.php @@ -2,7 +2,11 @@ namespace Kanboard\Controller; -use Kanboard\Model\TaskFilter; +use Kanboard\Core\Filter\QueryBuilder; +use Kanboard\Filter\TaskAssigneeFilter; +use Kanboard\Filter\TaskProjectFilter; +use Kanboard\Filter\TaskStatusFilter; +use Kanboard\Formatter\TaskICalFormatter; use Kanboard\Model\Task as TaskModel; use Eluceo\iCal\Component\Calendar as iCalendar; @@ -30,10 +34,11 @@ class Ical extends Base } // Common filter - $filter = $this->taskFilterICalendarFormatter - ->create() - ->filterByStatus(TaskModel::STATUS_OPEN) - ->filterByOwner($user['id']); + $queryBuilder = new QueryBuilder(); + $queryBuilder + ->withQuery($this->taskFinder->getICalQuery()) + ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN)) + ->withFilter(new TaskAssigneeFilter($user['id'])); // Calendar properties $calendar = new iCalendar('Kanboard'); @@ -41,7 +46,7 @@ class Ical extends Base $calendar->setDescription($user['name'] ?: $user['username']); $calendar->setPublishedTTL('PT1H'); - $this->renderCalendar($filter, $calendar); + $this->renderCalendar($queryBuilder, $calendar); } /** @@ -60,10 +65,11 @@ class Ical extends Base } // Common filter - $filter = $this->taskFilterICalendarFormatter - ->create() - ->filterByStatus(TaskModel::STATUS_OPEN) - ->filterByProject($project['id']); + $queryBuilder = new QueryBuilder(); + $queryBuilder + ->withQuery($this->taskFinder->getICalQuery()) + ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN)) + ->withFilter(new TaskProjectFilter($project['id'])); // Calendar properties $calendar = new iCalendar('Kanboard'); @@ -71,7 +77,7 @@ class Ical extends Base $calendar->setDescription($project['name']); $calendar->setPublishedTTL('PT1H'); - $this->renderCalendar($filter, $calendar); + $this->renderCalendar($queryBuilder, $calendar); } /** @@ -79,37 +85,14 @@ class Ical extends Base * * @access private */ - private function renderCalendar(TaskFilter $filter, iCalendar $calendar) + private function renderCalendar(QueryBuilder $queryBuilder, iCalendar $calendar) { $start = $this->request->getStringParam('start', strtotime('-2 month')); $end = $this->request->getStringParam('end', strtotime('+6 months')); - // Tasks - if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') { - $filter - ->copy() - ->filterByCreationDateRange($start, $end) - ->setColumns('date_creation', 'date_completed') - ->setCalendar($calendar) - ->addDateTimeEvents(); - } else { - $filter - ->copy() - ->filterByStartDateRange($start, $end) - ->setColumns('date_started', 'date_completed') - ->setCalendar($calendar) - ->addDateTimeEvents($calendar); - } - - // Tasks with due date - $filter - ->copy() - ->filterByDueDateRange($start, $end) - ->setColumns('date_due') - ->setCalendar($calendar) - ->addFullDayEvents($calendar); + $this->helper->ical->addTaskDateDueEvents($queryBuilder, $calendar, $start, $end); - $this->response->contentType('text/calendar; charset=utf-8'); - echo $filter->setCalendar($calendar)->format(); + $formatter = new TaskICalFormatter($this->container); + $this->response->ical($formatter->setCalendar($calendar)->format()); } } diff --git a/app/Controller/Listing.php b/app/Controller/Listing.php index c784dd50..2024ff03 100644 --- a/app/Controller/Listing.php +++ b/app/Controller/Listing.php @@ -2,6 +2,7 @@ namespace Kanboard\Controller; +use Kanboard\Filter\TaskProjectFilter; use Kanboard\Model\Task as TaskModel; /** @@ -19,22 +20,26 @@ class Listing extends Base */ public function show() { - $params = $this->getProjectFilters('listing', 'show'); - $query = $this->taskFilter->search($params['filters']['search'])->filterByProject($params['project']['id'])->getQuery(); + $project = $this->getProject(); + $search = $this->helper->projectHeader->getSearchQuery($project); $paginator = $this->paginator - ->setUrl('listing', 'show', array('project_id' => $params['project']['id'])) + ->setUrl('listing', 'show', array('project_id' => $project['id'])) ->setMax(30) ->setOrder(TaskModel::TABLE.'.id') ->setDirection('DESC') - ->setQuery($query) + ->setQuery($this->taskLexer + ->build($search) + ->withFilter(new TaskProjectFilter($project['id'])) + ->getQuery() + ) ->calculate(); - $this->response->html($this->helper->layout->app('listing/show', $params + array( + $this->response->html($this->helper->layout->app('listing/show', array( + 'project' => $project, + 'title' => $project['name'], + 'description' => $this->helper->projectHeader->getDescription($project), 'paginator' => $paginator, - 'categories_list' => $this->category->getList($params['project']['id'], false), - 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false), - 'custom_filters_list' => $this->customFilter->getAll($params['project']['id'], $this->userSession->getId()), ))); } } diff --git a/app/Controller/Oauth.php b/app/Controller/Oauth.php index 452faecd..12b91144 100644 --- a/app/Controller/Oauth.php +++ b/app/Controller/Oauth.php @@ -2,6 +2,8 @@ namespace Kanboard\Controller; +use Kanboard\Core\Security\OAuthAuthenticationProviderInterface; + /** * OAuth controller * @@ -11,25 +13,6 @@ namespace Kanboard\Controller; class Oauth extends Base { /** - * Unlink external account - * - * @access public - */ - public function unlink() - { - $backend = $this->request->getStringParam('backend'); - $this->checkCSRFParam(); - - if ($this->authenticationManager->getProvider($backend)->unlink($this->userSession->getId())) { - $this->flash->success(t('Your external account is not linked anymore to your profile.')); - } else { - $this->flash->failure(t('Unable to unlink your external account.')); - } - - $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId()))); - } - - /** * Redirect to the provider if no code received * * @access private @@ -38,9 +21,10 @@ class Oauth extends Base protected function step1($provider) { $code = $this->request->getStringParam('code'); + $state = $this->request->getStringParam('state'); if (! empty($code)) { - $this->step2($provider, $code); + $this->step2($provider, $code, $state); } else { $this->response->redirect($this->authenticationManager->getProvider($provider)->getService()->getAuthorizationUrl()); } @@ -50,34 +34,44 @@ class Oauth extends Base * Link or authenticate the user * * @access protected - * @param string $provider + * @param string $providerName * @param string $code + * @param string $state */ - protected function step2($provider, $code) + protected function step2($providerName, $code, $state) { - $this->authenticationManager->getProvider($provider)->setCode($code); + $provider = $this->authenticationManager->getProvider($providerName); + $provider->setCode($code); + $hasValidState = $provider->getService()->isValidateState($state); if ($this->userSession->isLogged()) { - $this->link($provider); + if ($hasValidState) { + $this->link($provider); + } else { + $this->flash->failure(t('The OAuth2 state parameter is invalid')); + $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId()))); + } + } else { + if ($hasValidState) { + $this->authenticate($providerName); + } else { + $this->authenticationFailure(t('The OAuth2 state parameter is invalid')); + } } - - $this->authenticate($provider); } /** * Link the account * * @access protected - * @param string $provider + * @param OAuthAuthenticationProviderInterface $provider */ - protected function link($provider) + protected function link(OAuthAuthenticationProviderInterface $provider) { - $authProvider = $this->authenticationManager->getProvider($provider); - - if (! $authProvider->authenticate()) { + if (! $provider->authenticate()) { $this->flash->failure(t('External authentication failed')); } else { - $this->userProfile->assign($this->userSession->getId(), $authProvider->getUser()); + $this->userProfile->assign($this->userSession->getId(), $provider->getUser()); $this->flash->success(t('Your external account is linked to your profile successfully.')); } @@ -85,22 +79,52 @@ class Oauth extends Base } /** + * Unlink external account + * + * @access public + */ + public function unlink() + { + $backend = $this->request->getStringParam('backend'); + $this->checkCSRFParam(); + + if ($this->authenticationManager->getProvider($backend)->unlink($this->userSession->getId())) { + $this->flash->success(t('Your external account is not linked anymore to your profile.')); + } else { + $this->flash->failure(t('Unable to unlink your external account.')); + } + + $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId()))); + } + + /** * Authenticate the account * * @access protected - * @param string $provider + * @param string $providerName */ - protected function authenticate($provider) + protected function authenticate($providerName) { - if ($this->authenticationManager->oauthAuthentication($provider)) { + if ($this->authenticationManager->oauthAuthentication($providerName)) { $this->response->redirect($this->helper->url->to('app', 'index')); } else { - $this->response->html($this->helper->layout->app('auth/index', array( - 'errors' => array('login' => t('External authentication failed')), - 'values' => array(), - 'no_layout' => true, - 'title' => t('Login') - ))); + $this->authenticationFailure(t('External authentication failed')); } } + + /** + * Show login failure page + * + * @access protected + * @param string $message + */ + protected function authenticationFailure($message) + { + $this->response->html($this->helper->layout->app('auth/index', array( + 'errors' => array('login' => $message), + 'values' => array(), + 'no_layout' => true, + 'title' => t('Login') + ))); + } } diff --git a/app/Controller/ProjectOverview.php b/app/Controller/ProjectOverview.php index b0687ed3..b2bb33d6 100644 --- a/app/Controller/ProjectOverview.php +++ b/app/Controller/ProjectOverview.php @@ -15,15 +15,18 @@ class ProjectOverview extends Base */ public function show() { - $params = $this->getProjectFilters('ProjectOverview', 'show'); - $params['users'] = $this->projectUserRole->getAllUsersGroupedByRole($params['project']['id']); - $params['roles'] = $this->role->getProjectRoles(); - $params['events'] = $this->projectActivity->getProject($params['project']['id'], 10); - $params['images'] = $this->projectFile->getAllImages($params['project']['id']); - $params['files'] = $this->projectFile->getAllDocuments($params['project']['id']); + $project = $this->getProject(); + $this->project->getColumnStats($project); - $this->project->getColumnStats($params['project']); - - $this->response->html($this->helper->layout->app('project_overview/show', $params)); + $this->response->html($this->helper->layout->app('project_overview/show', array( + 'project' => $project, + 'title' => $project['name'], + 'description' => $this->helper->projectHeader->getDescription($project), + 'users' => $this->projectUserRole->getAllUsersGroupedByRole($project['id']), + 'roles' => $this->role->getProjectRoles(), + 'events' => $this->helper->projectActivity->getProjectEvents($project['id'], 10), + 'images' => $this->projectFile->getAllImages($project['id']), + 'files' => $this->projectFile->getAllDocuments($project['id']), + ))); } } diff --git a/app/Controller/ProjectPermission.php b/app/Controller/ProjectPermission.php index 800da02f..e203c0db 100644 --- a/app/Controller/ProjectPermission.php +++ b/app/Controller/ProjectPermission.php @@ -83,7 +83,9 @@ class ProjectPermission extends Base $project = $this->getProject(); $values = $this->request->getValues(); - if ($this->projectUserRole->addUser($values['project_id'], $values['user_id'], $values['role'])) { + if (empty($values['user_id'])) { + $this->flash->failure(t('User not found.')); + } elseif ($this->projectUserRole->addUser($values['project_id'], $values['user_id'], $values['role'])) { $this->flash->success(t('Project updated successfully.')); } else { $this->flash->failure(t('Unable to update this project.')); diff --git a/app/Controller/Search.php b/app/Controller/Search.php index 9b9b9e65..a42e9d3d 100644 --- a/app/Controller/Search.php +++ b/app/Controller/Search.php @@ -2,6 +2,8 @@ namespace Kanboard\Controller; +use Kanboard\Filter\TaskProjectsFilter; + /** * Search controller * @@ -23,14 +25,12 @@ class Search extends Base ->setDirection('DESC'); if ($search !== '' && ! empty($projects)) { - $query = $this - ->taskFilter - ->search($search) - ->filterByProjects(array_keys($projects)) - ->getQuery(); - $paginator - ->setQuery($query) + ->setQuery($this->taskLexer + ->build($search) + ->withFilter(new TaskProjectsFilter(array_keys($projects))) + ->getQuery() + ) ->calculate(); $nb_tasks = $paginator->getTotal(); @@ -46,4 +46,22 @@ class Search extends Base 'title' => t('Search tasks').($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '') ))); } + + public function activity() + { + $search = urldecode($this->request->getStringParam('search')); + $events = $this->helper->projectActivity->searchEvents($search); + $nb_events = count($events); + + $this->response->html($this->helper->layout->app('search/activity', array( + 'values' => array( + 'search' => $search, + 'controller' => 'search', + 'action' => 'activity', + ), + 'title' => t('Search in activity stream').($nb_events > 0 ? ' ('.$nb_events.')' : ''), + 'nb_events' => $nb_events, + 'events' => $events, + ))); + } } diff --git a/app/Controller/Task.php b/app/Controller/Task.php index dc10604e..902a32d6 100644 --- a/app/Controller/Task.php +++ b/app/Controller/Task.php @@ -71,17 +71,16 @@ class Task extends Base $values = $this->dateParser->format($values, array('date_started'), $this->config->get('application_datetime_format', DateParser::DATE_TIME_FORMAT)); $this->response->html($this->helper->layout->task('task/show', array( + 'task' => $task, 'project' => $this->project->getById($task['project_id']), + 'values' => $values, 'files' => $this->taskFile->getAllDocuments($task['id']), 'images' => $this->taskFile->getAllImages($task['id']), 'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting()), 'subtasks' => $subtasks, 'internal_links' => $this->taskLink->getAllGroupedByLabel($task['id']), 'external_links' => $this->taskExternalLink->getAll($task['id']), - 'task' => $task, - 'values' => $values, 'link_label_list' => $this->link->getList(0, false), - 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id'], true, false, false), ))); } @@ -96,6 +95,7 @@ class Task extends Base $this->response->html($this->helper->layout->task('task/analytics', array( 'task' => $task, + 'project' => $this->project->getById($task['project_id']), 'lead_time' => $this->taskAnalytic->getLeadTime($task), 'cycle_time' => $this->taskAnalytic->getCycleTime($task), 'time_spent_columns' => $this->taskAnalytic->getTimeSpentByColumn($task), @@ -121,6 +121,7 @@ class Task extends Base $this->response->html($this->helper->layout->task('task/time_tracking_details', array( 'task' => $task, + 'project' => $this->project->getById($task['project_id']), 'subtask_paginator' => $subtask_paginator, ))); } @@ -136,6 +137,7 @@ class Task extends Base $this->response->html($this->helper->layout->task('task/transitions', array( 'task' => $task, + 'project' => $this->project->getById($task['project_id']), 'transitions' => $this->transition->getAllByTask($task['id']), ))); } diff --git a/app/Controller/TaskHelper.php b/app/Controller/TaskHelper.php index 7e340a6a..6835ab2b 100644 --- a/app/Controller/TaskHelper.php +++ b/app/Controller/TaskHelper.php @@ -2,6 +2,12 @@ namespace Kanboard\Controller; +use Kanboard\Filter\TaskIdExclusionFilter; +use Kanboard\Filter\TaskIdFilter; +use Kanboard\Filter\TaskProjectsFilter; +use Kanboard\Filter\TaskTitleFilter; +use Kanboard\Formatter\TaskAutoCompleteFormatter; + /** * Task Ajax Helper * @@ -11,31 +17,33 @@ namespace Kanboard\Controller; class TaskHelper extends Base { /** - * Task autocompletion (Ajax) + * Task auto-completion (Ajax) * * @access public */ public function autocomplete() { $search = $this->request->getStringParam('term'); - $projects = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); + $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); + $exclude_task_id = $this->request->getIntegerParam('exclude_task_id'); - if (empty($projects)) { + if (empty($project_ids)) { $this->response->json(array()); - } + } else { - $filter = $this->taskFilterAutoCompleteFormatter - ->create() - ->filterByProjects($projects) - ->excludeTasks(array($this->request->getIntegerParam('exclude_task_id'))); + $filter = $this->taskQuery->withFilter(new TaskProjectsFilter($project_ids)); - // Search by task id or by title - if (ctype_digit($search)) { - $filter->filterById($search); - } else { - $filter->filterByTitle($search); - } + if (! empty($exclude_task_id)) { + $filter->withFilter(new TaskIdExclusionFilter(array($exclude_task_id))); + } + + if (ctype_digit($search)) { + $filter->withFilter(new TaskIdFilter($search)); + } else { + $filter->withFilter(new TaskTitleFilter($search)); + } - $this->response->json($filter->format()); + $this->response->json($filter->format(new TaskAutoCompleteFormatter($this->container))); + } } } diff --git a/app/Controller/UserHelper.php b/app/Controller/UserHelper.php index 041ed2c8..47bbe554 100644 --- a/app/Controller/UserHelper.php +++ b/app/Controller/UserHelper.php @@ -2,6 +2,10 @@ namespace Kanboard\Controller; +use Kanboard\Filter\UserNameFilter; +use Kanboard\Formatter\UserAutoCompleteFormatter; +use Kanboard\Model\User as UserModel; + /** * User Helper * @@ -11,19 +15,20 @@ namespace Kanboard\Controller; class UserHelper extends Base { /** - * User autocompletion (Ajax) + * User auto-completion (Ajax) * * @access public */ public function autocomplete() { $search = $this->request->getStringParam('term'); - $users = $this->userFilterAutoCompleteFormatter->create($search)->filterByUsernameOrByName()->format(); - $this->response->json($users); + $filter = $this->userQuery->withFilter(new UserNameFilter($search)); + $filter->getQuery()->asc(UserModel::TABLE.'.name')->asc(UserModel::TABLE.'.username'); + $this->response->json($filter->format(new UserAutoCompleteFormatter($this->container))); } /** - * User mention autocompletion (Ajax) + * User mention auto-completion (Ajax) * * @access public */ diff --git a/app/Core/Action/ActionManager.php b/app/Core/Action/ActionManager.php index f1ea8abe..dfa5a140 100644 --- a/app/Core/Action/ActionManager.php +++ b/app/Core/Action/ActionManager.php @@ -18,7 +18,7 @@ class ActionManager extends Base * List of automatic actions * * @access private - * @var array + * @var ActionBase[] */ private $actions = array(); diff --git a/app/Core/Base.php b/app/Core/Base.php index f87f271a..2b619af5 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -48,18 +48,11 @@ use Pimple\Container; * @property \Kanboard\Core\User\UserSession $userSession * @property \Kanboard\Core\DateParser $dateParser * @property \Kanboard\Core\Helper $helper - * @property \Kanboard\Core\Lexer $lexer * @property \Kanboard\Core\Paginator $paginator * @property \Kanboard\Core\Template $template - * @property \Kanboard\Formatter\ProjectGanttFormatter $projectGanttFormatter - * @property \Kanboard\Formatter\TaskFilterGanttFormatter $taskFilterGanttFormatter - * @property \Kanboard\Formatter\TaskFilterAutoCompleteFormatter $taskFilterAutoCompleteFormatter - * @property \Kanboard\Formatter\TaskFilterCalendarFormatter $taskFilterCalendarFormatter - * @property \Kanboard\Formatter\TaskFilterICalendarFormatter $taskFilterICalendarFormatter - * @property \Kanboard\Formatter\UserFilterAutoCompleteFormatter $userFilterAutoCompleteFormatter - * @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter * @property \Kanboard\Model\Action $action * @property \Kanboard\Model\ActionParameter $actionParameter + * @property \Kanboard\Model\AvatarFile $avatarFile * @property \Kanboard\Model\Board $board * @property \Kanboard\Model\Category $category * @property \Kanboard\Model\Color $color @@ -84,7 +77,6 @@ use Pimple\Container; * @property \Kanboard\Model\ProjectMetadata $projectMetadata * @property \Kanboard\Model\ProjectPermission $projectPermission * @property \Kanboard\Model\ProjectUserRole $projectUserRole - * @property \Kanboard\Model\projectUserRoleFilter $projectUserRoleFilter * @property \Kanboard\Model\ProjectGroupRole $projectGroupRole * @property \Kanboard\Model\ProjectNotification $projectNotification * @property \Kanboard\Model\ProjectNotificationType $projectNotificationType @@ -98,7 +90,6 @@ use Pimple\Container; * @property \Kanboard\Model\TaskDuplication $taskDuplication * @property \Kanboard\Model\TaskExternalLink $taskExternalLink * @property \Kanboard\Model\TaskFinder $taskFinder - * @property \Kanboard\Model\TaskFilter $taskFilter * @property \Kanboard\Model\TaskLink $taskLink * @property \Kanboard\Model\TaskModification $taskModification * @property \Kanboard\Model\TaskPermission $taskPermission @@ -136,6 +127,14 @@ use Pimple\Container; * @property \Kanboard\Export\SubtaskExport $subtaskExport * @property \Kanboard\Export\TaskExport $taskExport * @property \Kanboard\Export\TransitionExport $transitionExport + * @property \Kanboard\Core\Filter\QueryBuilder $projectGroupRoleQuery + * @property \Kanboard\Core\Filter\QueryBuilder $projectUserRoleQuery + * @property \Kanboard\Core\Filter\QueryBuilder $projectActivityQuery + * @property \Kanboard\Core\Filter\QueryBuilder $userQuery + * @property \Kanboard\Core\Filter\QueryBuilder $projectQuery + * @property \Kanboard\Core\Filter\QueryBuilder $taskQuery + * @property \Kanboard\Core\Filter\LexerBuilder $taskLexer + * @property \Kanboard\Core\Filter\LexerBuilder $projectActivityLexer * @property \Psr\Log\LoggerInterface $logger * @property \PicoDb\Database $db * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher @@ -172,4 +171,18 @@ abstract class Base { return $this->container[$name]; } + + /** + * Get object instance + * + * @static + * @access public + * @param Container $container + * @return static + */ + public static function getInstance(Container $container) + { + $self = new static($container); + return $self; + } } diff --git a/app/Core/Cache/Base.php b/app/Core/Cache/Base.php index 2879f1f1..d62b8507 100644 --- a/app/Core/Cache/Base.php +++ b/app/Core/Cache/Base.php @@ -11,26 +11,6 @@ namespace Kanboard\Core\Cache; abstract class Base { /** - * Fetch value from cache - * - * @abstract - * @access public - * @param string $key - * @return mixed Null when not found, cached value otherwise - */ - abstract public function get($key); - - /** - * Save a new value in the cache - * - * @abstract - * @access public - * @param string $key - * @param mixed $value - */ - abstract public function set($key, $value); - - /** * Proxy cache * * Note: Arguments must be scalar types diff --git a/app/Core/ExternalLink/ExternalLinkManager.php b/app/Core/ExternalLink/ExternalLinkManager.php index 1fa423c2..804e6b34 100644 --- a/app/Core/ExternalLink/ExternalLinkManager.php +++ b/app/Core/ExternalLink/ExternalLinkManager.php @@ -23,7 +23,7 @@ class ExternalLinkManager extends Base * Registered providers * * @access private - * @var array + * @var ExternalLinkProviderInterface[] */ private $providers = array(); diff --git a/app/Core/Filter/CriteriaInterface.php b/app/Core/Filter/CriteriaInterface.php new file mode 100644 index 00000000..009c4bd3 --- /dev/null +++ b/app/Core/Filter/CriteriaInterface.php @@ -0,0 +1,40 @@ +<?php + +namespace Kanboard\Core\Filter; + +use PicoDb\Table; + +/** + * Criteria Interface + * + * @package filter + * @author Frederic Guillot + */ +interface CriteriaInterface +{ + /** + * Set the Query + * + * @access public + * @param Table $query + * @return CriteriaInterface + */ + public function withQuery(Table $query); + + /** + * Set filter + * + * @access public + * @param FilterInterface $filter + * @return CriteriaInterface + */ + public function withFilter(FilterInterface $filter); + + /** + * Apply condition + * + * @access public + * @return CriteriaInterface + */ + public function apply(); +} diff --git a/app/Core/Filter/FilterInterface.php b/app/Core/Filter/FilterInterface.php new file mode 100644 index 00000000..7b66ec28 --- /dev/null +++ b/app/Core/Filter/FilterInterface.php @@ -0,0 +1,56 @@ +<?php + +namespace Kanboard\Core\Filter; + +use PicoDb\Table; + +/** + * Filter Interface + * + * @package filter + * @author Frederic Guillot + */ +interface FilterInterface +{ + /** + * BaseFilter constructor + * + * @access public + * @param mixed $value + */ + public function __construct($value = null); + + /** + * Set the value + * + * @access public + * @param string $value + * @return FilterInterface + */ + public function withValue($value); + + /** + * Set query + * + * @access public + * @param Table $query + * @return FilterInterface + */ + public function withQuery(Table $query); + + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes(); + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply(); +} diff --git a/app/Core/Filter/FormatterInterface.php b/app/Core/Filter/FormatterInterface.php new file mode 100644 index 00000000..b7c04c51 --- /dev/null +++ b/app/Core/Filter/FormatterInterface.php @@ -0,0 +1,31 @@ +<?php + +namespace Kanboard\Core\Filter; + +use PicoDb\Table; + +/** + * Formatter interface + * + * @package filter + * @author Frederic Guillot + */ +interface FormatterInterface +{ + /** + * Set query + * + * @access public + * @param Table $query + * @return FormatterInterface + */ + public function withQuery(Table $query); + + /** + * Apply formatter + * + * @access public + * @return mixed + */ + public function format(); +} diff --git a/app/Core/Filter/Lexer.php b/app/Core/Filter/Lexer.php new file mode 100644 index 00000000..041b58d3 --- /dev/null +++ b/app/Core/Filter/Lexer.php @@ -0,0 +1,153 @@ +<?php + +namespace Kanboard\Core\Filter; + +/** + * Lexer + * + * @package filter + * @author Frederic Guillot + */ +class Lexer +{ + /** + * Current position + * + * @access private + * @var integer + */ + private $offset = 0; + + /** + * Token map + * + * @access private + * @var array + */ + private $tokenMap = array( + "/^(\s+)/" => 'T_WHITESPACE', + '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE', + '/^(yesterday|tomorrow|today)/' => 'T_DATE', + '/^("(.*?)")/' => 'T_STRING', + "/^(\w+)/" => 'T_STRING', + "/^(#\d+)/" => 'T_STRING', + ); + + /** + * Default token + * + * @access private + * @var string + */ + private $defaultToken = ''; + + /** + * Add token + * + * @access public + * @param string $regex + * @param string $token + * @return $this + */ + public function addToken($regex, $token) + { + $this->tokenMap = array($regex => $token) + $this->tokenMap; + return $this; + } + + /** + * Set default token + * + * @access public + * @param string $token + * @return $this + */ + public function setDefaultToken($token) + { + $this->defaultToken = $token; + return $this; + } + + /** + * Tokenize input string + * + * @access public + * @param string $input + * @return array + */ + public function tokenize($input) + { + $tokens = array(); + $this->offset = 0; + + while (isset($input[$this->offset])) { + $result = $this->match(substr($input, $this->offset)); + + if ($result === false) { + return array(); + } + + $tokens[] = $result; + } + + return $this->map($tokens); + } + + /** + * Find a token that match and move the offset + * + * @access protected + * @param string $string + * @return array|boolean + */ + protected function match($string) + { + foreach ($this->tokenMap as $pattern => $name) { + if (preg_match($pattern, $string, $matches)) { + $this->offset += strlen($matches[1]); + + return array( + 'match' => trim($matches[1], '"'), + 'token' => $name, + ); + } + } + + return false; + } + + /** + * Build map of tokens and matches + * + * @access protected + * @param array $tokens + * @return array + */ + protected function map(array $tokens) + { + $map = array(); + $leftOver = ''; + + while (false !== ($token = current($tokens))) { + if ($token['token'] === 'T_STRING' || $token['token'] === 'T_WHITESPACE') { + $leftOver .= $token['match']; + } else { + $next = next($tokens); + + if ($next !== false && in_array($next['token'], array('T_STRING', 'T_DATE'))) { + $map[$token['token']][] = $next['match']; + } + } + + next($tokens); + } + + $leftOver = trim($leftOver); + + if ($this->defaultToken !== '' && $leftOver !== '') { + $map[$this->defaultToken] = array($leftOver); + } + + return $map; + } +} diff --git a/app/Core/Filter/LexerBuilder.php b/app/Core/Filter/LexerBuilder.php new file mode 100644 index 00000000..7a9a714f --- /dev/null +++ b/app/Core/Filter/LexerBuilder.php @@ -0,0 +1,151 @@ +<?php + +namespace Kanboard\Core\Filter; + +use PicoDb\Table; + +/** + * Lexer Builder + * + * @package filter + * @author Frederic Guillot + */ +class LexerBuilder +{ + /** + * Lexer object + * + * @access protected + * @var Lexer + */ + protected $lexer; + + /** + * Query object + * + * @access protected + * @var Table + */ + protected $query; + + /** + * List of filters + * + * @access protected + * @var FilterInterface[] + */ + protected $filters; + + /** + * QueryBuilder object + * + * @access protected + * @var QueryBuilder + */ + protected $queryBuilder; + + /** + * Constructor + * + * @access public + */ + public function __construct() + { + $this->lexer = new Lexer; + $this->queryBuilder = new QueryBuilder(); + } + + /** + * Add a filter + * + * @access public + * @param FilterInterface $filter + * @param bool $default + * @return LexerBuilder + */ + public function withFilter(FilterInterface $filter, $default = false) + { + $attributes = $filter->getAttributes(); + + foreach ($attributes as $attribute) { + $this->filters[$attribute] = $filter; + $this->lexer->addToken(sprintf("/^(%s:)/", $attribute), $attribute); + + if ($default) { + $this->lexer->setDefaultToken($attribute); + } + } + + return $this; + } + + /** + * Set the query + * + * @access public + * @param Table $query + * @return LexerBuilder + */ + public function withQuery(Table $query) + { + $this->query = $query; + $this->queryBuilder->withQuery($this->query); + return $this; + } + + /** + * Parse the input and build the query + * + * @access public + * @param string $input + * @return QueryBuilder + */ + public function build($input) + { + $tokens = $this->lexer->tokenize($input); + + foreach ($tokens as $token => $values) { + if (isset($this->filters[$token])) { + $this->applyFilters($this->filters[$token], $values); + } + } + + return $this->queryBuilder; + } + + /** + * Apply filters to the query + * + * @access protected + * @param FilterInterface $filter + * @param array $values + */ + protected function applyFilters(FilterInterface $filter, array $values) + { + $len = count($values); + + if ($len > 1) { + $criteria = new OrCriteria(); + $criteria->withQuery($this->query); + + foreach ($values as $value) { + $currentFilter = clone($filter); + $criteria->withFilter($currentFilter->withValue($value)); + } + + $this->queryBuilder->withCriteria($criteria); + } elseif ($len === 1) { + $this->queryBuilder->withFilter($filter->withValue($values[0])); + } + } + + /** + * Clone object with deep copy + */ + public function __clone() + { + $this->lexer = clone $this->lexer; + $this->query = clone $this->query; + $this->queryBuilder = clone $this->queryBuilder; + } +} diff --git a/app/Core/Filter/OrCriteria.php b/app/Core/Filter/OrCriteria.php new file mode 100644 index 00000000..174b8458 --- /dev/null +++ b/app/Core/Filter/OrCriteria.php @@ -0,0 +1,68 @@ +<?php + +namespace Kanboard\Core\Filter; + +use PicoDb\Table; + +/** + * OR criteria + * + * @package filter + * @author Frederic Guillot + */ +class OrCriteria implements CriteriaInterface +{ + /** + * @var Table + */ + protected $query; + + /** + * @var FilterInterface[] + */ + protected $filters = array(); + + /** + * Set the Query + * + * @access public + * @param Table $query + * @return CriteriaInterface + */ + public function withQuery(Table $query) + { + $this->query = $query; + return $this; + } + + /** + * Set filter + * + * @access public + * @param FilterInterface $filter + * @return CriteriaInterface + */ + public function withFilter(FilterInterface $filter) + { + $this->filters[] = $filter; + return $this; + } + + /** + * Apply condition + * + * @access public + * @return CriteriaInterface + */ + public function apply() + { + $this->query->beginOr(); + + foreach ($this->filters as $filter) { + $filter->withQuery($this->query)->apply(); + } + + $this->query->closeOr(); + return $this; + } +} diff --git a/app/Core/Filter/QueryBuilder.php b/app/Core/Filter/QueryBuilder.php new file mode 100644 index 00000000..3de82b63 --- /dev/null +++ b/app/Core/Filter/QueryBuilder.php @@ -0,0 +1,103 @@ +<?php + +namespace Kanboard\Core\Filter; + +use PicoDb\Table; + +/** + * Class QueryBuilder + * + * @package filter + * @author Frederic Guillot + */ +class QueryBuilder +{ + /** + * Query object + * + * @access protected + * @var Table + */ + protected $query; + + /** + * Set the query + * + * @access public + * @param Table $query + * @return QueryBuilder + */ + public function withQuery(Table $query) + { + $this->query = $query; + return $this; + } + + /** + * Set a filter + * + * @access public + * @param FilterInterface $filter + * @return QueryBuilder + */ + public function withFilter(FilterInterface $filter) + { + $filter->withQuery($this->query)->apply(); + return $this; + } + + /** + * Set a criteria + * + * @access public + * @param CriteriaInterface $criteria + * @return QueryBuilder + */ + public function withCriteria(CriteriaInterface $criteria) + { + $criteria->withQuery($this->query)->apply(); + return $this; + } + + /** + * Set a formatter + * + * @access public + * @param FormatterInterface $formatter + * @return string|array + */ + public function format(FormatterInterface $formatter) + { + return $formatter->withQuery($this->query)->format(); + } + + /** + * Get the query result as array + * + * @access public + * @return array + */ + public function toArray() + { + return $this->query->findAll(); + } + + /** + * Get Query object + * + * @access public + * @return Table + */ + public function getQuery() + { + return $this->query; + } + + /** + * Clone object with deep copy + */ + public function __clone() + { + $this->query = clone $this->query; + } +} diff --git a/app/Core/Helper.php b/app/Core/Helper.php index 3764a67c..66f8d429 100644 --- a/app/Core/Helper.php +++ b/app/Core/Helper.php @@ -10,18 +10,23 @@ use Pimple\Container; * @package core * @author Frederic Guillot * - * @property \Kanboard\Helper\AppHelper $app - * @property \Kanboard\Helper\AssetHelper $asset - * @property \Kanboard\Helper\DateHelper $dt - * @property \Kanboard\Helper\FileHelper $file - * @property \Kanboard\Helper\FormHelper $form - * @property \Kanboard\Helper\ModelHelper $model - * @property \Kanboard\Helper\SubtaskHelper $subtask - * @property \Kanboard\Helper\TaskHelper $task - * @property \Kanboard\Helper\TextHelper $text - * @property \Kanboard\Helper\UrlHelper $url - * @property \Kanboard\Helper\UserHelper $user - * @property \Kanboard\Helper\LayoutHelper $layout + * @property \Kanboard\Helper\AppHelper $app + * @property \Kanboard\Helper\AssetHelper $asset + * @property \Kanboard\Helper\CalendarHelper $calendar + * @property \Kanboard\Helper\DateHelper $dt + * @property \Kanboard\Helper\FileHelper $file + * @property \Kanboard\Helper\FormHelper $form + * @property \Kanboard\Helper\HookHelper $hook + * @property \Kanboard\Helper\ICalHelper $ical + * @property \Kanboard\Helper\ModelHelper $model + * @property \Kanboard\Helper\SubtaskHelper $subtask + * @property \Kanboard\Helper\TaskHelper $task + * @property \Kanboard\Helper\TextHelper $text + * @property \Kanboard\Helper\UrlHelper $url + * @property \Kanboard\Helper\UserHelper $user + * @property \Kanboard\Helper\LayoutHelper $layout + * @property \Kanboard\Helper\ProjectHeaderHelper $projectHeader + * @property \Kanboard\Helper\ProjectActivityHelper $projectActivity */ class Helper { diff --git a/app/Core/Http/OAuth2.php b/app/Core/Http/OAuth2.php index 6fa1fb0a..211ca5b4 100644 --- a/app/Core/Http/OAuth2.php +++ b/app/Core/Http/OAuth2.php @@ -12,14 +12,14 @@ use Kanboard\Core\Base; */ class OAuth2 extends Base { - private $clientId; - private $secret; - private $callbackUrl; - private $authUrl; - private $tokenUrl; - private $scopes; - private $tokenType; - private $accessToken; + protected $clientId; + protected $secret; + protected $callbackUrl; + protected $authUrl; + protected $tokenUrl; + protected $scopes; + protected $tokenType; + protected $accessToken; /** * Create OAuth2 service @@ -46,6 +46,33 @@ class OAuth2 extends Base } /** + * Generate OAuth2 state and return the token value + * + * @access public + * @return string + */ + public function getState() + { + if (! isset($this->sessionStorage->oauthState) || empty($this->sessionStorage->oauthState)) { + $this->sessionStorage->oauthState = $this->token->getToken(); + } + + return $this->sessionStorage->oauthState; + } + + /** + * Check the validity of the state (CSRF token) + * + * @access public + * @param string $state + * @return bool + */ + public function isValidateState($state) + { + return $state === $this->getState(); + } + + /** * Get authorization url * * @access public @@ -58,6 +85,7 @@ class OAuth2 extends Base 'client_id' => $this->clientId, 'redirect_uri' => $this->callbackUrl, 'scope' => implode(' ', $this->scopes), + 'state' => $this->getState(), ); return $this->authUrl.'?'.http_build_query($params); @@ -94,6 +122,7 @@ class OAuth2 extends Base 'client_secret' => $this->secret, 'redirect_uri' => $this->callbackUrl, 'grant_type' => 'authorization_code', + 'state' => $this->getState(), ); $response = json_decode($this->httpClient->postForm($this->tokenUrl, $params, array('Accept: application/json')), true); diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php index d098f519..996fc58d 100644 --- a/app/Core/Http/Response.php +++ b/app/Core/Http/Response.php @@ -14,6 +14,24 @@ use Kanboard\Core\Csv; class Response extends Base { /** + * Send headers to cache a resource + * + * @access public + * @param integer $duration + * @param string $etag + */ + public function cache($duration, $etag = '') + { + header('Pragma: cache'); + header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $duration) . ' GMT'); + header('Cache-Control: public, max-age=' . $duration); + + if ($etag) { + header('ETag: "' . $etag . '"'); + } + } + + /** * Send no cache headers * * @access public @@ -214,6 +232,20 @@ class Response extends Base } /** + * Send a iCal response + * + * @access public + * @param string $data Raw data + * @param integer $status_code HTTP status code + */ + public function ical($data, $status_code = 200) + { + $this->status($status_code); + $this->contentType('text/calendar; charset=utf-8'); + echo $data; + } + + /** * Send the security header: Content-Security-Policy * * @access public diff --git a/app/Core/Ldap/Client.php b/app/Core/Ldap/Client.php index 05658190..867d67fe 100644 --- a/app/Core/Ldap/Client.php +++ b/app/Core/Ldap/Client.php @@ -3,6 +3,7 @@ namespace Kanboard\Core\Ldap; use LogicException; +use Psr\Log\LoggerInterface; /** * LDAP Client @@ -21,6 +22,14 @@ class Client protected $ldap; /** + * Logger instance + * + * @access private + * @var LoggerInterface + */ + private $logger; + + /** * Establish LDAP connection * * @static @@ -165,4 +174,39 @@ class Client { return LDAP_PASSWORD; } + + /** + * Set logger + * + * @access public + * @param LoggerInterface $logger + * @return Client + */ + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + return $this; + } + + /** + * Get logger + * + * @access public + * @return LoggerInterface + */ + public function getLogger() + { + return $this->logger; + } + + /** + * Test if a logger is defined + * + * @access public + * @return boolean + */ + public function hasLogger() + { + return $this->logger !== null; + } } diff --git a/app/Core/Ldap/Query.php b/app/Core/Ldap/Query.php index 1779fa61..7c1524ca 100644 --- a/app/Core/Ldap/Query.php +++ b/app/Core/Ldap/Query.php @@ -48,6 +48,12 @@ class Query */ public function execute($baseDn, $filter, array $attributes) { + if (DEBUG && $this->client->hasLogger()) { + $this->client->getLogger()->debug('BaseDN='.$baseDn); + $this->client->getLogger()->debug('Filter='.$filter); + $this->client->getLogger()->debug('Attributes='.implode(', ', $attributes)); + } + $sr = ldap_search($this->client->getConnection(), $baseDn, $filter, $attributes); if ($sr === false) { return $this; diff --git a/app/Core/Ldap/User.php b/app/Core/Ldap/User.php index 52283434..d23ec07e 100644 --- a/app/Core/Ldap/User.php +++ b/app/Core/Ldap/User.php @@ -44,8 +44,7 @@ class User */ public static function getUser(Client $client, $username) { - $className = get_called_class(); - $self = new $className(new Query($client)); + $self = new static(new Query($client)); return $self->find($self->getLdapUserPattern($username)); } diff --git a/app/Core/Lexer.php b/app/Core/Lexer.php deleted file mode 100644 index df2d90ae..00000000 --- a/app/Core/Lexer.php +++ /dev/null @@ -1,161 +0,0 @@ -<?php - -namespace Kanboard\Core; - -/** - * Lexer - * - * @package core - * @author Frederic Guillot - */ -class Lexer -{ - /** - * Current position - * - * @access private - * @var integer - */ - private $offset = 0; - - /** - * Token map - * - * @access private - * @var array - */ - private $tokenMap = array( - "/^(assignee:)/" => 'T_ASSIGNEE', - "/^(color:)/" => 'T_COLOR', - "/^(due:)/" => 'T_DUE', - "/^(updated:)/" => 'T_UPDATED', - "/^(modified:)/" => 'T_UPDATED', - "/^(created:)/" => 'T_CREATED', - "/^(status:)/" => 'T_STATUS', - "/^(description:)/" => 'T_DESCRIPTION', - "/^(category:)/" => 'T_CATEGORY', - "/^(column:)/" => 'T_COLUMN', - "/^(project:)/" => 'T_PROJECT', - "/^(swimlane:)/" => 'T_SWIMLANE', - "/^(ref:)/" => 'T_REFERENCE', - "/^(reference:)/" => 'T_REFERENCE', - "/^(link:)/" => 'T_LINK', - "/^(\s+)/" => 'T_WHITESPACE', - '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE', - '/^(yesterday|tomorrow|today)/' => 'T_DATE', - '/^("(.*?)")/' => 'T_STRING', - "/^(\w+)/" => 'T_STRING', - "/^(#\d+)/" => 'T_STRING', - ); - - /** - * Tokenize input string - * - * @access public - * @param string $input - * @return array - */ - public function tokenize($input) - { - $tokens = array(); - $this->offset = 0; - - while (isset($input[$this->offset])) { - $result = $this->match(substr($input, $this->offset)); - - if ($result === false) { - return array(); - } - - $tokens[] = $result; - } - - return $tokens; - } - - /** - * Find a token that match and move the offset - * - * @access public - * @param string $string - * @return array|boolean - */ - public function match($string) - { - foreach ($this->tokenMap as $pattern => $name) { - if (preg_match($pattern, $string, $matches)) { - $this->offset += strlen($matches[1]); - - return array( - 'match' => trim($matches[1], '"'), - 'token' => $name, - ); - } - } - - return false; - } - - /** - * Change the output of tokenizer to be easily parsed by the database filter - * - * Example: ['T_ASSIGNEE' => ['user1', 'user2'], 'T_TITLE' => 'task title'] - * - * @access public - * @param array $tokens - * @return array - */ - public function map(array $tokens) - { - $map = array( - 'T_TITLE' => '', - ); - - while (false !== ($token = current($tokens))) { - switch ($token['token']) { - case 'T_ASSIGNEE': - case 'T_COLOR': - case 'T_CATEGORY': - case 'T_COLUMN': - case 'T_PROJECT': - case 'T_SWIMLANE': - case 'T_LINK': - $next = next($tokens); - - if ($next !== false && $next['token'] === 'T_STRING') { - $map[$token['token']][] = $next['match']; - } - - break; - - case 'T_STATUS': - case 'T_DUE': - case 'T_UPDATED': - case 'T_CREATED': - case 'T_DESCRIPTION': - case 'T_REFERENCE': - $next = next($tokens); - - if ($next !== false && ($next['token'] === 'T_DATE' || $next['token'] === 'T_STRING')) { - $map[$token['token']] = $next['match']; - } - - break; - - default: - $map['T_TITLE'] .= $token['match']; - break; - } - - next($tokens); - } - - $map['T_TITLE'] = trim($map['T_TITLE']); - - if (empty($map['T_TITLE'])) { - unset($map['T_TITLE']); - } - - return $map; - } -} diff --git a/app/Core/Session/SessionStorage.php b/app/Core/Session/SessionStorage.php index 667d9253..6e2f9660 100644 --- a/app/Core/Session/SessionStorage.php +++ b/app/Core/Session/SessionStorage.php @@ -21,6 +21,7 @@ namespace Kanboard\Core\Session; * @property bool $boardCollapsed * @property bool $twoFactorBeforeCodeCalled * @property string $twoFactorSecret + * @property string $oauthState */ class SessionStorage { diff --git a/app/Core/Template.php b/app/Core/Template.php index f85c7f28..1874d44a 100644 --- a/app/Core/Template.php +++ b/app/Core/Template.php @@ -7,6 +7,21 @@ namespace Kanboard\Core; * * @package core * @author Frederic Guillot + * + * @property \Kanboard\Helper\AppHelper $app + * @property \Kanboard\Helper\AssetHelper $asset + * @property \Kanboard\Helper\DateHelper $dt + * @property \Kanboard\Helper\FileHelper $file + * @property \Kanboard\Helper\FormHelper $form + * @property \Kanboard\Helper\HookHelper $hook + * @property \Kanboard\Helper\ModelHelper $model + * @property \Kanboard\Helper\SubtaskHelper $subtask + * @property \Kanboard\Helper\TaskHelper $task + * @property \Kanboard\Helper\TextHelper $text + * @property \Kanboard\Helper\UrlHelper $url + * @property \Kanboard\Helper\UserHelper $user + * @property \Kanboard\Helper\LayoutHelper $layout + * @property \Kanboard\Helper\ProjectHeaderHelper $projectHeader */ class Template { @@ -84,25 +99,26 @@ class Template /** * Find template filename * - * Core template name: 'task/show' - * Plugin template name: 'myplugin:task/show' + * Core template: 'task/show' or 'kanboard:task/show' + * Plugin template: 'myplugin:task/show' * * @access public - * @param string $template_name + * @param string $template * @return string */ - public function getTemplateFile($template_name) + public function getTemplateFile($template) { - $template_name = isset($this->overrides[$template_name]) ? $this->overrides[$template_name] : $template_name; + $plugin = ''; + $template = isset($this->overrides[$template]) ? $this->overrides[$template] : $template; + + if (strpos($template, ':') !== false) { + list($plugin, $template) = explode(':', $template); + } - if (strpos($template_name, ':') !== false) { - list($plugin, $template) = explode(':', $template_name); - $path = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'plugins'; - $path .= DIRECTORY_SEPARATOR.ucfirst($plugin).DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.$template.'.php'; - } else { - $path = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.$template_name.'.php'; + if ($plugin !== 'kanboard' && $plugin !== '') { + return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', '..', 'plugins', ucfirst($plugin), 'Template', $template.'.php')); } - return $path; + return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', 'Template', $template.'.php')); } } diff --git a/app/Core/Thumbnail.php b/app/Core/Thumbnail.php new file mode 100644 index 00000000..733d3a3c --- /dev/null +++ b/app/Core/Thumbnail.php @@ -0,0 +1,172 @@ +<?php + +namespace Kanboard\Core; + +/** + * Thumbnail Generator + * + * @package core + * @author Frederic Guillot + */ +class Thumbnail +{ + protected $metadata = array(); + protected $srcImage; + protected $dstImage; + + /** + * Create a thumbnail from a local file + * + * @static + * @access public + * @param string $filename + * @return Thumbnail + */ + public static function createFromFile($filename) + { + $self = new static(); + $self->fromFile($filename); + return $self; + } + + /** + * Create a thumbnail from a string + * + * @static + * @access public + * @param string $blob + * @return Thumbnail + */ + public static function createFromString($blob) + { + $self = new static(); + $self->fromString($blob); + return $self; + } + + /** + * Load the local image file in memory with GD + * + * @access public + * @param string $filename + * @return Thumbnail + */ + public function fromFile($filename) + { + $this->metadata = getimagesize($filename); + $this->srcImage = imagecreatefromstring(file_get_contents($filename)); + return $this; + } + + /** + * Load the image blob in memory with GD + * + * @access public + * @param string $blob + * @return Thumbnail + */ + public function fromString($blob) + { + if (!function_exists('getimagesizefromstring')) { + $uri = 'data://application/octet-stream;base64,' . base64_encode($blob); + $this->metadata = getimagesize($uri); + } else { + $this->metadata = getimagesizefromstring($blob); + } + + $this->srcImage = imagecreatefromstring($blob); + return $this; + } + + /** + * Resize the image + * + * @access public + * @param int $width + * @param int $height + * @return Thumbnail + */ + public function resize($width = 250, $height = 100) + { + $srcWidth = $this->metadata[0]; + $srcHeight = $this->metadata[1]; + $dstX = 0; + $dstY = 0; + + if ($width == 0 && $height == 0) { + $width = 100; + $height = 100; + } + + if ($width > 0 && $height == 0) { + $dstWidth = $width; + $dstHeight = floor($srcHeight * ($width / $srcWidth)); + $this->dstImage = imagecreatetruecolor($dstWidth, $dstHeight); + } elseif ($width == 0 && $height > 0) { + $dstWidth = floor($srcWidth * ($height / $srcHeight)); + $dstHeight = $height; + $this->dstImage = imagecreatetruecolor($dstWidth, $dstHeight); + } else { + $srcRatio = $srcWidth / $srcHeight; + $resizeRatio = $width / $height; + + if ($srcRatio <= $resizeRatio) { + $dstWidth = $width; + $dstHeight = floor($srcHeight * ($width / $srcWidth)); + $dstY = ($dstHeight - $height) / 2 * (-1); + } else { + $dstWidth = floor($srcWidth * ($height / $srcHeight)); + $dstHeight = $height; + $dstX = ($dstWidth - $width) / 2 * (-1); + } + + $this->dstImage = imagecreatetruecolor($width, $height); + } + + imagecopyresampled($this->dstImage, $this->srcImage, $dstX, $dstY, 0, 0, $dstWidth, $dstHeight, $srcWidth, $srcHeight); + + return $this; + } + + /** + * Save the thumbnail to a local file + * + * @access public + * @param string $filename + * @return Thumbnail + */ + public function toFile($filename) + { + imagejpeg($this->dstImage, $filename); + imagedestroy($this->dstImage); + imagedestroy($this->srcImage); + return $this; + } + + /** + * Return the thumbnail as a string + * + * @access public + * @return string + */ + public function toString() + { + ob_start(); + imagejpeg($this->dstImage, null); + imagedestroy($this->dstImage); + imagedestroy($this->srcImage); + return ob_get_clean(); + } + + /** + * Output the thumbnail directly to the browser or stdout + * + * @access public + */ + public function toOutput() + { + imagejpeg($this->dstImage, null); + imagedestroy($this->dstImage); + imagedestroy($this->srcImage); + } +} diff --git a/app/Core/Tool.php b/app/Core/Tool.php index db2445a1..3423998d 100644 --- a/app/Core/Tool.php +++ b/app/Core/Tool.php @@ -75,78 +75,4 @@ class Tool return $container; } - - /** - * Generate a jpeg thumbnail from an image - * - * @static - * @access public - * @param string $src_file Source file image - * @param string $dst_file Destination file image - * @param integer $resize_width Desired image width - * @param integer $resize_height Desired image height - */ - public static function generateThumbnail($src_file, $dst_file, $resize_width = 250, $resize_height = 100) - { - $metadata = getimagesize($src_file); - $src_width = $metadata[0]; - $src_height = $metadata[1]; - $dst_y = 0; - $dst_x = 0; - - if (empty($metadata['mime'])) { - return; - } - - if ($resize_width == 0 && $resize_height == 0) { - $resize_width = 100; - $resize_height = 100; - } - - if ($resize_width > 0 && $resize_height == 0) { - $dst_width = $resize_width; - $dst_height = floor($src_height * ($resize_width / $src_width)); - $dst_image = imagecreatetruecolor($dst_width, $dst_height); - } elseif ($resize_width == 0 && $resize_height > 0) { - $dst_width = floor($src_width * ($resize_height / $src_height)); - $dst_height = $resize_height; - $dst_image = imagecreatetruecolor($dst_width, $dst_height); - } else { - $src_ratio = $src_width / $src_height; - $resize_ratio = $resize_width / $resize_height; - - if ($src_ratio <= $resize_ratio) { - $dst_width = $resize_width; - $dst_height = floor($src_height * ($resize_width / $src_width)); - - $dst_y = ($dst_height - $resize_height) / 2 * (-1); - } else { - $dst_width = floor($src_width * ($resize_height / $src_height)); - $dst_height = $resize_height; - - $dst_x = ($dst_width - $resize_width) / 2 * (-1); - } - - $dst_image = imagecreatetruecolor($resize_width, $resize_height); - } - - switch ($metadata['mime']) { - case 'image/jpeg': - case 'image/jpg': - $src_image = imagecreatefromjpeg($src_file); - break; - case 'image/png': - $src_image = imagecreatefrompng($src_file); - break; - case 'image/gif': - $src_image = imagecreatefromgif($src_file); - break; - default: - return; - } - - imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, 0, 0, $dst_width, $dst_height, $src_width, $src_height); - imagejpeg($dst_image, $dst_file); - imagedestroy($dst_image); - } } diff --git a/app/Core/User/Avatar/AvatarManager.php b/app/Core/User/Avatar/AvatarManager.php index 71bd8aa5..5b61cbdb 100644 --- a/app/Core/User/Avatar/AvatarManager.php +++ b/app/Core/User/Avatar/AvatarManager.php @@ -32,23 +32,25 @@ class AvatarManager } /** - * Render avatar html element + * Render avatar HTML element * * @access public * @param string $user_id * @param string $username * @param string $name * @param string $email + * @param string $avatar_path * @param int $size * @return string */ - public function render($user_id, $username, $name, $email, $size) + public function render($user_id, $username, $name, $email, $avatar_path, $size) { $user = array( 'id' => $user_id, 'username' => $username, 'name' => $name, 'email' => $email, + 'avatar_path' => $avatar_path, ); krsort($this->providers); @@ -80,6 +82,7 @@ class AvatarManager 'username' => '', 'name' => '?', 'email' => '', + 'avatar_path' => '', ); return $provider->render($user, $size); diff --git a/app/Core/User/UserSession.php b/app/Core/User/UserSession.php index e494e7b4..0034c47a 100644 --- a/app/Core/User/UserSession.php +++ b/app/Core/User/UserSession.php @@ -14,6 +14,19 @@ use Kanboard\Core\Security\Role; class UserSession extends Base { /** + * Refresh current session if necessary + * + * @access public + * @param integer $user_id + */ + public function refresh($user_id) + { + if ($this->getId() == $user_id) { + $this->initialize($this->user->getById($user_id)); + } + } + + /** * Update user session * * @access public diff --git a/app/Filter/BaseDateFilter.php b/app/Filter/BaseDateFilter.php new file mode 100644 index 00000000..56fb2d78 --- /dev/null +++ b/app/Filter/BaseDateFilter.php @@ -0,0 +1,103 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\DateParser; + +/** + * Base date filter class + * + * @package filter + * @author Frederic Guillot + */ +abstract class BaseDateFilter extends BaseFilter +{ + /** + * DateParser object + * + * @access protected + * @var DateParser + */ + protected $dateParser; + + /** + * Set DateParser object + * + * @access public + * @param DateParser $dateParser + * @return $this + */ + public function setDateParser(DateParser $dateParser) + { + $this->dateParser = $dateParser; + return $this; + } + + /** + * Parse operator in the input string + * + * @access protected + * @return string + */ + protected function parseOperator() + { + $operators = array( + '<=' => 'lte', + '>=' => 'gte', + '<' => 'lt', + '>' => 'gt', + ); + + foreach ($operators as $operator => $method) { + if (strpos($this->value, $operator) === 0) { + $this->value = substr($this->value, strlen($operator)); + return $method; + } + } + + return ''; + } + + /** + * Apply a date filter + * + * @access protected + * @param string $field + */ + protected function applyDateFilter($field) + { + $method = $this->parseOperator(); + $timestamp = $this->dateParser->getTimestampFromIsoFormat($this->value); + + if ($method !== '') { + $this->query->$method($field, $this->getTimestampFromOperator($method, $timestamp)); + } else { + $this->query->gte($field, $timestamp); + $this->query->lte($field, $timestamp + 86399); + } + } + + /** + * Get timestamp from the operator + * + * @access public + * @param string $method + * @param integer $timestamp + * @return integer + */ + protected function getTimestampFromOperator($method, $timestamp) + { + switch ($method) { + case 'lte': + return $timestamp + 86399; + case 'lt': + return $timestamp; + case 'gte': + return $timestamp; + case 'gt': + return $timestamp + 86400; + } + + return $timestamp; + } +} diff --git a/app/Filter/BaseFilter.php b/app/Filter/BaseFilter.php new file mode 100644 index 00000000..79a664be --- /dev/null +++ b/app/Filter/BaseFilter.php @@ -0,0 +1,75 @@ +<?php + +namespace Kanboard\Filter; + +use PicoDb\Table; + +/** + * Base filter class + * + * @package filter + * @author Frederic Guillot + */ +abstract class BaseFilter +{ + /** + * @var Table + */ + protected $query; + + /** + * @var mixed + */ + protected $value; + + /** + * BaseFilter constructor + * + * @access public + * @param mixed $value + */ + public function __construct($value = null) + { + $this->value = $value; + } + + /** + * Get object instance + * + * @static + * @access public + * @param mixed $value + * @return static + */ + public static function getInstance($value = null) + { + $self = new static($value); + return $self; + } + + /** + * Set query + * + * @access public + * @param Table $query + * @return \Kanboard\Core\Filter\FilterInterface + */ + public function withQuery(Table $query) + { + $this->query = $query; + return $this; + } + + /** + * Set the value + * + * @access public + * @param string $value + * @return \Kanboard\Core\Filter\FilterInterface + */ + public function withValue($value) + { + $this->value = $value; + return $this; + } +} diff --git a/app/Filter/ProjectActivityCreationDateFilter.php b/app/Filter/ProjectActivityCreationDateFilter.php new file mode 100644 index 00000000..d0b7f754 --- /dev/null +++ b/app/Filter/ProjectActivityCreationDateFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\ProjectActivity; + +/** + * Filter activity events by creation date + * + * @package filter + * @author Frederic Guillot + */ +class ProjectActivityCreationDateFilter extends BaseDateFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('created'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->applyDateFilter(ProjectActivity::TABLE.'.date_creation'); + return $this; + } +} diff --git a/app/Filter/ProjectActivityCreatorFilter.php b/app/Filter/ProjectActivityCreatorFilter.php new file mode 100644 index 00000000..c95569d6 --- /dev/null +++ b/app/Filter/ProjectActivityCreatorFilter.php @@ -0,0 +1,65 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\ProjectActivity; + +/** + * Filter activity events by creator + * + * @package filter + * @author Frederic Guillot + */ +class ProjectActivityCreatorFilter extends BaseFilter implements FilterInterface +{ + /** + * Current user id + * + * @access private + * @var int + */ + private $currentUserId = 0; + + /** + * Set current user id + * + * @access public + * @param integer $userId + * @return TaskAssigneeFilter + */ + public function setCurrentUserId($userId) + { + $this->currentUserId = $userId; + return $this; + } + + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('creator'); + } + + /** + * Apply filter + * + * @access public + * @return string + */ + public function apply() + { + if ($this->value === 'me') { + $this->query->eq(ProjectActivity::TABLE . '.creator_id', $this->currentUserId); + } else { + $this->query->beginOr(); + $this->query->ilike('uc.username', '%'.$this->value.'%'); + $this->query->ilike('uc.name', '%'.$this->value.'%'); + $this->query->closeOr(); + } + } +} diff --git a/app/Filter/ProjectActivityProjectIdFilter.php b/app/Filter/ProjectActivityProjectIdFilter.php new file mode 100644 index 00000000..bb4d8bd1 --- /dev/null +++ b/app/Filter/ProjectActivityProjectIdFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\ProjectActivity; + +/** + * Filter activity events by projectId + * + * @package filter + * @author Frederic Guillot + */ +class ProjectActivityProjectIdFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('project_id'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->eq(ProjectActivity::TABLE.'.project_id', $this->value); + return $this; + } +} diff --git a/app/Filter/ProjectActivityProjectIdsFilter.php b/app/Filter/ProjectActivityProjectIdsFilter.php new file mode 100644 index 00000000..47cf0c25 --- /dev/null +++ b/app/Filter/ProjectActivityProjectIdsFilter.php @@ -0,0 +1,43 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\ProjectActivity; + +/** + * Filter activity events by projectIds + * + * @package filter + * @author Frederic Guillot + */ +class ProjectActivityProjectIdsFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('projects'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + if (empty($this->value)) { + $this->query->eq(ProjectActivity::TABLE.'.project_id', 0); + } else { + $this->query->in(ProjectActivity::TABLE.'.project_id', $this->value); + } + + return $this; + } +} diff --git a/app/Filter/ProjectActivityProjectNameFilter.php b/app/Filter/ProjectActivityProjectNameFilter.php new file mode 100644 index 00000000..0cf73657 --- /dev/null +++ b/app/Filter/ProjectActivityProjectNameFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Project; + +/** + * Filter activity events by project name + * + * @package filter + * @author Frederic Guillot + */ +class ProjectActivityProjectNameFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('project'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->ilike(Project::TABLE.'.name', '%'.$this->value.'%'); + return $this; + } +} diff --git a/app/Filter/ProjectActivityTaskIdFilter.php b/app/Filter/ProjectActivityTaskIdFilter.php new file mode 100644 index 00000000..e99efe09 --- /dev/null +++ b/app/Filter/ProjectActivityTaskIdFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\ProjectActivity; + +/** + * Filter activity events by taskId + * + * @package filter + * @author Frederic Guillot + */ +class ProjectActivityTaskIdFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('task_id'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->eq(ProjectActivity::TABLE.'.task_id', $this->value); + return $this; + } +} diff --git a/app/Filter/ProjectActivityTaskStatusFilter.php b/app/Filter/ProjectActivityTaskStatusFilter.php new file mode 100644 index 00000000..69e2c52d --- /dev/null +++ b/app/Filter/ProjectActivityTaskStatusFilter.php @@ -0,0 +1,43 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; + +/** + * Filter activity events by task status + * + * @package filter + * @author Frederic Guillot + */ +class ProjectActivityTaskStatusFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('status'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + if ($this->value === 'open') { + $this->query->eq(Task::TABLE.'.is_active', Task::STATUS_OPEN); + } elseif ($this->value === 'closed') { + $this->query->eq(Task::TABLE.'.is_active', Task::STATUS_CLOSED); + } + + return $this; + } +} diff --git a/app/Filter/ProjectActivityTaskTitleFilter.php b/app/Filter/ProjectActivityTaskTitleFilter.php new file mode 100644 index 00000000..bf2afa30 --- /dev/null +++ b/app/Filter/ProjectActivityTaskTitleFilter.php @@ -0,0 +1,25 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; + +/** + * Filter activity events by task title + * + * @package filter + * @author Frederic Guillot + */ +class ProjectActivityTaskTitleFilter extends TaskTitleFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('title'); + } +} diff --git a/app/Filter/ProjectGroupRoleProjectFilter.php b/app/Filter/ProjectGroupRoleProjectFilter.php new file mode 100644 index 00000000..b0950868 --- /dev/null +++ b/app/Filter/ProjectGroupRoleProjectFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\ProjectGroupRole; + +/** + * Filter ProjectGroupRole users by project + * + * @package filter + * @author Frederic Guillot + */ +class ProjectGroupRoleProjectFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array(); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->eq(ProjectGroupRole::TABLE.'.project_id', $this->value); + return $this; + } +} diff --git a/app/Filter/ProjectGroupRoleUsernameFilter.php b/app/Filter/ProjectGroupRoleUsernameFilter.php new file mode 100644 index 00000000..c10855bc --- /dev/null +++ b/app/Filter/ProjectGroupRoleUsernameFilter.php @@ -0,0 +1,44 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\GroupMember; +use Kanboard\Model\ProjectGroupRole; +use Kanboard\Model\User; + +/** + * Filter ProjectGroupRole users by username + * + * @package filter + * @author Frederic Guillot + */ +class ProjectGroupRoleUsernameFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array(); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query + ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE) + ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE) + ->ilike(User::TABLE.'.username', $this->value.'%'); + + return $this; + } +} diff --git a/app/Filter/ProjectIdsFilter.php b/app/Filter/ProjectIdsFilter.php new file mode 100644 index 00000000..641f7f18 --- /dev/null +++ b/app/Filter/ProjectIdsFilter.php @@ -0,0 +1,43 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Project; + +/** + * Filter project by ids + * + * @package filter + * @author Frederic Guillot + */ +class ProjectIdsFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('project_ids'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + if (empty($this->value)) { + $this->query->eq(Project::TABLE.'.id', 0); + } else { + $this->query->in(Project::TABLE.'.id', $this->value); + } + + return $this; + } +} diff --git a/app/Filter/ProjectStatusFilter.php b/app/Filter/ProjectStatusFilter.php new file mode 100644 index 00000000..a994600c --- /dev/null +++ b/app/Filter/ProjectStatusFilter.php @@ -0,0 +1,45 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Project; + +/** + * Filter project by status + * + * @package filter + * @author Frederic Guillot + */ +class ProjectStatusFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('status'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + if (is_int($this->value) || ctype_digit($this->value)) { + $this->query->eq(Project::TABLE.'.is_active', $this->value); + } elseif ($this->value === 'inactive' || $this->value === 'closed' || $this->value === 'disabled') { + $this->query->eq(Project::TABLE.'.is_active', 0); + } else { + $this->query->eq(Project::TABLE.'.is_active', 1); + } + + return $this; + } +} diff --git a/app/Filter/ProjectTypeFilter.php b/app/Filter/ProjectTypeFilter.php new file mode 100644 index 00000000..e085e2f6 --- /dev/null +++ b/app/Filter/ProjectTypeFilter.php @@ -0,0 +1,45 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Project; + +/** + * Filter project by type + * + * @package filter + * @author Frederic Guillot + */ +class ProjectTypeFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('type'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + if (is_int($this->value) || ctype_digit($this->value)) { + $this->query->eq(Project::TABLE.'.is_private', $this->value); + } elseif ($this->value === 'private') { + $this->query->eq(Project::TABLE.'.is_private', Project::TYPE_PRIVATE); + } else { + $this->query->eq(Project::TABLE.'.is_private', Project::TYPE_TEAM); + } + + return $this; + } +} diff --git a/app/Filter/ProjectUserRoleProjectFilter.php b/app/Filter/ProjectUserRoleProjectFilter.php new file mode 100644 index 00000000..3b880df5 --- /dev/null +++ b/app/Filter/ProjectUserRoleProjectFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\ProjectUserRole; + +/** + * Filter ProjectUserRole users by project + * + * @package filter + * @author Frederic Guillot + */ +class ProjectUserRoleProjectFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array(); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->eq(ProjectUserRole::TABLE.'.project_id', $this->value); + return $this; + } +} diff --git a/app/Filter/ProjectUserRoleUsernameFilter.php b/app/Filter/ProjectUserRoleUsernameFilter.php new file mode 100644 index 00000000..c00493a3 --- /dev/null +++ b/app/Filter/ProjectUserRoleUsernameFilter.php @@ -0,0 +1,41 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\User; + +/** + * Filter ProjectUserRole users by username + * + * @package filter + * @author Frederic Guillot + */ +class ProjectUserRoleUsernameFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array(); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query + ->join(User::TABLE, 'id', 'user_id') + ->ilike(User::TABLE.'.username', $this->value.'%'); + + return $this; + } +} diff --git a/app/Filter/TaskAssigneeFilter.php b/app/Filter/TaskAssigneeFilter.php new file mode 100644 index 00000000..783d6a12 --- /dev/null +++ b/app/Filter/TaskAssigneeFilter.php @@ -0,0 +1,75 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; +use Kanboard\Model\User; + +/** + * Filter tasks by assignee + * + * @package filter + * @author Frederic Guillot + */ +class TaskAssigneeFilter extends BaseFilter implements FilterInterface +{ + /** + * Current user id + * + * @access private + * @var int + */ + private $currentUserId = 0; + + /** + * Set current user id + * + * @access public + * @param integer $userId + * @return TaskAssigneeFilter + */ + public function setCurrentUserId($userId) + { + $this->currentUserId = $userId; + return $this; + } + + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('assignee'); + } + + /** + * Apply filter + * + * @access public + * @return string + */ + public function apply() + { + if (is_int($this->value) || ctype_digit($this->value)) { + $this->query->eq(Task::TABLE.'.owner_id', $this->value); + } else { + switch ($this->value) { + case 'me': + $this->query->eq(Task::TABLE.'.owner_id', $this->currentUserId); + break; + case 'nobody': + $this->query->eq(Task::TABLE.'.owner_id', 0); + break; + default: + $this->query->beginOr(); + $this->query->ilike(User::TABLE.'.username', '%'.$this->value.'%'); + $this->query->ilike(User::TABLE.'.name', '%'.$this->value.'%'); + $this->query->closeOr(); + } + } + } +} diff --git a/app/Filter/TaskCategoryFilter.php b/app/Filter/TaskCategoryFilter.php new file mode 100644 index 00000000..517f24d9 --- /dev/null +++ b/app/Filter/TaskCategoryFilter.php @@ -0,0 +1,46 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Category; +use Kanboard\Model\Task; + +/** + * Filter tasks by category + * + * @package filter + * @author Frederic Guillot + */ +class TaskCategoryFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('category'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + if (is_int($this->value) || ctype_digit($this->value)) { + $this->query->eq(Task::TABLE.'.category_id', $this->value); + } elseif ($this->value === 'none') { + $this->query->eq(Task::TABLE.'.category_id', 0); + } else { + $this->query->eq(Category::TABLE.'.name', $this->value); + } + + return $this; + } +} diff --git a/app/Filter/TaskColorFilter.php b/app/Filter/TaskColorFilter.php new file mode 100644 index 00000000..784162d4 --- /dev/null +++ b/app/Filter/TaskColorFilter.php @@ -0,0 +1,60 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Color; +use Kanboard\Model\Task; + +/** + * Filter tasks by color + * + * @package filter + * @author Frederic Guillot + */ +class TaskColorFilter extends BaseFilter implements FilterInterface +{ + /** + * Color object + * + * @access private + * @var Color + */ + private $colorModel; + + /** + * Set color model object + * + * @access public + * @param Color $colorModel + * @return TaskColorFilter + */ + public function setColorModel(Color $colorModel) + { + $this->colorModel = $colorModel; + return $this; + } + + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('color', 'colour'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->eq(Task::TABLE.'.color_id', $this->colorModel->find($this->value)); + return $this; + } +} diff --git a/app/Filter/TaskColumnFilter.php b/app/Filter/TaskColumnFilter.php new file mode 100644 index 00000000..9a4d4253 --- /dev/null +++ b/app/Filter/TaskColumnFilter.php @@ -0,0 +1,44 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Column; +use Kanboard\Model\Task; + +/** + * Filter tasks by column + * + * @package filter + * @author Frederic Guillot + */ +class TaskColumnFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('column'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + if (is_int($this->value) || ctype_digit($this->value)) { + $this->query->eq(Task::TABLE.'.column_id', $this->value); + } else { + $this->query->eq(Column::TABLE.'.title', $this->value); + } + + return $this; + } +} diff --git a/app/Filter/TaskCommentFilter.php b/app/Filter/TaskCommentFilter.php new file mode 100644 index 00000000..455098c2 --- /dev/null +++ b/app/Filter/TaskCommentFilter.php @@ -0,0 +1,41 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Comment; +use Kanboard\Model\Task; + +/** + * Filter tasks by comment + * + * @package filter + * @author Frederic Guillot + */ +class TaskCommentFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('comment'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->ilike(Comment::TABLE.'.comment', '%'.$this->value.'%'); + $this->query->join(Comment::TABLE, 'task_id', 'id', Task::TABLE); + + return $this; + } +} diff --git a/app/Filter/TaskCompletionDateFilter.php b/app/Filter/TaskCompletionDateFilter.php new file mode 100644 index 00000000..f206a3e2 --- /dev/null +++ b/app/Filter/TaskCompletionDateFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; + +/** + * Filter tasks by completion date + * + * @package filter + * @author Frederic Guillot + */ +class TaskCompletionDateFilter extends BaseDateFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('completed'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->applyDateFilter(Task::TABLE.'.date_completed'); + return $this; + } +} diff --git a/app/Filter/TaskCreationDateFilter.php b/app/Filter/TaskCreationDateFilter.php new file mode 100644 index 00000000..bb6efad6 --- /dev/null +++ b/app/Filter/TaskCreationDateFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; + +/** + * Filter tasks by creation date + * + * @package filter + * @author Frederic Guillot + */ +class TaskCreationDateFilter extends BaseDateFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('created'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->applyDateFilter(Task::TABLE.'.date_creation'); + return $this; + } +} diff --git a/app/Filter/TaskCreatorFilter.php b/app/Filter/TaskCreatorFilter.php new file mode 100644 index 00000000..af35e6bc --- /dev/null +++ b/app/Filter/TaskCreatorFilter.php @@ -0,0 +1,74 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; + +/** + * Filter tasks by creator + * + * @package filter + * @author Frederic Guillot + */ +class TaskCreatorFilter extends BaseFilter implements FilterInterface +{ + /** + * Current user id + * + * @access private + * @var int + */ + private $currentUserId = 0; + + /** + * Set current user id + * + * @access public + * @param integer $userId + * @return TaskAssigneeFilter + */ + public function setCurrentUserId($userId) + { + $this->currentUserId = $userId; + return $this; + } + + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('creator'); + } + + /** + * Apply filter + * + * @access public + * @return string + */ + public function apply() + { + if (is_int($this->value) || ctype_digit($this->value)) { + $this->query->eq(Task::TABLE.'.creator_id', $this->value); + } else { + switch ($this->value) { + case 'me': + $this->query->eq(Task::TABLE.'.creator_id', $this->currentUserId); + break; + case 'nobody': + $this->query->eq(Task::TABLE.'.creator_id', 0); + break; + default: + $this->query->beginOr(); + $this->query->ilike('uc.username', '%'.$this->value.'%'); + $this->query->ilike('uc.name', '%'.$this->value.'%'); + $this->query->closeOr(); + } + } + } +} diff --git a/app/Filter/TaskDescriptionFilter.php b/app/Filter/TaskDescriptionFilter.php new file mode 100644 index 00000000..6dda58ae --- /dev/null +++ b/app/Filter/TaskDescriptionFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; + +/** + * Filter tasks by description + * + * @package filter + * @author Frederic Guillot + */ +class TaskDescriptionFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('description', 'desc'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->ilike(Task::TABLE.'.description', '%'.$this->value.'%'); + return $this; + } +} diff --git a/app/Filter/TaskDueDateFilter.php b/app/Filter/TaskDueDateFilter.php new file mode 100644 index 00000000..e36efdd0 --- /dev/null +++ b/app/Filter/TaskDueDateFilter.php @@ -0,0 +1,41 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; + +/** + * Filter tasks by due date + * + * @package filter + * @author Frederic Guillot + */ +class TaskDueDateFilter extends BaseDateFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('due'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->neq(Task::TABLE.'.date_due', 0); + $this->query->notNull(Task::TABLE.'.date_due'); + $this->applyDateFilter(Task::TABLE.'.date_due'); + + return $this; + } +} diff --git a/app/Filter/TaskDueDateRangeFilter.php b/app/Filter/TaskDueDateRangeFilter.php new file mode 100644 index 00000000..10deb0d3 --- /dev/null +++ b/app/Filter/TaskDueDateRangeFilter.php @@ -0,0 +1,39 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; + +/** + * Filter tasks by due date range + * + * @package filter + * @author Frederic Guillot + */ +class TaskDueDateRangeFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array(); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->gte(Task::TABLE.'.date_due', is_numeric($this->value[0]) ? $this->value[0] : strtotime($this->value[0])); + $this->query->lte(Task::TABLE.'.date_due', is_numeric($this->value[1]) ? $this->value[1] : strtotime($this->value[1])); + return $this; + } +} diff --git a/app/Filter/TaskIdExclusionFilter.php b/app/Filter/TaskIdExclusionFilter.php new file mode 100644 index 00000000..8bfefb2b --- /dev/null +++ b/app/Filter/TaskIdExclusionFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; + +/** + * Exclude task ids + * + * @package filter + * @author Frederic Guillot + */ +class TaskIdExclusionFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('exclude'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->notin(Task::TABLE.'.id', $this->value); + return $this; + } +} diff --git a/app/Filter/TaskIdFilter.php b/app/Filter/TaskIdFilter.php new file mode 100644 index 00000000..87bac794 --- /dev/null +++ b/app/Filter/TaskIdFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; + +/** + * Filter tasks by id + * + * @package filter + * @author Frederic Guillot + */ +class TaskIdFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('id'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->eq(Task::TABLE.'.id', $this->value); + return $this; + } +} diff --git a/app/Filter/TaskLinkFilter.php b/app/Filter/TaskLinkFilter.php new file mode 100644 index 00000000..18a13a09 --- /dev/null +++ b/app/Filter/TaskLinkFilter.php @@ -0,0 +1,85 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Link; +use Kanboard\Model\Task; +use Kanboard\Model\TaskLink; +use PicoDb\Database; +use PicoDb\Table; + +/** + * Filter tasks by link name + * + * @package filter + * @author Frederic Guillot + */ +class TaskLinkFilter extends BaseFilter implements FilterInterface +{ + /** + * Database object + * + * @access private + * @var Database + */ + private $db; + + /** + * Set database object + * + * @access public + * @param Database $db + * @return TaskLinkFilter + */ + public function setDatabase(Database $db) + { + $this->db = $db; + return $this; + } + + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('link'); + } + + /** + * Apply filter + * + * @access public + * @return string + */ + public function apply() + { + $task_ids = $this->getSubQuery()->findAllByColumn('task_id'); + + if (! empty($task_ids)) { + $this->query->in(Task::TABLE.'.id', $task_ids); + } else { + $this->query->eq(Task::TABLE.'.id', 0); // No match + } + } + + /** + * Get subquery + * + * @access protected + * @return Table + */ + protected function getSubQuery() + { + return $this->db->table(TaskLink::TABLE) + ->columns( + TaskLink::TABLE.'.task_id', + Link::TABLE.'.label' + ) + ->join(Link::TABLE, 'id', 'link_id', TaskLink::TABLE) + ->ilike(Link::TABLE.'.label', $this->value); + } +} diff --git a/app/Filter/TaskModificationDateFilter.php b/app/Filter/TaskModificationDateFilter.php new file mode 100644 index 00000000..5036e9c1 --- /dev/null +++ b/app/Filter/TaskModificationDateFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; + +/** + * Filter tasks by modification date + * + * @package filter + * @author Frederic Guillot + */ +class TaskModificationDateFilter extends BaseDateFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('updated', 'modified'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->applyDateFilter(Task::TABLE.'.date_modification'); + return $this; + } +} diff --git a/app/Filter/TaskProjectFilter.php b/app/Filter/TaskProjectFilter.php new file mode 100644 index 00000000..e432efee --- /dev/null +++ b/app/Filter/TaskProjectFilter.php @@ -0,0 +1,44 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Project; +use Kanboard\Model\Task; + +/** + * Filter tasks by project + * + * @package filter + * @author Frederic Guillot + */ +class TaskProjectFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('project'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + if (is_int($this->value) || ctype_digit($this->value)) { + $this->query->eq(Task::TABLE.'.project_id', $this->value); + } else { + $this->query->ilike(Project::TABLE.'.name', $this->value); + } + + return $this; + } +} diff --git a/app/Filter/TaskProjectsFilter.php b/app/Filter/TaskProjectsFilter.php new file mode 100644 index 00000000..47636b1d --- /dev/null +++ b/app/Filter/TaskProjectsFilter.php @@ -0,0 +1,43 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; + +/** + * Filter tasks by project ids + * + * @package filter + * @author Frederic Guillot + */ +class TaskProjectsFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('projects'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + if (empty($this->value)) { + $this->query->eq(Task::TABLE.'.project_id', 0); + } else { + $this->query->in(Task::TABLE.'.project_id', $this->value); + } + + return $this; + } +} diff --git a/app/Filter/TaskReferenceFilter.php b/app/Filter/TaskReferenceFilter.php new file mode 100644 index 00000000..4ad47dd5 --- /dev/null +++ b/app/Filter/TaskReferenceFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; + +/** + * Filter tasks by reference + * + * @package filter + * @author Frederic Guillot + */ +class TaskReferenceFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('reference', 'ref'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->eq(Task::TABLE.'.reference', $this->value); + return $this; + } +} diff --git a/app/Filter/TaskStartDateFilter.php b/app/Filter/TaskStartDateFilter.php new file mode 100644 index 00000000..dd30762b --- /dev/null +++ b/app/Filter/TaskStartDateFilter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; + +/** + * Filter tasks by start date + * + * @package filter + * @author Frederic Guillot + */ +class TaskStartDateFilter extends BaseDateFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('started'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->applyDateFilter(Task::TABLE.'.date_started'); + return $this; + } +} diff --git a/app/Filter/TaskStatusFilter.php b/app/Filter/TaskStatusFilter.php new file mode 100644 index 00000000..0ba4361e --- /dev/null +++ b/app/Filter/TaskStatusFilter.php @@ -0,0 +1,43 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; + +/** + * Filter tasks by status + * + * @package filter + * @author Frederic Guillot + */ +class TaskStatusFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('status'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + if ($this->value === 'open' || $this->value === 'closed') { + $this->query->eq(Task::TABLE.'.is_active', $this->value === 'open' ? Task::STATUS_OPEN : Task::STATUS_CLOSED); + } else { + $this->query->eq(Task::TABLE.'.is_active', $this->value); + } + + return $this; + } +} diff --git a/app/Filter/TaskSubtaskAssigneeFilter.php b/app/Filter/TaskSubtaskAssigneeFilter.php new file mode 100644 index 00000000..4c757315 --- /dev/null +++ b/app/Filter/TaskSubtaskAssigneeFilter.php @@ -0,0 +1,140 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Subtask; +use Kanboard\Model\Task; +use Kanboard\Model\User; +use PicoDb\Database; +use PicoDb\Table; + +/** + * Filter tasks by subtasks assignee + * + * @package filter + * @author Frederic Guillot + */ +class TaskSubtaskAssigneeFilter extends BaseFilter implements FilterInterface +{ + /** + * Database object + * + * @access private + * @var Database + */ + private $db; + + /** + * Current user id + * + * @access private + * @var int + */ + private $currentUserId = 0; + + /** + * Set current user id + * + * @access public + * @param integer $userId + * @return TaskSubtaskAssigneeFilter + */ + public function setCurrentUserId($userId) + { + $this->currentUserId = $userId; + return $this; + } + + /** + * Set database object + * + * @access public + * @param Database $db + * @return TaskSubtaskAssigneeFilter + */ + public function setDatabase(Database $db) + { + $this->db = $db; + return $this; + } + + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('subtask:assignee'); + } + + /** + * Apply filter + * + * @access public + * @return string + */ + public function apply() + { + $task_ids = $this->getSubQuery()->findAllByColumn('task_id'); + + if (! empty($task_ids)) { + $this->query->in(Task::TABLE.'.id', $task_ids); + } else { + $this->query->eq(Task::TABLE.'.id', 0); // No match + } + } + + /** + * Get subquery + * + * @access protected + * @return Table + */ + protected function getSubQuery() + { + $subquery = $this->db->table(Subtask::TABLE) + ->columns( + Subtask::TABLE.'.user_id', + Subtask::TABLE.'.task_id', + User::TABLE.'.name', + User::TABLE.'.username' + ) + ->join(User::TABLE, 'id', 'user_id', Subtask::TABLE) + ->neq(Subtask::TABLE.'.status', Subtask::STATUS_DONE); + + return $this->applySubQueryFilter($subquery); + } + + /** + * Apply subquery filter + * + * @access protected + * @param Table $subquery + * @return Table + */ + protected function applySubQueryFilter(Table $subquery) + { + if (is_int($this->value) || ctype_digit($this->value)) { + $subquery->eq(Subtask::TABLE.'.user_id', $this->value); + } else { + switch ($this->value) { + case 'me': + $subquery->eq(Subtask::TABLE.'.user_id', $this->currentUserId); + break; + case 'nobody': + $subquery->eq(Subtask::TABLE.'.user_id', 0); + break; + default: + $subquery->beginOr(); + $subquery->ilike(User::TABLE.'.username', $this->value.'%'); + $subquery->ilike(User::TABLE.'.name', '%'.$this->value.'%'); + $subquery->closeOr(); + } + } + + return $subquery; + } +} diff --git a/app/Filter/TaskSwimlaneFilter.php b/app/Filter/TaskSwimlaneFilter.php new file mode 100644 index 00000000..4e030244 --- /dev/null +++ b/app/Filter/TaskSwimlaneFilter.php @@ -0,0 +1,50 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Project; +use Kanboard\Model\Swimlane; +use Kanboard\Model\Task; + +/** + * Filter tasks by swimlane + * + * @package filter + * @author Frederic Guillot + */ +class TaskSwimlaneFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('swimlane'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + if (is_int($this->value) || ctype_digit($this->value)) { + $this->query->eq(Task::TABLE.'.swimlane_id', $this->value); + } elseif ($this->value === 'default') { + $this->query->eq(Task::TABLE.'.swimlane_id', 0); + } else { + $this->query->beginOr(); + $this->query->ilike(Swimlane::TABLE.'.name', $this->value); + $this->query->ilike(Project::TABLE.'.default_swimlane', $this->value); + $this->query->closeOr(); + } + + return $this; + } +} diff --git a/app/Filter/TaskTitleFilter.php b/app/Filter/TaskTitleFilter.php new file mode 100644 index 00000000..9853369c --- /dev/null +++ b/app/Filter/TaskTitleFilter.php @@ -0,0 +1,46 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; +use Kanboard\Model\Task; + +/** + * Filter tasks by title + * + * @package filter + * @author Frederic Guillot + */ +class TaskTitleFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('title'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + if (ctype_digit($this->value) || (strlen($this->value) > 1 && $this->value{0} === '#' && ctype_digit(substr($this->value, 1)))) { + $this->query->beginOr(); + $this->query->eq(Task::TABLE.'.id', str_replace('#', '', $this->value)); + $this->query->ilike(Task::TABLE.'.title', '%'.$this->value.'%'); + $this->query->closeOr(); + } else { + $this->query->ilike(Task::TABLE.'.title', '%'.$this->value.'%'); + } + + return $this; + } +} diff --git a/app/Filter/UserNameFilter.php b/app/Filter/UserNameFilter.php new file mode 100644 index 00000000..dfb07fdd --- /dev/null +++ b/app/Filter/UserNameFilter.php @@ -0,0 +1,35 @@ +<?php + +namespace Kanboard\Filter; + +use Kanboard\Core\Filter\FilterInterface; + +class UserNameFilter extends BaseFilter implements FilterInterface +{ + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('name'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->beginOr() + ->ilike('username', '%'.$this->value.'%') + ->ilike('name', '%'.$this->value.'%') + ->closeOr(); + + return $this; + } +} diff --git a/app/Formatter/BaseFormatter.php b/app/Formatter/BaseFormatter.php new file mode 100644 index 00000000..a9f0ad15 --- /dev/null +++ b/app/Formatter/BaseFormatter.php @@ -0,0 +1,37 @@ +<?php + +namespace Kanboard\Formatter; + +use Kanboard\Core\Base; +use Kanboard\Core\Filter\FormatterInterface; +use PicoDb\Table; + +/** + * Class BaseFormatter + * + * @package formatter + * @author Frederic Guillot + */ +abstract class BaseFormatter extends Base +{ + /** + * Query object + * + * @access protected + * @var Table + */ + protected $query; + + /** + * Set query + * + * @access public + * @param Table $query + * @return FormatterInterface + */ + public function withQuery(Table $query) + { + $this->query = $query; + return $this; + } +} diff --git a/app/Formatter/TaskFilterCalendarEvent.php b/app/Formatter/BaseTaskCalendarFormatter.php index 12ea8687..8fab3e9a 100644 --- a/app/Formatter/TaskFilterCalendarEvent.php +++ b/app/Formatter/BaseTaskCalendarFormatter.php @@ -2,7 +2,7 @@ namespace Kanboard\Formatter; -use Kanboard\Model\TaskFilter; +use Kanboard\Core\Filter\FormatterInterface; /** * Common class to handle calendar events @@ -10,7 +10,7 @@ use Kanboard\Model\TaskFilter; * @package formatter * @author Frederic Guillot */ -abstract class TaskFilterCalendarEvent extends TaskFilter +abstract class BaseTaskCalendarFormatter extends BaseFormatter { /** * Column used for event start date @@ -29,20 +29,12 @@ abstract class TaskFilterCalendarEvent extends TaskFilter protected $endColumn = 'date_completed'; /** - * Full day event flag - * - * @access private - * @var boolean - */ - private $fullDay = false; - - /** * Transform results to calendar events * * @access public * @param string $start_column Column name for the start date * @param string $end_column Column name for the end date - * @return TaskFilterCalendarEvent + * @return FormatterInterface */ public function setColumns($start_column, $end_column = '') { @@ -50,27 +42,4 @@ abstract class TaskFilterCalendarEvent extends TaskFilter $this->endColumn = $end_column ?: $start_column; return $this; } - - /** - * When called calendar events will be full day - * - * @access public - * @return TaskFilterCalendarEvent - */ - public function setFullDay() - { - $this->fullDay = true; - return $this; - } - - /** - * Return true if the events are full day - * - * @access public - * @return boolean - */ - public function isFullDay() - { - return $this->fullDay; - } } diff --git a/app/Formatter/BoardFormatter.php b/app/Formatter/BoardFormatter.php new file mode 100644 index 00000000..6a96b3e6 --- /dev/null +++ b/app/Formatter/BoardFormatter.php @@ -0,0 +1,56 @@ +<?php + +namespace Kanboard\Formatter; + +use Kanboard\Core\Filter\FormatterInterface; +use Kanboard\Model\Task; + +/** + * Board Formatter + * + * @package formatter + * @author Frederic Guillot + */ +class BoardFormatter extends BaseFormatter implements FormatterInterface +{ + /** + * Project id + * + * @access protected + * @var integer + */ + protected $projectId; + + /** + * Set ProjectId + * + * @access public + * @param integer $projectId + * @return $this + */ + public function setProjectId($projectId) + { + $this->projectId = $projectId; + return $this; + } + + /** + * Apply formatter + * + * @access public + * @return array + */ + public function format() + { + $tasks = $this->query + ->eq(Task::TABLE.'.project_id', $this->projectId) + ->asc(Task::TABLE.'.position') + ->findAll(); + + return $this->board->getBoard($this->projectId, function ($project_id, $column_id, $swimlane_id) use ($tasks) { + return array_filter($tasks, function (array $task) use ($column_id, $swimlane_id) { + return $task['column_id'] == $column_id && $task['swimlane_id'] == $swimlane_id; + }); + }); + } +} diff --git a/app/Formatter/FormatterInterface.php b/app/Formatter/FormatterInterface.php deleted file mode 100644 index 0bb61292..00000000 --- a/app/Formatter/FormatterInterface.php +++ /dev/null @@ -1,14 +0,0 @@ -<?php - -namespace Kanboard\Formatter; - -/** - * Formatter Interface - * - * @package formatter - * @author Frederic Guillot - */ -interface FormatterInterface -{ - public function format(); -} diff --git a/app/Formatter/GroupAutoCompleteFormatter.php b/app/Formatter/GroupAutoCompleteFormatter.php index 7023e367..4d552886 100644 --- a/app/Formatter/GroupAutoCompleteFormatter.php +++ b/app/Formatter/GroupAutoCompleteFormatter.php @@ -2,8 +2,12 @@ namespace Kanboard\Formatter; +use Kanboard\Core\Filter\FormatterInterface; +use Kanboard\Core\Group\GroupProviderInterface; +use PicoDb\Table; + /** - * Autocomplete formatter for groups + * Auto-complete formatter for groups * * @package formatter * @author Frederic Guillot @@ -14,25 +18,35 @@ class GroupAutoCompleteFormatter implements FormatterInterface * Groups found * * @access private - * @var array + * @var GroupProviderInterface[] */ private $groups; /** - * Format groups for the ajax autocompletion + * Format groups for the ajax auto-completion * * @access public - * @param array $groups - * @return GroupAutoCompleteFormatter + * @param GroupProviderInterface[] $groups */ - public function setGroups(array $groups) + public function __construct(array $groups) { $this->groups = $groups; + } + + /** + * Set query + * + * @access public + * @param Table $query + * @return FormatterInterface + */ + public function withQuery(Table $query) + { return $this; } /** - * Format groups for the ajax autocompletion + * Format groups for the ajax auto-completion * * @access public * @return array diff --git a/app/Formatter/ProjectActivityEventFormatter.php b/app/Formatter/ProjectActivityEventFormatter.php new file mode 100644 index 00000000..ae80e5e7 --- /dev/null +++ b/app/Formatter/ProjectActivityEventFormatter.php @@ -0,0 +1,61 @@ +<?php + +namespace Kanboard\Formatter; + +use Kanboard\Core\Filter\FormatterInterface; + +class ProjectActivityEventFormatter extends BaseFormatter implements FormatterInterface +{ + /** + * Apply formatter + * + * @access public + * @return array + */ + public function format() + { + $events = $this->query->findAll(); + + foreach ($events as &$event) { + $event += $this->unserializeEvent($event['data']); + unset($event['data']); + + $event['author'] = $event['author_name'] ?: $event['author_username']; + $event['event_title'] = $this->notification->getTitleWithAuthor($event['author'], $event['event_name'], $event); + $event['event_content'] = $this->renderEvent($event); + } + + return $events; + } + + /** + * Decode event data, supports unserialize() and json_decode() + * + * @access protected + * @param string $data Serialized data + * @return array + */ + protected function unserializeEvent($data) + { + if ($data{0} === 'a') { + return unserialize($data); + } + + return json_decode($data, true) ?: array(); + } + + /** + * Get the event html content + * + * @access protected + * @param array $params Event properties + * @return string + */ + protected function renderEvent(array $params) + { + return $this->template->render( + 'event/'.str_replace('.', '_', $params['event_name']), + $params + ); + } +} diff --git a/app/Formatter/ProjectGanttFormatter.php b/app/Formatter/ProjectGanttFormatter.php index 4f73e217..aee1f27f 100644 --- a/app/Formatter/ProjectGanttFormatter.php +++ b/app/Formatter/ProjectGanttFormatter.php @@ -2,7 +2,7 @@ namespace Kanboard\Formatter; -use Kanboard\Model\Project; +use Kanboard\Core\Filter\FormatterInterface; /** * Gantt chart formatter for projects @@ -10,41 +10,9 @@ use Kanboard\Model\Project; * @package formatter * @author Frederic Guillot */ -class ProjectGanttFormatter extends Project implements FormatterInterface +class ProjectGanttFormatter extends BaseFormatter implements FormatterInterface { /** - * List of projects - * - * @access private - * @var array - */ - private $projects = array(); - - /** - * Filter projects to generate the Gantt chart - * - * @access public - * @param int[] $project_ids - * @return ProjectGanttFormatter - */ - public function filter(array $project_ids) - { - if (empty($project_ids)) { - $this->projects = array(); - } else { - $this->projects = $this->db - ->table(self::TABLE) - ->asc('start_date') - ->in('id', $project_ids) - ->eq('is_active', self::ACTIVE) - ->eq('is_private', 0) - ->findAll(); - } - - return $this; - } - - /** * Format projects to be displayed in the Gantt chart * * @access public @@ -52,10 +20,11 @@ class ProjectGanttFormatter extends Project implements FormatterInterface */ public function format() { + $projects = $this->query->findAll(); $colors = $this->color->getDefaultColors(); $bars = array(); - foreach ($this->projects as $project) { + foreach ($projects as $project) { $start = empty($project['start_date']) ? time() : strtotime($project['start_date']); $end = empty($project['end_date']) ? $start : strtotime($project['end_date']); $color = next($colors) ?: reset($colors); diff --git a/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php b/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php new file mode 100644 index 00000000..c5d4e2be --- /dev/null +++ b/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Formatter; + +use Kanboard\Core\Filter\FormatterInterface; + +class SubtaskTimeTrackingCalendarFormatter extends BaseFormatter implements FormatterInterface +{ + /** + * Format calendar events + * + * @access public + * @return array + */ + public function format() + { + $events = array(); + + foreach ($this->query->findAll() as $row) { + $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : ''; + + $events[] = array( + 'id' => $row['id'], + 'subtask_id' => $row['subtask_id'], + 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user, + 'start' => date('Y-m-d\TH:i:s', $row['start']), + 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()), + 'backgroundColor' => $this->color->getBackgroundColor($row['color_id']), + 'borderColor' => $this->color->getBorderColor($row['color_id']), + 'textColor' => 'black', + 'url' => $this->helper->url->to('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])), + 'editable' => false, + ); + } + + return $events; + } +} diff --git a/app/Formatter/TaskFilterAutoCompleteFormatter.php b/app/Formatter/TaskAutoCompleteFormatter.php index c9af4654..480ee797 100644 --- a/app/Formatter/TaskFilterAutoCompleteFormatter.php +++ b/app/Formatter/TaskAutoCompleteFormatter.php @@ -2,19 +2,19 @@ namespace Kanboard\Formatter; +use Kanboard\Core\Filter\FormatterInterface; use Kanboard\Model\Task; -use Kanboard\Model\TaskFilter; /** - * Autocomplete formatter for task filter + * Task AutoComplete Formatter * - * @package formatter - * @author Frederic Guillot + * @package formatter + * @author Frederic Guillot */ -class TaskFilterAutoCompleteFormatter extends TaskFilter implements FormatterInterface +class TaskAutoCompleteFormatter extends BaseFormatter implements FormatterInterface { /** - * Format the tasks for the ajax autocompletion + * Apply formatter * * @access public * @return array diff --git a/app/Formatter/TaskFilterCalendarFormatter.php b/app/Formatter/TaskCalendarFormatter.php index 1b5d6ca4..60b9a062 100644 --- a/app/Formatter/TaskFilterCalendarFormatter.php +++ b/app/Formatter/TaskCalendarFormatter.php @@ -2,15 +2,37 @@ namespace Kanboard\Formatter; +use Kanboard\Core\Filter\FormatterInterface; + /** * Calendar event formatter for task filter * * @package formatter * @author Frederic Guillot */ -class TaskFilterCalendarFormatter extends TaskFilterCalendarEvent implements FormatterInterface +class TaskCalendarFormatter extends BaseTaskCalendarFormatter implements FormatterInterface { /** + * Full day event flag + * + * @access private + * @var boolean + */ + private $fullDay = false; + + /** + * When called calendar events will be full day + * + * @access public + * @return FormatterInterface + */ + public function setFullDay() + { + $this->fullDay = true; + return $this; + } + + /** * Transform tasks to calendar events * * @access public @@ -31,8 +53,8 @@ class TaskFilterCalendarFormatter extends TaskFilterCalendarEvent implements For 'url' => $this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), 'start' => date($this->getDateTimeFormat(), $task[$this->startColumn]), 'end' => date($this->getDateTimeFormat(), $task[$this->endColumn] ?: time()), - 'editable' => $this->isFullDay(), - 'allday' => $this->isFullDay(), + 'editable' => $this->fullDay, + 'allday' => $this->fullDay, ); } @@ -47,6 +69,6 @@ class TaskFilterCalendarFormatter extends TaskFilterCalendarEvent implements For */ private function getDateTimeFormat() { - return $this->isFullDay() ? 'Y-m-d' : 'Y-m-d\TH:i:s'; + return $this->fullDay ? 'Y-m-d' : 'Y-m-d\TH:i:s'; } } diff --git a/app/Formatter/TaskFilterGanttFormatter.php b/app/Formatter/TaskGanttFormatter.php index a4eef1ee..3209aa37 100644 --- a/app/Formatter/TaskFilterGanttFormatter.php +++ b/app/Formatter/TaskGanttFormatter.php @@ -2,15 +2,15 @@ namespace Kanboard\Formatter; -use Kanboard\Model\TaskFilter; +use Kanboard\Core\Filter\FormatterInterface; /** - * Gantt chart formatter for task filter + * Task Gantt Formatter * - * @package formatter - * @author Frederic Guillot + * @package formatter + * @author Frederic Guillot */ -class TaskFilterGanttFormatter extends TaskFilter implements FormatterInterface +class TaskGanttFormatter extends BaseFormatter implements FormatterInterface { /** * Local cache for project columns @@ -19,9 +19,9 @@ class TaskFilterGanttFormatter extends TaskFilter implements FormatterInterface * @var array */ private $columns = array(); - + /** - * Format tasks to be displayed in the Gantt chart + * Apply formatter * * @access public * @return array diff --git a/app/Formatter/TaskFilterICalendarFormatter.php b/app/Formatter/TaskICalFormatter.php index 25b3aea0..a149f725 100644 --- a/app/Formatter/TaskFilterICalendarFormatter.php +++ b/app/Formatter/TaskICalFormatter.php @@ -6,14 +6,15 @@ use DateTime; use Eluceo\iCal\Component\Calendar; use Eluceo\iCal\Component\Event; use Eluceo\iCal\Property\Event\Attendees; +use Kanboard\Core\Filter\FormatterInterface; /** - * iCal event formatter for task filter + * iCal event formatter for tasks * * @package formatter * @author Frederic Guillot */ -class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements FormatterInterface +class TaskICalFormatter extends BaseTaskCalendarFormatter implements FormatterInterface { /** * Calendar object @@ -39,7 +40,7 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo * * @access public * @param \Eluceo\iCal\Component\Calendar $vCalendar - * @return TaskFilterICalendarFormatter + * @return FormatterInterface */ public function setCalendar(Calendar $vCalendar) { @@ -48,10 +49,10 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo } /** - * Transform results to ical events + * Transform results to iCal events * * @access public - * @return TaskFilterICalendarFormatter + * @return FormatterInterface */ public function addDateTimeEvents() { @@ -73,10 +74,10 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo } /** - * Transform results to all day ical events + * Transform results to all day iCal events * * @access public - * @return TaskFilterICalendarFormatter + * @return FormatterInterface */ public function addFullDayEvents() { @@ -96,7 +97,7 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo } /** - * Get common events for task ical events + * Get common events for task iCal events * * @access protected * @param array $task diff --git a/app/Formatter/UserFilterAutoCompleteFormatter.php b/app/Formatter/UserAutoCompleteFormatter.php index b98e0d69..c46a24d0 100644 --- a/app/Formatter/UserFilterAutoCompleteFormatter.php +++ b/app/Formatter/UserAutoCompleteFormatter.php @@ -3,15 +3,15 @@ namespace Kanboard\Formatter; use Kanboard\Model\User; -use Kanboard\Model\UserFilter; +use Kanboard\Core\Filter\FormatterInterface; /** - * Autocomplete formatter for user filter + * Auto-complete formatter for user filter * * @package formatter * @author Frederic Guillot */ -class UserFilterAutoCompleteFormatter extends UserFilter implements FormatterInterface +class UserAutoCompleteFormatter extends BaseFormatter implements FormatterInterface { /** * Format the tasks for the ajax autocompletion diff --git a/app/Helper/AvatarHelper.php b/app/Helper/AvatarHelper.php index c4e27ed9..a36d9b4a 100644 --- a/app/Helper/AvatarHelper.php +++ b/app/Helper/AvatarHelper.php @@ -20,16 +20,17 @@ class AvatarHelper extends Base * @param string $username * @param string $name * @param string $email + * @param string $avatar_path * @param string $css * @param int $size * @return string */ - public function render($user_id, $username, $name, $email, $css = 'avatar-left', $size = 48) + public function render($user_id, $username, $name, $email, $avatar_path, $css = 'avatar-left', $size = 48) { if (empty($user_id) && empty($username)) { $html = $this->avatarManager->renderDefault($size); } else { - $html = $this->avatarManager->render($user_id, $username, $name, $email, $size); + $html = $this->avatarManager->render($user_id, $username, $name, $email, $avatar_path, $size); } return '<div class="avatar avatar-'.$size.' '.$css.'">'.$html.'</div>'; @@ -39,26 +40,29 @@ class AvatarHelper extends Base * Render small user avatar * * @access public - * @param string $user_id - * @param string $username - * @param string $name - * @param string $email + * @param string $user_id + * @param string $username + * @param string $name + * @param string $email + * @param string $avatar_path + * @param string $css * @return string */ - public function small($user_id, $username, $name, $email, $css = '') + public function small($user_id, $username, $name, $email, $avatar_path, $css = '') { - return $this->render($user_id, $username, $name, $email, $css, 20); + return $this->render($user_id, $username, $name, $email, $avatar_path, $css, 20); } /** * Get a small avatar for the current user * * @access public + * @param string $css * @return string */ public function currentUserSmall($css = '') { $user = $this->userSession->getAll(); - return $this->small($user['id'], $user['username'], $user['name'], $user['email'], $css); + return $this->small($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], $css); } } diff --git a/app/Helper/CalendarHelper.php b/app/Helper/CalendarHelper.php new file mode 100644 index 00000000..d5f4af21 --- /dev/null +++ b/app/Helper/CalendarHelper.php @@ -0,0 +1,112 @@ +<?php + +namespace Kanboard\Helper; + +use Kanboard\Core\Base; +use Kanboard\Core\Filter\QueryBuilder; +use Kanboard\Filter\TaskDueDateRangeFilter; +use Kanboard\Formatter\SubtaskTimeTrackingCalendarFormatter; +use Kanboard\Formatter\TaskCalendarFormatter; + +/** + * Calendar Helper + * + * @package helper + * @author Frederic Guillot + */ +class CalendarHelper extends Base +{ + /** + * Get formatted calendar task due events + * + * @access public + * @param QueryBuilder $queryBuilder + * @param string $start + * @param string $end + * @return array + */ + public function getTaskDateDueEvents(QueryBuilder $queryBuilder, $start, $end) + { + $formatter = new TaskCalendarFormatter($this->container); + $formatter->setFullDay(); + $formatter->setColumns('date_due'); + + return $queryBuilder + ->withFilter(new TaskDueDateRangeFilter(array($start, $end))) + ->format($formatter); + } + + /** + * Get formatted calendar task events + * + * @access public + * @param QueryBuilder $queryBuilder + * @param string $start + * @param string $end + * @return array + */ + public function getTaskEvents(QueryBuilder $queryBuilder, $start, $end) + { + $startColumn = $this->config->get('calendar_project_tasks', 'date_started'); + + $queryBuilder->getQuery()->addCondition($this->getCalendarCondition( + $this->dateParser->getTimestampFromIsoFormat($start), + $this->dateParser->getTimestampFromIsoFormat($end), + $startColumn, + 'date_due' + )); + + $formatter = new TaskCalendarFormatter($this->container); + $formatter->setColumns($startColumn, 'date_due'); + + return $queryBuilder->format($formatter); + } + + /** + * Get formatted calendar subtask time tracking events + * + * @access public + * @param integer $user_id + * @param string $start + * @param string $end + * @return array + */ + public function getSubtaskTimeTrackingEvents($user_id, $start, $end) + { + $formatter = new SubtaskTimeTrackingCalendarFormatter($this->container); + return $formatter + ->withQuery($this->subtaskTimeTracking->getUserQuery($user_id) + ->addCondition($this->getCalendarCondition( + $this->dateParser->getTimestampFromIsoFormat($start), + $this->dateParser->getTimestampFromIsoFormat($end), + 'start', + 'end' + )) + ) + ->format(); + } + + /** + * Build SQL condition for a given time range + * + * @access public + * @param string $start_time Start timestamp + * @param string $end_time End timestamp + * @param string $start_column Start column name + * @param string $end_column End column name + * @return string + */ + public function getCalendarCondition($start_time, $end_time, $start_column, $end_column) + { + $start_column = $this->db->escapeIdentifier($start_column); + $end_column = $this->db->escapeIdentifier($end_column); + + $conditions = array( + "($start_column >= '$start_time' AND $start_column <= '$end_time')", + "($start_column <= '$start_time' AND $end_column >= '$start_time')", + "($start_column <= '$start_time' AND ($end_column = '0' OR $end_column IS NULL))", + ); + + return $start_column.' IS NOT NULL AND '.$start_column.' > 0 AND ('.implode(' OR ', $conditions).')'; + } +} diff --git a/app/Helper/ICalHelper.php b/app/Helper/ICalHelper.php new file mode 100644 index 00000000..dc399bf8 --- /dev/null +++ b/app/Helper/ICalHelper.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Helper; + +use Kanboard\Core\Base; +use Kanboard\Core\Filter\QueryBuilder; +use Kanboard\Filter\TaskDueDateRangeFilter; +use Kanboard\Formatter\TaskICalFormatter; +use Eluceo\iCal\Component\Calendar as iCalendar; + +/** + * ICal Helper + * + * @package helper + * @author Frederic Guillot + */ +class ICalHelper extends Base +{ + /** + * Get formatted calendar task due events + * + * @access public + * @param QueryBuilder $queryBuilder + * @param iCalendar $calendar + * @param string $start + * @param string $end + */ + public function addTaskDateDueEvents(QueryBuilder $queryBuilder, iCalendar $calendar, $start, $end) + { + $queryBuilder->withFilter(new TaskDueDateRangeFilter(array($start, $end))); + + $formatter = new TaskICalFormatter($this->container); + $formatter->setColumns('date_due'); + $formatter->setCalendar($calendar); + $formatter->withQuery($queryBuilder->getQuery()); + $formatter->addFullDayEvents(); + } +} diff --git a/app/Helper/ProjectActivityHelper.php b/app/Helper/ProjectActivityHelper.php new file mode 100644 index 00000000..0638a978 --- /dev/null +++ b/app/Helper/ProjectActivityHelper.php @@ -0,0 +1,105 @@ +<?php + +namespace Kanboard\Helper; + +use Kanboard\Core\Base; +use Kanboard\Filter\ProjectActivityProjectIdFilter; +use Kanboard\Filter\ProjectActivityProjectIdsFilter; +use Kanboard\Filter\ProjectActivityTaskIdFilter; +use Kanboard\Formatter\ProjectActivityEventFormatter; +use Kanboard\Model\ProjectActivity; + +/** + * Project Activity Helper + * + * @package helper + * @author Frederic Guillot + */ +class ProjectActivityHelper extends Base +{ + /** + * Search events + * + * @access public + * @param string $search + * @return array + */ + public function searchEvents($search) + { + $projects = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); + $events = array(); + + if ($search !== '') { + $queryBuilder = $this->projectActivityLexer->build($search); + $queryBuilder + ->withFilter(new ProjectActivityProjectIdsFilter(array_keys($projects))) + ->getQuery() + ->desc(ProjectActivity::TABLE.'.id') + ->limit(500) + ; + + $events = $queryBuilder->format(new ProjectActivityEventFormatter($this->container)); + } + + return $events; + } + + /** + * Get project activity events + * + * @access public + * @param integer $project_id + * @param int $limit + * @return array + */ + public function getProjectEvents($project_id, $limit = 50) + { + $queryBuilder = $this->projectActivityQuery + ->withFilter(new ProjectActivityProjectIdFilter($project_id)); + + $queryBuilder->getQuery() + ->desc(ProjectActivity::TABLE.'.id') + ->limit($limit) + ; + + return $queryBuilder->format(new ProjectActivityEventFormatter($this->container)); + } + + /** + * Get projects activity events + * + * @access public + * @param int[] $project_ids + * @param int $limit + * @return array + */ + public function getProjectsEvents(array $project_ids, $limit = 50) + { + $queryBuilder = $this->projectActivityQuery + ->withFilter(new ProjectActivityProjectIdsFilter($project_ids)); + + $queryBuilder->getQuery() + ->desc(ProjectActivity::TABLE.'.id') + ->limit($limit) + ; + + return $queryBuilder->format(new ProjectActivityEventFormatter($this->container)); + } + + /** + * Get task activity events + * + * @access public + * @param integer $task_id + * @return array + */ + public function getTaskEvents($task_id) + { + $queryBuilder = $this->projectActivityQuery + ->withFilter(new ProjectActivityTaskIdFilter($task_id)); + + $queryBuilder->getQuery()->desc(ProjectActivity::TABLE.'.id'); + + return $queryBuilder->format(new ProjectActivityEventFormatter($this->container)); + } +} diff --git a/app/Helper/ProjectHeaderHelper.php b/app/Helper/ProjectHeaderHelper.php new file mode 100644 index 00000000..19570059 --- /dev/null +++ b/app/Helper/ProjectHeaderHelper.php @@ -0,0 +1,80 @@ +<?php + +namespace Kanboard\Helper; + +use Kanboard\Core\Base; + +/** + * Project Header Helper + * + * @package helper + * @author Frederic Guillot + */ +class ProjectHeaderHelper extends Base +{ + /** + * Get current search query + * + * @access public + * @param array $project + * @return string + */ + public function getSearchQuery(array $project) + { + $search = $this->request->getStringParam('search', $this->userSession->getFilters($project['id'])); + $this->userSession->setFilters($project['id'], $search); + return urldecode($search); + } + + /** + * Render project header (views switcher and search box) + * + * @access public + * @param array $project + * @param string $controller + * @param string $action + * @param bool $boardView + * @return string + */ + public function render(array $project, $controller, $action, $boardView = false) + { + $filters = array( + 'controller' => $controller, + 'action' => $action, + 'project_id' => $project['id'], + 'search' => $this->getSearchQuery($project), + ); + + return $this->template->render('project_header/header', array( + 'project' => $project, + 'filters' => $filters, + 'categories_list' => $this->category->getList($project['id'], false), + 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id'], false), + 'custom_filters_list' => $this->customFilter->getAll($project['id'], $this->userSession->getId()), + 'board_view' => $boardView, + )); + } + + /** + * Get project description + * + * @access public + * @param array &$project + * @return string + */ + public function getDescription(array &$project) + { + if ($project['owner_id'] > 0) { + $description = t('Project owner: ').'**'.$this->helper->text->e($project['owner_name'] ?: $project['owner_username']).'**'.PHP_EOL.PHP_EOL; + + if (! empty($project['description'])) { + $description .= '***'.PHP_EOL.PHP_EOL; + $description .= $project['description']; + } + } else { + $description = $project['description']; + } + + return $description; + } +} diff --git a/app/Helper/UserHelper.php b/app/Helper/UserHelper.php index ee7d8ba5..c3369dfd 100644 --- a/app/Helper/UserHelper.php +++ b/app/Helper/UserHelper.php @@ -34,7 +34,7 @@ class UserHelper extends Base { $initials = ''; - foreach (explode(' ', $name) as $string) { + foreach (explode(' ', $name, 2) as $string) { $initials .= mb_substr($string, 0, 1); } diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php index c9abcd08..fadf0a1b 100644 --- a/app/Locale/bs_BA/translations.php +++ b/app/Locale/bs_BA/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Svi projekti', 'Add a new column' => 'Dodaj novu kolonu', 'Title' => 'Naslov', - 'Nobody assigned' => 'Niko nije dodijeljen', 'Assigned to %s' => 'Dodijeljen korisniku %s', 'Remove a column' => 'Ukloni kolonu', 'Remove a column from a board' => 'Ukloni kolonu sa table', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Broj zadataka', 'User' => 'Korisnik', 'Comments' => 'Komentari', - 'Write your text in Markdown' => 'Pisanje teksta pomoću Markdown', 'Leave a comment' => 'Ostavi komentar', 'Comment is required' => 'Komentar je obavezan', 'Leave a description' => 'Dodaj opis', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Praćenje vremena:', 'New sub-task' => 'Novi pod-zadatak', 'New attachment added "%s"' => 'Ubačen novi prilog "%s"', - 'Comment updated' => 'Komentar ažuriran', 'New comment posted by %s' => '%s ostavio novi komentar', 'New attachment' => 'Novi prilog', 'New comment' => 'Novi komentar', + 'Comment updated' => 'Komentar ažuriran', 'New subtask' => 'Novi pod-zadatak', 'Subtask updated' => 'Pod-zadatak ažuriran', 'Task updated' => 'Zadatak ažuriran', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO je uvijek prihvatljiv, primjer: "%s", "%s"', 'New private project' => 'Novi privatni projekat', 'This project is private' => 'Ovaj projekat je privatan', - 'Type here to create a new sub-task' => 'Piši ovdje za kreiranje novog pod-zadatka', 'Add' => 'Dodaj', 'Start date' => 'Datum početka', 'Time estimated' => 'Procijenjeno vrijeme', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'Izvoz zbirnog pregleda po danima za "%s"', 'Exports' => 'Izvozi', 'This export contains the number of tasks per column grouped per day.' => 'Ovaj izvoz sadržava broj zadataka po koloni grupisanih po danima.', - 'Nothing to preview...' => 'Ništa za pokazati...', - 'Preview' => 'Pregled', - 'Write' => 'Piši', 'Active swimlanes' => 'Aktivne swimline trake', 'Add a new swimlane' => 'Dodaj novu swimline traku', 'Change default swimlane' => 'Preimenuj podrazumijevanu swimline traku', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Trajanje zadatka u danima', 'Days in this column' => 'Dani u ovoj koloni', '%dd' => '%dd', - 'Add a link' => 'Dodaj vezu', 'Add a new link' => 'Dodaj novu vezu', 'Do you really want to remove this link: "%s"?' => 'Da li zaista želite ukloniti ovu vezu: "%s"?', 'Do you really want to remove this link with task #%d?' => 'Da li zaista želite ukloniti ovu vezu sa zadatkom #%d?', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'Tok mojih aktivnosti', 'My calendar' => 'Moj kalendar', 'Search tasks' => 'Pretraga zadataka', - 'Back to the calendar' => 'Vrati na kalendar', - 'Filters' => 'Filteri', 'Reset filters' => 'Vrati filtere na početno', 'My tasks due tomorrow' => 'Moji zadaci koje treba završiti sutra', 'Tasks due today' => 'Zadaci koje treba završiti danas', @@ -772,14 +763,14 @@ return array( 'List' => 'Lista', 'Filter' => 'Filter', 'Advanced search' => 'Napredna pretraga', - 'Example of query: ' => 'Primjer za upit', - 'Search by project: ' => 'Pretraga po projektu', - 'Search by column: ' => 'Pretraga po koloni', - 'Search by assignee: ' => 'Pretraga po izvršiocu', - 'Search by color: ' => 'Pretraga po boji', - 'Search by category: ' => 'Pretraga po kategoriji', - 'Search by description: ' => 'Pretraga po opisu', - 'Search by due date: ' => 'Pretraga po datumu završetka', + 'Example of query: ' => 'Primjer za upit: ', + 'Search by project: ' => 'Pretraga po projektu: ', + 'Search by column: ' => 'Pretraga po koloni: ', + 'Search by assignee: ' => 'Pretraga po izvršiocu: ', + 'Search by color: ' => 'Pretraga po boji: ', + 'Search by category: ' => 'Pretraga po kategoriji: ', + 'Search by description: ' => 'Pretraga po opisu: ', + 'Search by due date: ' => 'Pretraga po datumu završetka: ', 'Lead and Cycle time for "%s"' => 'Vrijeme upravljanje i vremenski ciklus za "%s"', 'Average time spent into each column for "%s"' => 'Prosjek utrošenog vremena u svakoj koloni za "%s"', 'Average time spent into each column' => 'Prosjek utrošenog vrmena u svakoj koloni', @@ -850,7 +841,6 @@ return array( 'End date:' => 'Datum završetka:', 'There is no start date or end date for this project.' => 'Nema početnog ili krajnjeg datuma za ovaj projekat.', 'Projects Gantt chart' => 'Gantogram projekata', - 'Link type' => 'Tip veze', 'Change task color when using a specific task link' => 'Promijeni boju zadatka kada se koristi određena veza na zadatku', 'Task link creation or modification' => 'Veza na zadatku je napravljena ili izmijenjena', 'Milestone' => 'Prekretnica', @@ -902,7 +892,6 @@ return array( 'Shared' => 'Podijeljeno', 'Owner' => 'Vlasnik', 'Unread notifications' => 'Nepročitana obavještenja', - 'My filters' => 'Moji filteri', 'Notification methods:' => 'Metode obavještenja:', 'Import tasks from CSV file' => 'Uvezi zadatke putem CSV fajla', 'Unable to read your file' => 'Nemoguće pročitati fajl', @@ -940,6 +929,7 @@ return array( 'Usernames must be lowercase and unique' => 'Korisničko ime mora biti malim slovima i jedinstveno', 'Passwords will be encrypted if present' => 'Šifra će biti kriptovana', '%s attached a new file to the task %s' => '%s je dodano novi fajl u zadatak %s', + 'Link type' => 'Tip veze', 'Assign automatically a category based on a link' => 'Automatsko pridruživanje kategorije bazirano na vezi', 'BAM - Konvertible Mark' => 'BAM - Konvertibilna marka', 'Assignee Username' => 'Pridruži korisničko ime', @@ -1049,7 +1039,6 @@ return array( 'Close a task when there is no activity' => 'Zatvori zadatak kada nema aktivnosti', 'Duration in days' => 'Dužina trajanja u danima', 'Send email when there is no activity on a task' => 'Pošalji email kada nema aktivnosti na zadatku', - 'List of external links' => 'Lista vanjskih veza', 'Unable to fetch link information.' => 'Ne mogu da pribavim informacije o vezi.', 'Daily background job for tasks' => 'Dnevni pozadinski poslovi na zadacima', 'Auto' => 'Automatski', @@ -1067,9 +1056,7 @@ return array( 'External link' => 'Vanjska veza', 'Copy and paste your link here...' => 'Kopiraj i zalijepi svoju vezu ovdje...', 'URL' => 'URL', - 'There is no external link for the moment.' => 'Trenutno nema vanjskih veza.', 'Internal links' => 'Unutrašnje veze', - 'There is no internal link for the moment.' => 'Trenutno nema unutrašnjih veza.', 'Assign to me' => 'Dodijeli meni', 'Me' => 'Za mene', 'Do not duplicate anything' => 'Ništa ne dupliciraj', @@ -1077,7 +1064,6 @@ return array( 'Users management' => 'Menadžment korisnika', 'Groups management' => 'Menadžment grupa', 'Create from another project' => 'Napravi iz drugog projekta', - 'There is no subtask at the moment.' => 'Trenutno nema pod-zadataka.', 'open' => 'otvoreno', 'closed' => 'zatvoreno', 'Priority:' => 'Prioritet:', @@ -1096,7 +1082,6 @@ return array( 'Started:' => 'Početo:', 'Moved:' => 'Pomjereno:', 'Task #%d' => 'Zadatak #%d', - 'Sub-tasks' => 'Pod-zadaci', 'Date and time format' => 'Format za datum i vrijeme', 'Time format' => 'Format za vrijeme', 'Start date: ' => 'Početni datum:', @@ -1137,7 +1122,6 @@ return array( 'User filters' => 'Korisnički filteri', 'Category filters' => 'Kategorija filtera', 'Upload a file' => 'Dodaj fajl', - 'There is no attachment at the moment.' => 'Trenutno nema priloga.', 'View file' => 'Pregled fajla', 'Last activity' => 'Posljednja aktivnost', 'Change subtask position' => 'Promijeni poziciju pod-zadatka', @@ -1151,4 +1135,36 @@ return array( 'There is no action at the moment.' => 'Trenutno nema akcija.', 'Import actions from another project' => 'Uvezi akcije iz drugog projekta', 'There is no available project.' => 'Trenutno nema dostupnih projekata.', + 'Local File' => 'Lokalni fajl', + 'Configuration' => 'Konfiguracija', + 'PHP version:' => 'Verzija PHP-a:', + 'PHP SAPI:' => 'Verzija SAPI-a:', + 'OS version:' => 'Verzija OS-a:', + 'Database version:' => 'Verzija baze podataka:', + 'Browser:' => 'Pretraživač:', + 'Task view' => 'Pregled zadatka', + 'Edit task' => 'Uredi zadatak', + 'Edit description' => 'Uredi opis', + 'New internal link' => 'Nova unutrašnja veza', + 'Display list of keyboard shortcuts' => 'Prikaži listu prečica na tastaturi', + 'Menu' => 'Meni', + 'Set start date' => 'Postavi početni datum', + 'Avatar' => 'Avatar', + 'Upload my avatar image' => 'Dodaj sliku za moj avatar', + 'Remove my image' => 'Ukloni moju sliku', + 'The OAuth2 state parameter is invalid' => 'OAuth2 status parametar nije validan', + 'User not found.' => 'Korisnik nije pronađen.', + 'Search in activity stream' => 'Pretraži aktivnosti', + 'My activities' => 'Moje aktivnosti', + 'Activity until yesterday' => 'Aktivnosti do jučer', + 'Activity until today' => 'Aktivnosti do danas', + 'Search by creator: ' => 'Pretraga po kreatoru: ', + 'Search by creation date: ' => 'Pretraga po datumu kreiranja: ', + 'Search by task status: ' => 'Pretraga po statusu zadatka: ', + 'Search by task title: ' => 'Pretraga po naslovu zadatka: ', + 'Activity stream search' => 'Pretraga aktivnosti', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php index 08836dec..777e9b42 100644 --- a/app/Locale/cs_CZ/translations.php +++ b/app/Locale/cs_CZ/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Všechny projekty', 'Add a new column' => 'Přidat nový sloupec', 'Title' => 'Název', - 'Nobody assigned' => 'Nepřiřazena žádná osoba', 'Assigned to %s' => 'Přiřazeno uživateli: %s', 'Remove a column' => 'Vyjmout sloupec', 'Remove a column from a board' => 'Vyjmout sloupec z nástěnky', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Počet úkolů', 'User' => 'Uživatel', 'Comments' => 'Komentáře', - 'Write your text in Markdown' => 'Můžete použít i Markdown-syntaxi', 'Leave a comment' => 'Zanechte komentář', 'Comment is required' => 'Komentář je vyžadován', 'Leave a description' => 'Vložte popis', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Sledování času:', 'New sub-task' => 'Nový dílčí úkol', 'New attachment added "%s"' => 'Byla přidána nová příloha "%s".', - 'Comment updated' => 'Komentář byl aktualizován.', 'New comment posted by %s' => 'Nový komentář publikovaný uživatelem %s', 'New attachment' => 'Nová příloha', 'New comment' => 'Nový komentář', + 'Comment updated' => 'Komentář byl aktualizován.', 'New subtask' => 'Nový dílčí úkol', 'Subtask updated' => 'Dílčí úkol byl aktualizován', 'Task updated' => 'Úkol byl aktualizován', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formát je vždy akceptován, například: "%s" a "%s"', 'New private project' => 'Nový soukromý projekt', 'This project is private' => 'Tento projekt je soukromuý', - 'Type here to create a new sub-task' => 'Uveďte zde pro vytvoření nového dílčího úkolu', 'Add' => 'Přidat', 'Start date' => 'Počáteční datum', 'Time estimated' => 'Odhadovaný čas', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'Export denních přehledů pro "%s"', 'Exports' => 'Exporty', 'This export contains the number of tasks per column grouped per day.' => 'Tento export obsahuje počet úkolů pro jednotlivé sloupce seskupených podle dní.', - 'Nothing to preview...' => 'Žádná položka k zobrazení ...', - 'Preview' => 'Náhled', - 'Write' => 'Režim psaní', 'Active swimlanes' => 'Aktive Swimlane', 'Add a new swimlane' => 'Přidat nový řádek', 'Change default swimlane' => 'Standard Swimlane ändern', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Doba trvání úkolu ve dnech', 'Days in this column' => 'Dní v tomto sloupci', '%dd' => '%d d', - 'Add a link' => 'Přidat odkaz', 'Add a new link' => 'Přidat nový odkaz', 'Do you really want to remove this link: "%s"?' => 'Die Verbindung "%s" wirklich löschen?', 'Do you really want to remove this link with task #%d?' => 'Die Verbindung mit der Aufgabe #%d wirklich löschen?', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'Přehled mých aktivit', 'My calendar' => 'Můj kalendář', 'Search tasks' => 'Hledání úkolů', - 'Back to the calendar' => 'Zpět do kalendáře', - 'Filters' => 'Filtry', 'Reset filters' => 'Resetovat filtry', 'My tasks due tomorrow' => 'Moje zítřejší úkoly', 'Tasks due today' => 'Dnešní úkoly', @@ -850,7 +841,6 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', // 'Milestone' => '', @@ -902,7 +892,6 @@ return array( // 'Shared' => '', // 'Owner' => '', // 'Unread notifications' => '', - // 'My filters' => '', // 'Notification methods:' => '', // 'Import tasks from CSV file' => '', // 'Unable to read your file' => '', @@ -940,6 +929,7 @@ return array( // 'Usernames must be lowercase and unique' => '', // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', + // 'Link type' => '', // 'Assign automatically a category based on a link' => '', // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', @@ -1049,7 +1039,6 @@ return array( // 'Close a task when there is no activity' => '', // 'Duration in days' => '', // 'Send email when there is no activity on a task' => '', - // 'List of external links' => '', // 'Unable to fetch link information.' => '', // 'Daily background job for tasks' => '', // 'Auto' => '', @@ -1067,9 +1056,7 @@ return array( // 'External link' => '', // 'Copy and paste your link here...' => '', // 'URL' => '', - // 'There is no external link for the moment.' => '', // 'Internal links' => '', - // 'There is no internal link for the moment.' => '', // 'Assign to me' => '', // 'Me' => '', // 'Do not duplicate anything' => '', @@ -1077,7 +1064,6 @@ return array( // 'Users management' => '', // 'Groups management' => '', // 'Create from another project' => '', - // 'There is no subtask at the moment.' => '', // 'open' => '', // 'closed' => '', // 'Priority:' => '', @@ -1096,7 +1082,6 @@ return array( // 'Started:' => '', // 'Moved:' => '', // 'Task #%d' => '', - // 'Sub-tasks' => '', // 'Date and time format' => '', // 'Time format' => '', // 'Start date: ' => '', @@ -1137,7 +1122,6 @@ return array( // 'User filters' => '', // 'Category filters' => '', // 'Upload a file' => '', - // 'There is no attachment at the moment.' => '', // 'View file' => '', // 'Last activity' => '', // 'Change subtask position' => '', @@ -1151,4 +1135,36 @@ return array( // 'There is no action at the moment.' => '', // 'Import actions from another project' => '', // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index 8e686ee4..7c255561 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Alle Projekter', 'Add a new column' => 'Tilføj en ny kolonne', 'Title' => 'Titel', - 'Nobody assigned' => 'Ingen ansvarlig', 'Assigned to %s' => 'Ansvarlig: %s', 'Remove a column' => 'Fjern en kolonne', 'Remove a column from a board' => 'Fjern en kolonne fra et board', @@ -166,7 +165,6 @@ return array( // 'Task count' => '', 'User' => 'Bruger', 'Comments' => 'Kommentarer', - 'Write your text in Markdown' => 'Skriv din tekst i markdown', 'Leave a comment' => 'Efterlad en kommentar', 'Comment is required' => 'Kommentar er krævet', 'Leave a description' => 'Efterlad en beskrivelse...', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Tidsmåling:', 'New sub-task' => 'Ny under-opgave', 'New attachment added "%s"' => 'Ny vedhæftning tilføjet "%s"', - 'Comment updated' => 'Kommentar opdateret', 'New comment posted by %s' => 'Ny kommentar af %s', // 'New attachment' => '', // 'New comment' => '', + 'Comment updated' => 'Kommentar opdateret', // 'New subtask' => '', // 'Subtask updated' => '', // 'Task updated' => '', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO format er altid accepteret, eksempelvis: "%s" og "%s"', 'New private project' => 'Nyt privat projekt', 'This project is private' => 'Dette projekt er privat', - 'Type here to create a new sub-task' => 'Skriv her for at tilføje en ny under-opgave', 'Add' => 'Tilføj', 'Start date' => 'Start dato', 'Time estimated' => 'Tid estimeret', @@ -483,9 +480,6 @@ return array( // 'Daily project summary export for "%s"' => '', // 'Exports' => '', // 'This export contains the number of tasks per column grouped per day.' => '', - // 'Nothing to preview...' => '', - // 'Preview' => '', - // 'Write' => '', // 'Active swimlanes' => '', // 'Add a new swimlane' => '', // 'Change default swimlane' => '', @@ -539,7 +533,6 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', - // 'Add a link' => '', // 'Add a new link' => '', // 'Do you really want to remove this link: "%s"?' => '', // 'Do you really want to remove this link with task #%d?' => '', @@ -749,8 +742,6 @@ return array( // 'My activity stream' => '', // 'My calendar' => '', // 'Search tasks' => '', - // 'Back to the calendar' => '', - // 'Filters' => '', // 'Reset filters' => '', // 'My tasks due tomorrow' => '', // 'Tasks due today' => '', @@ -850,7 +841,6 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', // 'Milestone' => '', @@ -902,7 +892,6 @@ return array( // 'Shared' => '', // 'Owner' => '', // 'Unread notifications' => '', - // 'My filters' => '', // 'Notification methods:' => '', // 'Import tasks from CSV file' => '', // 'Unable to read your file' => '', @@ -940,6 +929,7 @@ return array( // 'Usernames must be lowercase and unique' => '', // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', + // 'Link type' => '', // 'Assign automatically a category based on a link' => '', // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', @@ -1049,7 +1039,6 @@ return array( // 'Close a task when there is no activity' => '', // 'Duration in days' => '', // 'Send email when there is no activity on a task' => '', - // 'List of external links' => '', // 'Unable to fetch link information.' => '', // 'Daily background job for tasks' => '', // 'Auto' => '', @@ -1067,9 +1056,7 @@ return array( // 'External link' => '', // 'Copy and paste your link here...' => '', // 'URL' => '', - // 'There is no external link for the moment.' => '', // 'Internal links' => '', - // 'There is no internal link for the moment.' => '', // 'Assign to me' => '', // 'Me' => '', // 'Do not duplicate anything' => '', @@ -1077,7 +1064,6 @@ return array( // 'Users management' => '', // 'Groups management' => '', // 'Create from another project' => '', - // 'There is no subtask at the moment.' => '', // 'open' => '', // 'closed' => '', // 'Priority:' => '', @@ -1096,7 +1082,6 @@ return array( // 'Started:' => '', // 'Moved:' => '', // 'Task #%d' => '', - // 'Sub-tasks' => '', // 'Date and time format' => '', // 'Time format' => '', // 'Start date: ' => '', @@ -1137,7 +1122,6 @@ return array( // 'User filters' => '', // 'Category filters' => '', // 'Upload a file' => '', - // 'There is no attachment at the moment.' => '', // 'View file' => '', // 'Last activity' => '', // 'Change subtask position' => '', @@ -1151,4 +1135,36 @@ return array( // 'There is no action at the moment.' => '', // 'Import actions from another project' => '', // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index 59c16e11..43b80561 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Alle Projekte', 'Add a new column' => 'Neue Spalte hinzufügen', 'Title' => 'Titel', - 'Nobody assigned' => 'Nicht zugeordnet', 'Assigned to %s' => 'Zuständig: %s', 'Remove a column' => 'Spalte löschen', 'Remove a column from a board' => 'Spalte einer Pinnwand löschen', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Aufgabenanzahl', 'User' => 'Benutzer', 'Comments' => 'Kommentare', - 'Write your text in Markdown' => 'Schreibe deinen Text in Markdown-Syntax', 'Leave a comment' => 'Kommentar eingeben', 'Comment is required' => 'Ein Kommentar wird benötigt', 'Leave a description' => 'Beschreibung eingeben', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Zeittracking', 'New sub-task' => 'Neue Teilaufgabe', 'New attachment added "%s"' => 'Neuer Anhang "%s" wurde hinzugefügt.', - 'Comment updated' => 'Kommentar wurde aktualisiert', 'New comment posted by %s' => 'Neuer Kommentar verfasst durch %s', 'New attachment' => 'Neuer Anhang', 'New comment' => 'Neuer Kommentar', + 'Comment updated' => 'Kommentar wurde aktualisiert', 'New subtask' => 'Neue Teilaufgabe', 'Subtask updated' => 'Teilaufgabe aktualisiert', 'Task updated' => 'Aufgabe aktualisiert', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO Format wird immer akzeptiert, z.B.: "%s" und "%s"', 'New private project' => 'Neues privates Projekt', 'This project is private' => 'Dieses Projekt ist privat', - 'Type here to create a new sub-task' => 'Hier tippen, um eine neue Teilaufgabe zu erstellen', 'Add' => 'Hinzufügen', 'Start date' => 'Startdatum', 'Time estimated' => 'Geschätzte Zeit', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'Export der täglichen Projektzusammenfassung für "%s"', 'Exports' => 'Exporte', 'This export contains the number of tasks per column grouped per day.' => 'Dieser Export enthält die Anzahl der Aufgaben pro Spalte nach Tagen gruppiert.', - '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', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Aufgabenalter in Tagen', 'Days in this column' => 'Tage in dieser Spalte', '%dd' => '%dT', - 'Add a link' => 'Verbindung hinzufügen', 'Add a new link' => 'Neue Verbindung hinzufügen', 'Do you really want to remove this link: "%s"?' => 'Die Verbindung "%s" wirklich löschen?', 'Do you really want to remove this link with task #%d?' => 'Die Verbindung mit der Aufgabe #%d wirklich löschen?', @@ -627,9 +620,9 @@ return array( 'The two factor authentication code is valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist gültig.', 'Code' => 'Code', 'Two factor authentication' => 'Zwei-Faktor-Authentifizierung', - 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI', + 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI: ', 'Check my code' => 'Überprüfe meinen Code', - 'Secret key: ' => 'Geheimer Schlüssel', + 'Secret key: ' => 'Geheimer Schlüssel: ', 'Test your device' => 'Teste dein Gerät', 'Assign a color when the task is moved to a specific column' => 'Weise eine Farbe zu, wenn die Aufgabe zu einer bestimmten Spalte bewegt wird', '%s via Kanboard' => '%s via Kanboard', @@ -660,15 +653,15 @@ return array( 'Timeframe to calculate new due date' => 'Zeitfenster zur Berechnung für neues Ablaufdatum', 'Base date to calculate new due date' => 'Basisdatum zur Berechnung für neues Ablaufdatum', 'Action date' => 'Aktionsdatum', - 'Base date to calculate new due date: ' => 'Basisdatum zur Berechnung für neues Ablaufdatum:', - 'This task has created this child task: ' => 'Diese Aufgabe hat diese Teilaufgabe erstellt:', + 'Base date to calculate new due date: ' => 'Basisdatum zur Berechnung für neues Ablaufdatum: ', + 'This task has created this child task: ' => 'Diese Aufgabe hat diese Teilaufgabe erstellt: ', 'Day(s)' => 'Tag(e)', 'Existing due date' => 'Existierendes Ablaufdatum', - 'Factor to calculate new due date: ' => 'Faktor zur Berechnung für neues Ablaufdatum', + 'Factor to calculate new due date: ' => 'Faktor zur Berechnung für neues Ablaufdatum: ', 'Month(s)' => 'Monat(e)', 'Recurrence' => 'Wiederholung', - 'This task has been created by: ' => 'DIese Aufgabe wurde erstellt von:', - 'Recurrent task has been generated:' => 'Wiederkehrende Aufgabe wurde erstellt', + 'This task has been created by: ' => 'DIese Aufgabe wurde erstellt von: ', + 'Recurrent task has been generated:' => 'Wiederkehrende Aufgabe wurde erstellt ', 'Timeframe to calculate new due date: ' => 'Zeitfenster zur Berechnung für neues Ablaufdatum: ', 'Trigger to generate recurrent task: ' => 'Auslöser für wiederkehrende Aufgabe: ', 'When task is closed' => 'Wenn Aufgabe geshlossen wird', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'Aktivitätsstream', 'My calendar' => 'Mein Kalender', 'Search tasks' => 'Suche nach Aufgaben', - 'Back to the calendar' => 'Zurück zum Kalender', - 'Filters' => 'Filter', 'Reset filters' => 'Filter zurücksetzen', 'My tasks due tomorrow' => 'Meine morgen fälligen Aufgaben', 'Tasks due today' => 'Heute fällige Aufgaben', @@ -786,15 +777,15 @@ return array( 'Average time spent' => 'Durchschnittlicher Zeitverbrauch', 'This chart show the average time spent into each column for the last %d tasks.' => 'Dieses Diagramm zeigt die durchschnittliche Zeit in jeder Spalte der letzten %d Aufgaben.', 'Average Lead and Cycle time' => 'Durchschnittliche Zyklus- und Durchlaufzeit', - 'Average lead time: ' => 'Durchschnittliche Durchlaufzeit:', - 'Average cycle time: ' => 'Durchschnittliche Zykluszeit:', + 'Average lead time: ' => 'Durchschnittliche Durchlaufzeit: ', + 'Average cycle time: ' => 'Durchschnittliche Zykluszeit: ', 'Cycle Time' => 'Zykluszeit', 'Lead Time' => 'Durchlaufzeit', 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Das Diagramm zeigt die durchschnittliche Durchlauf- und Zykluszeit der letzten %d Aufgaben über die Zeit an.', 'Average time into each column' => 'Durchschnittzeit in jeder Spalte', 'Lead and cycle time' => 'Durchlauf- und Zykluszeit', - 'Lead time: ' => 'Durchlaufzeit:', - 'Cycle time: ' => 'Zykluszeit:', + 'Lead time: ' => 'Durchlaufzeit: ', + 'Cycle time: ' => 'Zykluszeit: ', 'Time spent into each column' => 'zeit verbracht in jeder Spalte', 'The lead time is the duration between the task creation and the completion.' => 'Die Durchlaufzeit ist die Dauer zwischen Erstellung und Fertigstellung.', 'The cycle time is the duration between the start date and the completion.' => 'Die Zykluszeit ist die Dauer zwischen Start und Fertigstellung.', @@ -802,7 +793,7 @@ return array( 'Set automatically the start date' => 'Setze Startdatum automatisch', 'Edit Authentication' => 'Authentifizierung bearbeiten', 'Remote user' => 'Remote-Benutzer', - 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Remote-Benutzer haben kein Passwort in der Kanboard Datenbank, Beispiel LDAP, Google und Github Accounts', + 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Remote-Benutzer haben kein Passwort in der Kanboard Datenbank, Beispiel: LDAP, Google und Github Accounts', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Wenn die Box "Verbiete Login-Formular" angeschaltet ist, werden Eingaben in das Login Formular ignoriert.', 'New remote user' => 'Neuer Remote-Benutzer', 'New local user' => 'Neuer lokaler Benutzer', @@ -850,7 +841,6 @@ return array( 'End date:' => 'Endedatum:', 'There is no start date or end date for this project.' => 'Es gibt kein Startdatum oder Endedatum für dieses Projekt', 'Projects Gantt chart' => 'Projekt Gantt Diagramm', - 'Link type' => 'Verbindungstyp', 'Change task color when using a specific task link' => 'Aufgabefarbe ändern bei bestimmter Aufgabenverbindung', 'Task link creation or modification' => 'Aufgabenverbindung erstellen oder bearbeiten', 'Milestone' => 'Meilenstein', @@ -902,7 +892,6 @@ return array( 'Shared' => 'Geteilt', 'Owner' => 'Eigentümer', 'Unread notifications' => 'Ungelesene Benachrichtigungen', - 'My filters' => 'Meine Filter', 'Notification methods:' => 'Benachrichtigungs-Methoden:', 'Import tasks from CSV file' => 'Importiere Aufgaben aus CSV Datei', 'Unable to read your file' => 'Die Datei kann nicht gelesen werden', @@ -940,6 +929,7 @@ return array( 'Usernames must be lowercase and unique' => 'Benutzernamen müssen in Kleinbuschstaben und eindeutig sein', 'Passwords will be encrypted if present' => 'Passwörter werden verschlüsselt wenn vorhanden', '%s attached a new file to the task %s' => '%s hat eine neue Datei zur Aufgabe %s hinzufgefügt', + 'Link type' => 'Verbindungstyp', 'Assign automatically a category based on a link' => 'Linkbasiert eine Kategorie automatisch zuordnen', 'BAM - Konvertible Mark' => 'BAM - Konvertible Mark', 'Assignee Username' => 'Benutzername des Zuständigen', @@ -1032,7 +1022,7 @@ return array( 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Kein Plugin hat eine Projekt-Benachrichtigungsmethode registriert. Sie können individuelle Meldungen in Ihrem Benutzerprofil konfigurieren', 'My dashboard' => 'Mein Dashboard', 'My profile' => 'Mein Profil', - 'Project owner: ' => 'Projekt-Besitzer:', + 'Project owner: ' => 'Projekt-Besitzer: ', 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'Die Projekt-Kennung ist optional und muss alphanumerisch sein, beispielsweise: MYPROJECT.', 'Project owner' => 'Projekt-Besitzer', 'Those dates are useful for the project Gantt chart.' => 'Diese Daten sind nützlich für das Gantt-Diagramm.', @@ -1049,7 +1039,6 @@ return array( 'Close a task when there is no activity' => 'Schliesse eine Aufgabe, wenn keine Aktivitäten vorhanden sind', 'Duration in days' => 'Dauer in Tagen', 'Send email when there is no activity on a task' => 'Versende eine Email, wenn keine Aktivitäten an einer Aufgabe vorhanden sind', - 'List of external links' => 'Liste der externen Verbindungen', 'Unable to fetch link information.' => 'Kann keine Informationen über Verbindungen holen', 'Daily background job for tasks' => 'Tägliche Hintergrundarbeit für Aufgaben', 'Auto' => 'Auto', @@ -1067,9 +1056,7 @@ return array( 'External link' => 'Externe Verbindung', 'Copy and paste your link here...' => 'Kopieren Sie Ihren Link hier...', 'URL' => 'URL', - 'There is no external link for the moment.' => 'Es gibt im Moment keine externe Verbindung.', 'Internal links' => 'Interne Verbindungen', - 'There is no internal link for the moment.' => 'Es gibt im Moment keine interne Verbindung.', 'Assign to me' => 'Mir zuweisen', 'Me' => 'Mich', 'Do not duplicate anything' => 'Nichts duplizieren', @@ -1077,7 +1064,6 @@ return array( 'Users management' => 'Benutzermanagement', 'Groups management' => 'Gruppenmanagement', 'Create from another project' => 'Von einem anderen Projekt erstellen', - 'There is no subtask at the moment.' => 'Es gibt im Moment keine Teilaufgabe', 'open' => 'offen', 'closed' => 'geschlossen', 'Priority:' => 'Priorität:', @@ -1096,13 +1082,12 @@ return array( 'Started:' => 'Gestarted:', 'Moved:' => 'Verschoben:', 'Task #%d' => 'Aufgabe #%d', - 'Sub-tasks' => 'Teilaufgaben', 'Date and time format' => 'Datums- und Zeitformat', 'Time format' => 'Zeitformat', - 'Start date: ' => 'Anfangsdatum:', - 'End date: ' => 'Enddatum:', - 'New due date: ' => 'Neues Fälligkeitsdatum', - 'Start date changed: ' => 'Anfangsdatum geändert:', + 'Start date: ' => 'Anfangsdatum: ', + 'End date: ' => 'Enddatum: ', + 'New due date: ' => 'Neues Fälligkeitsdatum: ', + 'Start date changed: ' => 'Anfangsdatum geändert: ', 'Disable private projects' => 'Private Projekte deaktivieren', 'Do you really want to remove this custom filter: "%s"?' => 'Wollen Sie diesen benutzerdefinierten Filter wirklich entfernen: "%s"?', 'Remove a custom filter' => 'Benutzerdefinierten Filter entfernen', @@ -1137,7 +1122,6 @@ return array( 'User filters' => 'Benutzer-Filter', 'Category filters' => 'Kategorie-Filter', 'Upload a file' => 'Eine Datei hochladen', - 'There is no attachment at the moment.' => 'Es gibt zur Zeit keine Anhänge', 'View file' => 'Datei ansehen', 'Last activity' => 'Letzte Aktivität', 'Change subtask position' => 'Position der Unteraufgabe ändern', @@ -1151,4 +1135,36 @@ return array( 'There is no action at the moment.' => 'Es gibt zur Zeit keine Aktionen.', 'Import actions from another project' => 'Aktionen von einem anderen Projekt importieren', 'There is no available project.' => 'Es ist kein Projekt verfügbar.', + 'Local File' => 'Lokale Datei', + 'Configuration' => 'Konfiguration', + 'PHP version:' => 'PHP Version:', + 'PHP SAPI:' => 'PHP SAPI:', + 'OS version:' => 'OS Version:', + 'Database version:' => 'Datenbank Version:', + 'Browser:' => 'Browser:', + 'Task view' => 'Aufgaben Ansicht', + 'Edit task' => 'Aufgabe bearbeiten', + 'Edit description' => 'Beschreibung bearbeiten', + 'New internal link' => 'Neue interne Verbindung', + 'Display list of keyboard shortcuts' => 'Liste der Tastaturkürzel anzeigen', + 'Menu' => 'Menü', + 'Set start date' => 'Anfangsdatum setzen', + 'Avatar' => 'Avatar', + 'Upload my avatar image' => 'Mein Avatar Bild hochladen', + 'Remove my image' => 'Mein Bild entfernen', + 'The OAuth2 state parameter is invalid' => 'Der OAuth2 Statusparameter ist ungültig', + 'User not found.' => 'Benutzer nicht gefunden', + 'Search in activity stream' => 'Im Aktivitätenstrom suchen', + 'My activities' => 'Meine Aktivitäten', + 'Activity until yesterday' => 'Aktivitäten bis gestern', + 'Activity until today' => 'Aktivitäten bis heute', + 'Search by creator: ' => 'nach Ersteller suchen', + 'Search by creation date: ' => 'nach Datum suchen', + 'Search by task status: ' => 'nach Aufgabenstatus suchen', + 'Search by task title: ' => 'nach Titel suchen', + 'Activity stream search' => 'Im Aktivitätenstrom suchen', + 'Projects where "%s" is manager' => 'Projekte in denen "%s" Manager ist', + 'Projects where "%s" is member' => 'Projekte in denen "%s" Mitglied ist', + 'Open tasks assigned to "%s"' => 'Offene Aufgaben, die "%s" zugeteilt sind', + 'Closed tasks assigned to "%s"' => 'Geschlossene Aufgaben, die "%s" zugeteilt sind', ); diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php index 1f58fb69..664bf328 100644 --- a/app/Locale/el_GR/translations.php +++ b/app/Locale/el_GR/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Όλα τα έργα', 'Add a new column' => 'Πρόσθήκη στήλης', 'Title' => 'Τίτλος', - 'Nobody assigned' => 'Δεν έχει ανατεθεί', 'Assigned to %s' => 'Ανατιθεμένο στον %s', 'Remove a column' => 'Αφαίρεση στήλης', 'Remove a column from a board' => 'Αφαίρεση στήλης από τον πίνακα', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Αρίθμηση εργασιών', 'User' => 'Χρήστης', 'Comments' => 'Σχόλια', - 'Write your text in Markdown' => 'Δυνατότητα γραφής και σε Markdown', 'Leave a comment' => 'Αφήστε ένα σχόλιο', 'Comment is required' => 'Το σχόλιο απαιτείται', 'Leave a description' => 'Αφήστε μια περιγραφή', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Παρακολούθηση του χρόνου:', 'New sub-task' => 'Νέα υπο-εργασία', 'New attachment added "%s"' => 'Νέα επικόλληση προστέθηκε « %s »', - 'Comment updated' => 'Το σχόλιο ενημερώθηκε', 'New comment posted by %s' => 'Νέο σχόλιο από τον χρήστη « %s »', 'New attachment' => 'New attachment', 'New comment' => 'Νέο σχόλιο', + 'Comment updated' => 'Το σχόλιο ενημερώθηκε', 'New subtask' => 'Νέα υπο-εργασία', 'Subtask updated' => 'Η Υπο-Εργασία ενημερώθηκε', 'Task updated' => 'Η εργασία ενημερώθηκε', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO format είναι πάντα αποδεκτό, π.χ.: « %s » και « %s »', 'New private project' => 'Νέο ιδιωτικό έργο', 'This project is private' => 'Αυτό το έργο είναι ιδιωτικό', - 'Type here to create a new sub-task' => 'Πληκτρολογήστε εδώ για να δημιουργήσετε μια νέα υπο-εργασία', 'Add' => 'Προσθήκη', 'Start date' => 'Ημερομηνία έναρξης', 'Time estimated' => 'Εκτιμώμενος χρόνος', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'Εξαγωγή της καθημερινής περίληψης του έργου « %s »', 'Exports' => 'Εξαγωγές', 'This export contains the number of tasks per column grouped per day.' => 'Αυτή η κατάσταση περιέχει τον αριθμό των εργασιών ανά στήλη ομαδοποιημένα ανά ημέρα.', - 'Nothing to preview...' => 'Τίποτα για προεπισκόπηση...', - 'Preview' => 'Προεπισκόπηση', - 'Write' => 'Write', 'Active swimlanes' => 'Ενεργές λωρίδες', 'Add a new swimlane' => 'Προσθήκη λωρίδας', 'Change default swimlane' => 'Αλλαγή της εξ\' ορισμού λωρίδας', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Χρόνος εργασίας σε μέρες', 'Days in this column' => 'Μέρες σε αυτή την στήλη', '%dd' => '%dημ', - 'Add a link' => 'Προσθήκη ενός link', 'Add a new link' => 'Προσθήκη ενός νέου link', 'Do you really want to remove this link: "%s"?' => 'Θέλετε σίγουρα να αφαιρέσετε αυτό το link : « %s » ?', 'Do you really want to remove this link with task #%d?' => 'Θέλετε σίγουρα να αφαιρέσετε αυτό το link του έργου n°%d ?', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'Η ροή δραστηριοτήτων μου', 'My calendar' => 'Το ημερολόγιο μου', 'Search tasks' => 'Αναζήτηση εργασιών', - 'Back to the calendar' => 'Πίσω στο ημερολόγιο', - 'Filters' => 'Φίλτρα', 'Reset filters' => 'Επαναφορά φίλτρων', 'My tasks due tomorrow' => 'Οι εργασίες καθηκόντων μου αύριο', 'Tasks due today' => 'Οι εργασίες καθηκόντων μου αύριο', @@ -850,7 +841,6 @@ return array( 'End date:' => 'Ημερομηνία λήξης :', 'There is no start date or end date for this project.' => 'Δεν υπάρχει ημερομηνία έναρξης ή λήξης για το έργο αυτό.', 'Projects Gantt chart' => 'Διάγραμμα Gantt έργων', - 'Link type' => 'Τύπος συνδέσμου', 'Change task color when using a specific task link' => 'Αλλαγή χρώματος εργασίας χρησιμοποιώντας συγκεκριμένο σύνδεσμο εργασίας', 'Task link creation or modification' => 'Σύνδεσμος δημιουργίας ή τροποποίησης εργασίας', 'Milestone' => 'Ορόσημο', @@ -902,7 +892,6 @@ return array( 'Shared' => 'Διαμοιρασμένα', 'Owner' => 'Ιδιοκτήτης', 'Unread notifications' => 'Αδιάβαστες ειδοποιήσεις', - 'My filters' => 'Τα φίλτρα μου', 'Notification methods:' => 'Μέθοδοι ειδοποίησης:', 'Import tasks from CSV file' => 'Εισαγωγή εργασιών μέσω αρχείου CSV', 'Unable to read your file' => 'Δεν είναι δυνατή η ανάγνωση του αρχείου', @@ -940,6 +929,7 @@ return array( 'Usernames must be lowercase and unique' => 'Οι ονομασίες χρηστών πρέπει να είναι σε μικρά γράμματα (lowercase) και μοναδικά', 'Passwords will be encrypted if present' => 'Οι κωδικοί πρόσβασης κρυπτογραφούνται, αν υπάρχουν', '%s attached a new file to the task %s' => '%s νέο συνημμένο αρχείο της εργασίας %s', + 'Link type' => 'Τύπος συνδέσμου', 'Assign automatically a category based on a link' => 'Ανατίθεται αυτόματα κατηγορία, βασισμένη στον σύνδεσμο', 'BAM - Konvertible Mark' => 'BAM - Konvertible Mark', 'Assignee Username' => 'Δικαιοδόχο όνομα χρήστη', @@ -1049,7 +1039,6 @@ return array( 'Close a task when there is no activity' => 'Κλείσιμο εργασίας όταν δεν υπάρχει δραστηριότητα', 'Duration in days' => 'Διάρκεια σε ημέρες', 'Send email when there is no activity on a task' => 'Αποστολή email όταν δεν υπάρχει δραστηριότητα σε εργασία', - 'List of external links' => 'Λίστα εξωτερικών συνδέσμων', 'Unable to fetch link information.' => 'Δεν είναι δυνατή η ανάλυση της πληροφορίας συνδεσμου', 'Daily background job for tasks' => 'Ημερήσια παρασκηνιακή δουλειά για τις εργασίες', 'Auto' => 'Αυτόματο', @@ -1067,9 +1056,7 @@ return array( 'External link' => 'Εξωτερικός σύνδεσμος', 'Copy and paste your link here...' => 'Κάντε αντιγραφή και επικόλληση εδώ', 'URL' => 'URL', - 'There is no external link for the moment.' => 'Προς το παρών, δεν υπάρχουν εξωτερικοί σύνδεσμοι.', 'Internal links' => 'Εσωτερικοί σύνδεσμοι', - 'There is no internal link for the moment.' => 'Προς το παρών, δεν υπάρχουν εσωτερικοί σύνδεσμοι.', 'Assign to me' => 'Αναττίθεται σε εμένα', 'Me' => 'Σε μένα', 'Do not duplicate anything' => 'Να μην γίνει κλωνοποίηση από άλλο έργο', @@ -1077,7 +1064,6 @@ return array( 'Users management' => 'Διαχείριση χρηστών', 'Groups management' => 'Διαχείριση ομάδων', 'Create from another project' => 'Δημιουργία από άλλο έργο', - 'There is no subtask at the moment.' => 'Προς το παρών, δεν υπάρχει καμία υπο-εργασία.', 'open' => 'Ανοικτό', 'closed' => 'Κλειστό', 'Priority:' => 'Προτεραιότητα:', @@ -1096,7 +1082,6 @@ return array( 'Started:' => 'Ξεκίνησε:', 'Moved:' => 'Μετακινήθηκε:', 'Task #%d' => 'Εργασία #%d', - 'Sub-tasks' => 'Υπο-εργασίες', 'Date and time format' => 'Μορφή ημερομηνίας και ώρας', 'Time format' => 'Μορφή ώρας', 'Start date: ' => 'Ημερομηνία έναρξης: ', @@ -1137,7 +1122,6 @@ return array( 'User filters' => 'Φίλτρα οριζόμενα από τον χρήστη', 'Category filters' => 'Κατηγορία φίλτρων', 'Upload a file' => 'Ανέβασμα αρχείου', - 'There is no attachment at the moment.' => 'Προς το παρόν, δεν υπάρχουν συνημμένα', 'View file' => 'Προβολή αρχείου', 'Last activity' => 'Τελευταία δραστηριότητα', 'Change subtask position' => 'Αλλαγή θέσης υπο-εργασίας', @@ -1151,4 +1135,36 @@ return array( // 'There is no action at the moment.' => '', // 'Import actions from another project' => '', // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index bda20e61..6b4dda42 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Todos los proyectos', 'Add a new column' => 'Añadir una nueva columna', 'Title' => 'Título', - 'Nobody assigned' => 'Nadie asignado', 'Assigned to %s' => 'Asignada a %s', 'Remove a column' => 'Suprimir esta columna', 'Remove a column from a board' => 'Suprimir una columna de un tablero', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Contador de tareas', 'User' => 'Usuario', 'Comments' => 'Comentarios', - 'Write your text in Markdown' => 'Redacta el texto en Markdown', 'Leave a comment' => 'Dejar un comentario', 'Comment is required' => 'El comentario es obligatorio', 'Leave a description' => 'Dejar una descripción', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Control de tiempo:', 'New sub-task' => 'Nueva subtarea', 'New attachment added "%s"' => 'Nuevo adjunto agregado "%s"', - 'Comment updated' => 'Comentario actualizado', 'New comment posted by %s' => 'Nuevo comentario agregado por %s', 'New attachment' => 'Nuevo adjunto', 'New comment' => 'Nuevo comentario', + 'Comment updated' => 'Comentario actualizado', 'New subtask' => 'Nueva subtarea', 'Subtask updated' => 'Subtarea actualizada', 'Task updated' => 'Tarea actualizada', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'El formato ISO siempre es aceptado, ejemplo: "%s" y "%s"', 'New private project' => 'Nuevo proyecto privado', 'This project is private' => 'Este proyecto es privado', - 'Type here to create a new sub-task' => 'Escriba aquí para crear una nueva sub-tarea', 'Add' => 'Añadir', 'Start date' => 'Fecha de inicio', 'Time estimated' => 'Tiempo estimado', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'Exportar sumario diario del proyecto para "%s"', 'Exports' => 'Exportaciones', 'This export contains the number of tasks per column grouped per day.' => 'Esta exportación contiene el número de tereas por columna agrupada por día.', - 'Nothing to preview...' => 'Nada que previsualizar...', - 'Preview' => 'Previsualizar', - 'Write' => 'Grabar', 'Active swimlanes' => 'Calles activas', 'Add a new swimlane' => 'Añadir nueva calle', 'Change default swimlane' => 'Cambiar la calle por defecto', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Edad de la tarea en días', 'Days in this column' => 'Días en esta columna', '%dd' => '%dd', - 'Add a link' => 'Añadir enlace', 'Add a new link' => 'Añadir nuevo enlace', 'Do you really want to remove this link: "%s"?' => '¿Realmente quiere quitar este enlace: "%s"?', 'Do you really want to remove this link with task #%d?' => '¿Realmente quiere quitar este enlace con esta tarea: #%d?', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'Mi flujo de actividad', 'My calendar' => 'Mi calendario', 'Search tasks' => 'Buscar tareas', - 'Back to the calendar' => 'Volver al calendario', - 'Filters' => 'Filtros', 'Reset filters' => 'Limpiar filtros', 'My tasks due tomorrow' => 'Mis tareas a entregar mañana', 'Tasks due today' => 'Tareas a antregar hoy', @@ -850,7 +841,6 @@ return array( 'End date:' => 'Fecha final', 'There is no start date or end date for this project.' => 'No existe fecha de inicio o de fin para este proyecto.', 'Projects Gantt chart' => 'Diagramas de Gantt de los proyectos', - 'Link type' => 'Tipo de enlace', 'Change task color when using a specific task link' => 'Cambiar colo de la tarea al usar un enlace específico a tarea', 'Task link creation or modification' => 'Creación o modificación de enlace a tarea', 'Milestone' => 'Hito', @@ -902,7 +892,6 @@ return array( 'Shared' => 'Compartido', 'Owner' => 'Dueño', 'Unread notifications' => 'Notificaciones sin leer', - 'My filters' => 'Mis filtros', 'Notification methods:' => 'Métodos de notificación', 'Import tasks from CSV file' => 'Importar tareas desde archivo CSV', 'Unable to read your file' => 'No es posible leer el archivo', @@ -940,6 +929,7 @@ return array( 'Usernames must be lowercase and unique' => 'Los nombres de usuario deben ser únicos y contener sólo minúsculas', 'Passwords will be encrypted if present' => 'Las contraseñas serán cifradas si es que existen', '%s attached a new file to the task %s' => '%s adjuntó un nuevo archivo a la tarea %s', + 'Link type' => 'Tipo de enlace', 'Assign automatically a category based on a link' => 'Asignar una categoría automáticamente basado en un enlace', 'BAM - Konvertible Mark' => 'BAM - marco convertible', 'Assignee Username' => 'Nombre de usuario del concesionario', @@ -1049,7 +1039,6 @@ return array( 'Close a task when there is no activity' => 'Cerrar tarea cuando no haya actividad', 'Duration in days' => 'Duración en días', 'Send email when there is no activity on a task' => 'Enviar correo cuando no haya actividad en una tarea', - 'List of external links' => 'Lista de enlaces externos', 'Unable to fetch link information.' => 'No es posible obtener información sobre el enlace', 'Daily background job for tasks' => 'Tarea de fondo diaria para las tareas', 'Auto' => 'Automático', @@ -1067,9 +1056,7 @@ return array( 'External link' => 'Enlace externo', 'Copy and paste your link here...' => 'Copia y pega tu enlace aquí...', 'URL' => 'URL', - 'There is no external link for the moment.' => 'No existe un enlace externo por el momento', 'Internal links' => 'Enlaces internos', - 'There is no internal link for the moment.' => 'No existe un enlace interno por el momento', 'Assign to me' => 'Asignar a mí', 'Me' => 'Yo', 'Do not duplicate anything' => 'No duplicar nada', @@ -1077,7 +1064,6 @@ return array( 'Users management' => 'Administración de usuarios', 'Groups management' => 'Administración de grupos', 'Create from another project' => 'Crear de otro proyecto', - 'There is no subtask at the moment.' => 'No existe subtarea por el momento', 'open' => 'abierto', 'closed' => 'cerrado', 'Priority:' => 'Prioridad', @@ -1096,7 +1082,6 @@ return array( 'Started:' => 'Iniciado', 'Moved:' => 'Movido', 'Task #%d' => 'Tarea #%d', - 'Sub-tasks' => 'Subtareas', 'Date and time format' => 'Formato de hora y fecha', 'Time format' => 'Formato de hora', 'Start date: ' => 'Fecha de inicio', @@ -1137,7 +1122,6 @@ return array( 'User filters' => 'Usar filtros', 'Category filters' => 'Categoría y filtros', 'Upload a file' => 'Subir archivo', - 'There is no attachment at the moment.' => 'No existe ningún adjunto por el momento', 'View file' => 'Ver archivo', 'Last activity' => 'Última actividad', 'Change subtask position' => 'Cambiar posición de la subtarea', @@ -1151,4 +1135,36 @@ return array( 'There is no action at the moment.' => 'No hay ninguna acción en este momento.', 'Import actions from another project' => 'Importar acciones de otro proyecto', 'There is no available project.' => 'No hay proyectos disponibles.', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index 48b6d659..f30b7b4c 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Kaikki projektit', 'Add a new column' => 'Lisää uusi sarake', 'Title' => 'Nimi', - 'Nobody assigned' => 'Ei suorittajaa', 'Assigned to %s' => 'Tekijä: %s', 'Remove a column' => 'Poista sarake', 'Remove a column from a board' => 'Poista sarake taulusta', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Tehtävien määrä', 'User' => 'Käyttäjät', 'Comments' => 'Kommentit', - 'Write your text in Markdown' => 'Kirjoita kommenttisi Markdownilla', 'Leave a comment' => 'Lisää kommentti', 'Comment is required' => 'Kommentti vaaditaan', 'Leave a description' => 'Lisää kuvaus', @@ -329,10 +327,10 @@ return array( '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', // 'New attachment' => '', // 'New comment' => '', + 'Comment updated' => 'Kommentti päivitetty', // 'New subtask' => '', // 'Subtask updated' => '', // 'Task updated' => '', @@ -432,7 +430,6 @@ return array( '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ää', 'Start date' => 'Aloituspäivä', 'Time estimated' => 'Arvioitu aika', @@ -483,9 +480,6 @@ return array( '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', @@ -539,7 +533,6 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', - // 'Add a link' => '', // 'Add a new link' => '', // 'Do you really want to remove this link: "%s"?' => '', // 'Do you really want to remove this link with task #%d?' => '', @@ -749,8 +742,6 @@ return array( // 'My activity stream' => '', // 'My calendar' => '', // 'Search tasks' => '', - // 'Back to the calendar' => '', - // 'Filters' => '', // 'Reset filters' => '', // 'My tasks due tomorrow' => '', // 'Tasks due today' => '', @@ -850,7 +841,6 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', // 'Milestone' => '', @@ -902,7 +892,6 @@ return array( // 'Shared' => '', // 'Owner' => '', // 'Unread notifications' => '', - // 'My filters' => '', // 'Notification methods:' => '', // 'Import tasks from CSV file' => '', // 'Unable to read your file' => '', @@ -940,6 +929,7 @@ return array( // 'Usernames must be lowercase and unique' => '', // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', + // 'Link type' => '', // 'Assign automatically a category based on a link' => '', // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', @@ -1049,7 +1039,6 @@ return array( // 'Close a task when there is no activity' => '', // 'Duration in days' => '', // 'Send email when there is no activity on a task' => '', - // 'List of external links' => '', // 'Unable to fetch link information.' => '', // 'Daily background job for tasks' => '', // 'Auto' => '', @@ -1067,9 +1056,7 @@ return array( // 'External link' => '', // 'Copy and paste your link here...' => '', // 'URL' => '', - // 'There is no external link for the moment.' => '', // 'Internal links' => '', - // 'There is no internal link for the moment.' => '', // 'Assign to me' => '', // 'Me' => '', // 'Do not duplicate anything' => '', @@ -1077,7 +1064,6 @@ return array( // 'Users management' => '', // 'Groups management' => '', // 'Create from another project' => '', - // 'There is no subtask at the moment.' => '', // 'open' => '', // 'closed' => '', // 'Priority:' => '', @@ -1096,7 +1082,6 @@ return array( // 'Started:' => '', // 'Moved:' => '', // 'Task #%d' => '', - // 'Sub-tasks' => '', // 'Date and time format' => '', // 'Time format' => '', // 'Start date: ' => '', @@ -1137,7 +1122,6 @@ return array( // 'User filters' => '', // 'Category filters' => '', // 'Upload a file' => '', - // 'There is no attachment at the moment.' => '', // 'View file' => '', // 'Last activity' => '', // 'Change subtask position' => '', @@ -1151,4 +1135,36 @@ return array( // 'There is no action at the moment.' => '', // 'Import actions from another project' => '', // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index ec8a36bd..ed4638cd 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Tous les projets', 'Add a new column' => 'Ajouter une nouvelle colonne', 'Title' => 'Titre', - 'Nobody assigned' => 'Personne assigné', 'Assigned to %s' => 'Assigné à %s', 'Remove a column' => 'Supprimer une colonne', 'Remove a column from a board' => 'Supprimer une colonne d\'un tableau', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Nombre de tâches', 'User' => 'Utilisateur', 'Comments' => 'Commentaires', - 'Write your text in Markdown' => 'Écrivez votre texte en Markdown', 'Leave a comment' => 'Laissez un commentaire', 'Comment is required' => 'Le commentaire est obligatoire', 'Leave a description' => 'Laissez une description', @@ -329,14 +327,12 @@ return array( 'Time tracking:' => 'Gestion du temps :', 'New sub-task' => 'Nouvelle sous-tâche', 'New attachment added "%s"' => 'Nouvelle pièce-jointe ajoutée « %s »', - 'Comment updated' => 'Commentaire ajouté', 'New comment posted by %s' => 'Nouveau commentaire ajouté par « %s »', 'New attachment' => 'Nouveau document', 'New comment' => 'Nouveau commentaire', 'Comment updated' => 'Commentaire mis à jour', 'New subtask' => 'Nouvelle sous-tâche', 'Subtask updated' => 'Sous-tâche mise à jour', - 'New task' => 'Nouvelle tâche', 'Task updated' => 'Tâche mise à jour', 'Task closed' => 'Tâche fermée', 'Task opened' => 'Tâche ouverte', @@ -434,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'Le format ISO est toujours accepté, exemple : « %s » et « %s »', 'New private project' => 'Nouveau projet privé', 'This project is private' => 'Ce projet est privé', - 'Type here to create a new sub-task' => 'Créer une sous-tâche en écrivant le titre ici', 'Add' => 'Ajouter', 'Start date' => 'Date de début', 'Time estimated' => 'Temps estimé', @@ -485,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'Export du résumé quotidien du projet pour « %s »', 'Exports' => 'Exports', 'This export contains the number of tasks per column grouped per day.' => 'Cet export contient le nombre de tâches par colonne groupé par jour.', - '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', @@ -541,7 +533,6 @@ return array( 'Task age in days' => 'Âge de la tâche en jours', 'Days in this column' => 'Jours dans cette colonne', '%dd' => '%dj', - 'Add a link' => 'Ajouter un lien', 'Add a new link' => 'Ajouter un nouveau lien', 'Do you really want to remove this link: "%s"?' => 'Voulez-vous vraiment supprimer ce lien : « %s » ?', 'Do you really want to remove this link with task #%d?' => 'Voulez-vous vraiment supprimer ce lien avec la tâche n°%d ?', @@ -751,8 +742,6 @@ return array( 'My activity stream' => 'Mon flux d\'activité', 'My calendar' => 'Mon agenda', 'Search tasks' => 'Rechercher des tâches', - 'Back to the calendar' => 'Retour au calendrier', - 'Filters' => 'Filtres', 'Reset filters' => 'Réinitialiser les filtres', 'My tasks due tomorrow' => 'Mes tâches qui arrivent à échéance demain', 'Tasks due today' => 'Tâches qui arrivent à échéance aujourd\'hui', @@ -852,7 +841,6 @@ return array( 'End date:' => 'Date de fin :', 'There is no start date or end date for this project.' => 'Il n\'y a pas de date de début ou de date de fin pour ce projet.', 'Projects Gantt chart' => 'Diagramme de Gantt des projets', - 'Link type' => 'Type de lien', 'Change task color when using a specific task link' => 'Changer la couleur de la tâche lorsqu\'un lien spécifique est utilisé', 'Task link creation or modification' => 'Création ou modification d\'un lien sur une tâche', 'Milestone' => 'Étape importante', @@ -904,7 +892,6 @@ return array( 'Shared' => 'Partagé', 'Owner' => 'Propriétaire', 'Unread notifications' => 'Notifications non lus', - 'My filters' => 'Mes filtres', 'Notification methods:' => 'Méthodes de notifications :', 'Import tasks from CSV file' => 'Importer les tâches depuis un fichier CSV', 'Unable to read your file' => 'Impossible de lire votre fichier', @@ -1052,7 +1039,6 @@ return array( 'Close a task when there is no activity' => 'Fermer une tâche sans activité', 'Duration in days' => 'Durée en jours', 'Send email when there is no activity on a task' => 'Envoyer un email lorsqu\'il n\'y a pas d\'activité sur une tâche', - 'List of external links' => 'Liste des liens externes', 'Unable to fetch link information.' => 'Impossible de récupérer les informations sur le lien.', 'Daily background job for tasks' => 'Tâche planifiée quotidienne pour les tâches', 'Auto' => 'Auto', @@ -1070,9 +1056,7 @@ return array( 'External link' => 'Lien externe', 'Copy and paste your link here...' => 'Copier-coller vôtre lien ici...', 'URL' => 'URL', - 'There is no external link for the moment.' => 'Il n\'y a pas de lien externe pour le moment.', 'Internal links' => 'Liens internes', - 'There is no internal link for the moment.' => 'Il n\'y a pas de lien interne pour le moment.', 'Assign to me' => 'Assigner à moi', 'Me' => 'Moi', 'Do not duplicate anything' => 'Ne rien dupliquer', @@ -1080,7 +1064,6 @@ return array( 'Users management' => 'Gestion des utilisateurs', 'Groups management' => 'Gestion des groupes', 'Create from another project' => 'Créer depuis un autre projet', - 'There is no subtask at the moment.' => 'Il n\'y a aucune sous-tâche pour le moment.', 'open' => 'ouvert', 'closed' => 'fermé', 'Priority:' => 'Priorité :', @@ -1099,7 +1082,6 @@ return array( 'Started:' => 'Commençé le :', 'Moved:' => 'Déplacé le : ', 'Task #%d' => 'Tâche n°%d', - 'Sub-tasks' => 'Sous-tâches', 'Date and time format' => 'Format de la date et de l\'heure', 'Time format' => 'Format de l\'heure', 'Start date: ' => 'Date de début : ', @@ -1140,7 +1122,6 @@ return array( 'User filters' => 'Filtres des utilisateurs', 'Category filters' => 'Filtres des catégories', 'Upload a file' => 'Uploader un fichier', - 'There is no attachment at the moment.' => 'Il n\'y a aucune pièce-jointe pour le moment.', 'View file' => 'Voir le fichier', 'Last activity' => 'Dernières activités', 'Change subtask position' => 'Changer la position de la sous-tâche', @@ -1154,4 +1135,36 @@ return array( 'There is no action at the moment.' => 'Il n\'y a aucune action pour le moment.', 'Import actions from another project' => 'Importer les actions depuis un autre projet', 'There is no available project.' => 'Il n\'y a pas de projet disponible.', + 'Local File' => 'Fichier local', + 'Configuration' => 'Configuration', + 'PHP version:' => 'Version de PHP :', + 'PHP SAPI:' => 'PHP SAPI :', + 'OS version:' => 'Version du système d\'exploitation :', + 'Database version:' => 'Version de la base de donnée :', + 'Browser:' => 'Navigateur web :', + 'Task view' => 'Vue détaillée d\'une tâche', + 'Edit task' => 'Modifier la tâche', + 'Edit description' => 'Modifier la description', + 'New internal link' => 'Nouveau lien interne', + 'Display list of keyboard shortcuts' => 'Afficher la liste des raccourcis claviers', + 'Menu' => 'Menu', + 'Set start date' => 'Définir la date de début', + 'Avatar' => 'Avatar', + 'Upload my avatar image' => 'Uploader mon image d\'avatar', + 'Remove my image' => 'Supprimer mon image', + 'The OAuth2 state parameter is invalid' => 'Le paramètre "state" de OAuth2 est invalide', + 'User not found.' => 'Utilisateur introuvable.', + 'Search in activity stream' => 'Chercher dans le flux d\'activité', + 'My activities' => 'Mes activités', + 'Activity until yesterday' => 'Activités jusqu\'à hier', + 'Activity until today' => 'Activités jusqu\'à aujourd\'hui', + 'Search by creator: ' => 'Rechercher par créateur : ', + 'Search by creation date: ' => 'Rechercher par date de création : ', + 'Search by task status: ' => 'Rechercher par le statut des tâches : ', + 'Search by task title: ' => 'Rechercher par le titre des tâches : ', + 'Activity stream search' => 'Recherche dans le flux d\'activité', + 'Projects where "%s" is manager' => 'Projets où « %s » est gestionnaire', + 'Projects where "%s" is member' => 'Projets où « %s » est membre du projet', + 'Open tasks assigned to "%s"' => 'Tâches ouvertes assignées à « %s »', + 'Closed tasks assigned to "%s"' => 'Tâches fermées assignées à « %s »', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index 53e8ace5..394f89a0 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Minden projekt', 'Add a new column' => 'Új oszlop', 'Title' => 'Cím', - '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', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Feladatok száma', 'User' => 'Felhasználó', 'Comments' => 'Hozzászólások', - '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 ...', @@ -329,10 +327,10 @@ return array( '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', 'New attachment' => 'Új melléklet', 'New comment' => 'Új megjegyzés', + 'Comment updated' => 'Megjegyzés frissítve', 'New subtask' => 'Új részfeladat', 'Subtask updated' => 'Részfeladat frissítve', 'Task updated' => 'Feladat frissítve', @@ -432,7 +430,6 @@ return array( '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ás', 'Start date' => 'Kezdés dátuma', 'Time estimated' => 'Becsült időtartam', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'Napi projektösszefoglaló exportálása: %s', 'Exports' => 'Exportálások', 'This export contains the number of tasks per column grouped per day.' => 'Ez az export tartalmazza a feladatok számát oszloponként összesítve, napokra lebontva.', - 'Nothing to preview...' => 'Nincs semmi az előnézetben ...', - 'Preview' => 'Előnézet', - 'Write' => 'Szerkesztés', 'Active swimlanes' => 'Aktív folyamatok', 'Add a new swimlane' => 'Új folyamat', 'Change default swimlane' => 'Alapértelmezett folyamat változtatás', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Feladat életkora napokban', 'Days in this column' => 'Napok ebben az oszlopban', '%dd' => '%dd', - 'Add a link' => 'Hivatkozás hozzáadása', 'Add a new link' => 'Új hivatkozás hozzáadása', 'Do you really want to remove this link: "%s"?' => 'Biztos törölni akarja a hivatkozást: "%s"?', 'Do you really want to remove this link with task #%d?' => 'Biztos törölni akarja a(z) #%d. feladatra mutató hivatkozást?', @@ -749,8 +742,6 @@ return array( // 'My activity stream' => '', // 'My calendar' => '', // 'Search tasks' => '', - // 'Back to the calendar' => '', - // 'Filters' => '', // 'Reset filters' => '', // 'My tasks due tomorrow' => '', // 'Tasks due today' => '', @@ -850,7 +841,6 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', // 'Milestone' => '', @@ -902,7 +892,6 @@ return array( // 'Shared' => '', // 'Owner' => '', // 'Unread notifications' => '', - // 'My filters' => '', // 'Notification methods:' => '', // 'Import tasks from CSV file' => '', // 'Unable to read your file' => '', @@ -940,6 +929,7 @@ return array( // 'Usernames must be lowercase and unique' => '', // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', + // 'Link type' => '', // 'Assign automatically a category based on a link' => '', // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', @@ -1049,7 +1039,6 @@ return array( // 'Close a task when there is no activity' => '', // 'Duration in days' => '', // 'Send email when there is no activity on a task' => '', - // 'List of external links' => '', // 'Unable to fetch link information.' => '', // 'Daily background job for tasks' => '', // 'Auto' => '', @@ -1067,9 +1056,7 @@ return array( // 'External link' => '', // 'Copy and paste your link here...' => '', // 'URL' => '', - // 'There is no external link for the moment.' => '', // 'Internal links' => '', - // 'There is no internal link for the moment.' => '', // 'Assign to me' => '', // 'Me' => '', // 'Do not duplicate anything' => '', @@ -1077,7 +1064,6 @@ return array( // 'Users management' => '', // 'Groups management' => '', // 'Create from another project' => '', - // 'There is no subtask at the moment.' => '', // 'open' => '', // 'closed' => '', // 'Priority:' => '', @@ -1096,7 +1082,6 @@ return array( // 'Started:' => '', // 'Moved:' => '', // 'Task #%d' => '', - // 'Sub-tasks' => '', // 'Date and time format' => '', // 'Time format' => '', // 'Start date: ' => '', @@ -1137,7 +1122,6 @@ return array( // 'User filters' => '', // 'Category filters' => '', // 'Upload a file' => '', - // 'There is no attachment at the moment.' => '', // 'View file' => '', // 'Last activity' => '', // 'Change subtask position' => '', @@ -1151,4 +1135,36 @@ return array( // 'There is no action at the moment.' => '', // 'Import actions from another project' => '', // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php index b4b8dcd4..bd1dd684 100644 --- a/app/Locale/id_ID/translations.php +++ b/app/Locale/id_ID/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Semua proyek', 'Add a new column' => 'Tambah kolom baru', 'Title' => 'Judul', - 'Nobody assigned' => 'Tidak ada yang ditugaskan', 'Assigned to %s' => 'Ditugaskan ke %s', 'Remove a column' => 'Hapus kolom', 'Remove a column from a board' => 'Hapus kolom dari papan', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Jumlah tugas', 'User' => 'Pengguna', 'Comments' => 'Komentar', - 'Write your text in Markdown' => 'Menulis teks anda didalam Markdown', 'Leave a comment' => 'Tinggalkan komentar', 'Comment is required' => 'Komentar diperlukan', 'Leave a description' => 'Tinggalkan deskripsi', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Pelacakan waktu :', 'New sub-task' => 'Sub-tugas baru', 'New attachment added "%s"' => 'Lampiran baru ditambahkan « %s »', - 'Comment updated' => 'Komentar diperbaharui', 'New comment posted by %s' => 'Komentar baru ditambahkan oleh « %s »', 'New attachment' => 'Lampirkan baru', 'New comment' => 'Komentar baru', + 'Comment updated' => 'Komentar diperbaharui', 'New subtask' => 'Sub-tugas baru', 'Subtask updated' => 'Sub-tugas diperbaharui', 'Task updated' => 'Tugas diperbaharui', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO selalu diterima, contoh : « %s » et « %s »', 'New private project' => 'Proyek pribadi baru', 'This project is private' => 'Proyek ini adalah pribadi', - 'Type here to create a new sub-task' => 'Ketik disini untuk membuat sub-tugas baru', 'Add' => 'Tambah', 'Start date' => 'Tanggal mulai', 'Time estimated' => 'Perkiraan waktu', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'Ekspor ringkasan proyek harian untuk « %s »', 'Exports' => 'Ekspor', 'This export contains the number of tasks per column grouped per day.' => 'Ekspor ini berisi jumlah dari tugas per kolom dikelompokan perhari.', - 'Nothing to preview...' => 'Tidak ada yang dapat dilihat...', - 'Preview' => 'Preview', - 'Write' => 'Tulis', 'Active swimlanes' => 'Swimlanes aktif', 'Add a new swimlane' => 'Tambah swimlane baru', 'Change default swimlane' => 'Modifikasi standar swimlane', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Usia tugas dalam hari', 'Days in this column' => 'Hari dalam kolom ini', '%dd' => '%dj', - 'Add a link' => 'Menambahkan tautan', 'Add a new link' => 'Tambah tautan baru', 'Do you really want to remove this link: "%s"?' => 'Apakah anda yakin akan menghapus tautan ini : « %s » ?', 'Do you really want to remove this link with task #%d?' => 'Apakah anda yakin akan menghapus tautan ini dengan tugas n°%d ?', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'Aliran kegiatan saya', 'My calendar' => 'Kalender saya', 'Search tasks' => 'Cari tugas', - 'Back to the calendar' => 'Kembali ke kalender', - 'Filters' => 'Filter', 'Reset filters' => 'Reset ulang filter', 'My tasks due tomorrow' => 'Tugas saya yang berakhir besok', 'Tasks due today' => 'Tugas yang berakhir hari ini', @@ -850,7 +841,6 @@ return array( 'End date:' => 'Waktu berakhir :', 'There is no start date or end date for this project.' => 'Tidak ada waktu mulai atau waktu berakhir untuk proyek ini', 'Projects Gantt chart' => 'Proyek grafik Gantt', - 'Link type' => 'Tipe tautan', 'Change task color when using a specific task link' => 'Rubah warna tugas ketika menggunakan tautan tugas yang spesifik', 'Task link creation or modification' => 'Tautan pembuatan atau modifikasi tugas ', 'Milestone' => 'Milestone', @@ -902,7 +892,6 @@ return array( // 'Shared' => '', // 'Owner' => '', // 'Unread notifications' => '', - // 'My filters' => '', // 'Notification methods:' => '', // 'Import tasks from CSV file' => '', // 'Unable to read your file' => '', @@ -940,6 +929,7 @@ return array( // 'Usernames must be lowercase and unique' => '', // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', + 'Link type' => 'Tipe tautan', // 'Assign automatically a category based on a link' => '', // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', @@ -1049,7 +1039,6 @@ return array( // 'Close a task when there is no activity' => '', // 'Duration in days' => '', // 'Send email when there is no activity on a task' => '', - // 'List of external links' => '', // 'Unable to fetch link information.' => '', // 'Daily background job for tasks' => '', // 'Auto' => '', @@ -1067,9 +1056,7 @@ return array( // 'External link' => '', // 'Copy and paste your link here...' => '', // 'URL' => '', - // 'There is no external link for the moment.' => '', // 'Internal links' => '', - // 'There is no internal link for the moment.' => '', // 'Assign to me' => '', // 'Me' => '', // 'Do not duplicate anything' => '', @@ -1077,7 +1064,6 @@ return array( // 'Users management' => '', // 'Groups management' => '', // 'Create from another project' => '', - // 'There is no subtask at the moment.' => '', // 'open' => '', // 'closed' => '', // 'Priority:' => '', @@ -1096,7 +1082,6 @@ return array( // 'Started:' => '', // 'Moved:' => '', // 'Task #%d' => '', - // 'Sub-tasks' => '', // 'Date and time format' => '', // 'Time format' => '', // 'Start date: ' => '', @@ -1137,7 +1122,6 @@ return array( // 'User filters' => '', // 'Category filters' => '', // 'Upload a file' => '', - // 'There is no attachment at the moment.' => '', // 'View file' => '', // 'Last activity' => '', // 'Change subtask position' => '', @@ -1151,4 +1135,36 @@ return array( // 'There is no action at the moment.' => '', // 'Import actions from another project' => '', // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index bba629cb..cee1c16a 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Tutti i progetti', 'Add a new column' => 'Aggiungi una nuova colonna', 'Title' => 'Titolo', - 'Nobody assigned' => 'Nessuno assegnato', 'Assigned to %s' => 'Assegnato a %s', 'Remove a column' => 'Cancella questa colonna', 'Remove a column from a board' => 'Cancella una colonna da una bacheca', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Numero di task', 'User' => 'Utente', 'Comments' => 'Commenti', - 'Write your text in Markdown' => 'Scrivi il testo in Markdown', 'Leave a comment' => 'Lascia un commento', 'Comment is required' => 'Si richiede un commento', 'Leave a description' => 'Lascia una descrizione', @@ -329,10 +327,10 @@ return array( // 'Time tracking:' => '', 'New sub-task' => 'Nuovo sotto-task', 'New attachment added "%s"' => 'Nuovo allegato aggiunto "%s"', - 'Comment updated' => 'Commento aggiornato', 'New comment posted by %s' => 'Nuovo commento aggiunto da "%s"', 'New attachment' => 'Nuovo allegato', 'New comment' => 'Nuovo commento', + 'Comment updated' => 'Commento aggiornato', 'New subtask' => 'Nuovo sotto-task', 'Subtask updated' => 'Sotto-task aggiornato', 'Task updated' => 'Task aggiornato', @@ -432,13 +430,12 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'Il formato ISO è sempre accettato, esempio: "%s" e "%s"', 'New private project' => 'Nuovo progetto privato', 'This project is private' => 'Questo progetto è privato', - 'Type here to create a new sub-task' => 'Scrivi qui per creare un sotto-task', 'Add' => 'Aggiungi', 'Start date' => 'Data di inizio', 'Time estimated' => 'Tempo stimato', 'There is nothing assigned to you.' => 'Non c\'è nulla assegnato a te.', 'My tasks' => 'I miei task', - 'Activity stream' => 'Flusso di attività', + 'Activity stream' => 'Flusso attività', 'Dashboard' => 'Bacheca', 'Confirmation' => 'Conferma', 'Allow everybody to access to this project' => 'Abilita tutti ad accedere a questo progetto', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'Export del sommario giornaliero del progetto per "%s"', 'Exports' => 'Esporta', 'This export contains the number of tasks per column grouped per day.' => 'Questo export contiene il numero di task per colonna raggruppati per giorno', - 'Nothing to preview...' => 'Nessuna anteprima...', - 'Preview' => 'Anteprima', - 'Write' => 'Scrivi', 'Active swimlanes' => 'Corsie attive', 'Add a new swimlane' => 'Aggiungi una corsia', 'Change default swimlane' => 'Cambia la corsia predefinita', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Anzianità del task in giorni', 'Days in this column' => 'Giorni in questa colonna', '%dd' => '%dg', - 'Add a link' => 'Aggiungi una relazione', 'Add a new link' => 'Aggiungi una nuova relazione', 'Do you really want to remove this link: "%s"?' => 'Vuoi davvero rimuovere la seguente relazione: "%s"?', 'Do you really want to remove this link with task #%d?' => 'Vuoi davvero rimuovere questa relazione dal task #%d?', @@ -746,11 +739,9 @@ return array( 'Start timer' => 'Avvia il timer', 'Add project member' => 'Aggiungi un membro di progetto', 'Enable notifications' => 'Abilita notifiche', - 'My activity stream' => 'Il mio flusso di attività', + 'My activity stream' => 'Il mio flusso attività', 'My calendar' => 'Il mio calendario', 'Search tasks' => 'Ricerca task', - 'Back to the calendar' => 'Torna al calendario', - 'Filters' => 'Filtri', 'Reset filters' => 'Annulla filtri', 'My tasks due tomorrow' => 'I miei task da completare per domani', 'Tasks due today' => 'Task da completare oggi', @@ -771,7 +762,7 @@ return array( 'Keyboard shortcut: "%s"' => 'Scorciatoia da tastiera: "%s"', 'List' => 'Lista', 'Filter' => 'Filtro', - 'Advanced search' => 'Riceca avanzata', + 'Advanced search' => 'Ricerca avanzata', 'Example of query: ' => 'Esempio di query: ', 'Search by project: ' => 'Ricerca per progetto: ', 'Search by column: ' => 'Ricerca per colonna: ', @@ -842,7 +833,7 @@ return array( 'Users overview' => 'Panoramica utenti', 'Members' => 'Membri', 'Shared project' => 'Progetto condiviso', - 'Project managers' => 'Manager del progetto', + 'Project managers' => 'Manager di progetto', 'Gantt chart for all projects' => 'Grafico Gantt per tutti i progetti', 'Projects list' => 'Elenco progetti', 'Gantt chart for this project' => 'Grafico Gantt per questo progetto', @@ -850,7 +841,6 @@ return array( 'End date:' => 'Data di fine:', 'There is no start date or end date for this project.' => 'Non è prevista una data di inzio o fine per questo progetto.', 'Projects Gantt chart' => 'Grafico Gantt dei progetti', - 'Link type' => 'Tipo di link', 'Change task color when using a specific task link' => 'Cambia colore del task quando si un utilizza una determinata relazione di task', 'Task link creation or modification' => 'Creazione o modifica di relazione di task', // 'Milestone' => '', @@ -902,7 +892,6 @@ return array( 'Shared' => 'Condiviso', 'Owner' => 'Proprietario', 'Unread notifications' => 'Notifiche non lette', - 'My filters' => 'I miei filtri', 'Notification methods:' => 'Metodi di notifica', 'Import tasks from CSV file' => 'Importa task da file CSV', 'Unable to read your file' => 'Impossibile leggere il file', @@ -940,6 +929,7 @@ return array( 'Usernames must be lowercase and unique' => 'I nomi utente devono essere in minuscolo e univoci', 'Passwords will be encrypted if present' => 'Se presenti, le password verranno criptate', '%s attached a new file to the task %s' => '%s ha allegato un nuovo file al task %s', + 'Link type' => 'Tipo di link', 'Assign automatically a category based on a link' => 'Assegna automaticamente una categoria sulla base di una relazione', 'BAM - Konvertible Mark' => 'BAM - Marco bosniaco', 'Assignee Username' => 'Nome utente dell\'assegnatario', @@ -990,7 +980,7 @@ return array( 'Group Name' => 'Nome del gruppo', 'Enter group name...' => 'Inserisci il nome del gruppo...', 'Role:' => 'Ruolo:', - 'Project members' => 'Membri del progetto', + 'Project members' => 'Membri di progetto', 'Compare hours for "%s"' => 'Confronta le ore per "%s"', '%s mentioned you in the task #%d' => '%s ti ha menzionato nel task #%d', '%s mentioned you in a comment on the task #%d' => '%s ti ha menzionato in un commento del task #%d', @@ -1049,7 +1039,6 @@ return array( 'Close a task when there is no activity' => 'Chiudi un task quando non c\'è nessuna attività', 'Duration in days' => 'Durata in giorni', 'Send email when there is no activity on a task' => 'Invia un\'email quando non c\'è attività sul task', - 'List of external links' => 'Elenco dei link esterni', 'Unable to fetch link information.' => 'Impossibile recuperare informazioni sul link.', 'Daily background job for tasks' => 'Job giornaliero in background per i task', // 'Auto' => '', @@ -1067,17 +1056,14 @@ return array( 'External link' => 'Link esterno', 'Copy and paste your link here...' => 'Copia e incolla il tuo link qui...', // 'URL' => '', - 'There is no external link for the moment.' => 'Nessun link esterno presente al momento.', 'Internal links' => 'Link interni', - 'There is no internal link for the moment.' => 'Nessun link interno presente al momento.', 'Assign to me' => 'Assegna a me', - // 'Me' => '', + 'Me' => 'Me stesso', 'Do not duplicate anything' => 'Non duplicare nulla', 'Projects management' => 'Gestione progetti', 'Users management' => 'Gestione utenti', 'Groups management' => 'Gestione gruppi', 'Create from another project' => 'Crea da un\'altro progetto', - 'There is no subtask at the moment.' => 'Nessun sotto-task presente al momento.', 'open' => 'aperto', 'closed' => 'chiuso', 'Priority:' => 'Priorità:', @@ -1096,7 +1082,6 @@ return array( 'Started:' => 'Iniziato:', 'Moved:' => 'Spostato:', // 'Task #%d' => '', - 'Sub-tasks' => 'Sotto-task', 'Date and time format' => 'Formato data e ora', 'Time format' => 'Formato ora', 'Start date: ' => 'Data di inizio: ', @@ -1137,18 +1122,49 @@ return array( 'User filters' => 'Filtri utente', 'Category filters' => 'Filtri di categorie', 'Upload a file' => 'Carica un file', - 'There is no attachment at the moment.' => 'Nessun allegato presente.', 'View file' => 'Visualizza file', 'Last activity' => 'Attività recente', 'Change subtask position' => 'Cambia la posizione del sotto-task', 'This value must be greater than %d' => 'Questo valore deve essere magiore di %d', 'Another swimlane with the same name exists in the project' => 'Un\'altra corsia con lo stesso nome è già esistente in questo progetto', 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Esempio: http://example.kanboard.net/ (usato per generare URL assolute)', - // 'Actions duplicated successfully.' => '', - // 'Unable to duplicate actions.' => '', - // 'Add a new action' => '', - // 'Import from another project' => '', - // 'There is no action at the moment.' => '', - // 'Import actions from another project' => '', - // 'There is no available project.' => '', + 'Actions duplicated successfully.' => 'Azioni duplicate con successo.', + 'Unable to duplicate actions.' => 'Impossibile duplicare le azioni.', + 'Add a new action' => 'Aggiungi una nuova azione', + 'Import from another project' => 'Importa da un altro progetto', + 'There is no action at the moment.' => 'Nessuna azione disponibile al momento.', + 'Import actions from another project' => 'Importa azioni da un altro progetto', + 'There is no available project.' => 'Nessun progetto disponibile.', + 'Local File' => 'File locale', + 'Configuration' => 'Configurazione', + 'PHP version:' => 'Versione PHP:', + // 'PHP SAPI:' => '', + 'OS version:' => 'Versione OS:', + 'Database version:' => 'Versione database:', + // 'Browser:' => '', + 'Task view' => 'Vista dei task', + 'Edit task' => 'Modifica task', + 'Edit description' => 'Modifica descrizione', + 'New internal link' => 'Nuovo link interno', + 'Display list of keyboard shortcuts' => 'Mostra una lista di scorciatoie da tastiera', + // 'Menu' => '', + 'Set start date' => 'Imposta la data di inzio', + // 'Avatar' => '', + 'Upload my avatar image' => 'Carica l\'immagine del mio avatar', + 'Remove my image' => 'Rimuovi la mia immagine', + 'The OAuth2 state parameter is invalid' => 'Il parametro di stato OAuth2 non è valido.', + 'User not found.' => 'Utente non trovato.', + 'Search in activity stream' => 'Ricerca nel mio flusso attività', + 'My activities' => 'Le mie attività', + 'Activity until yesterday' => 'Attività ad oggi', + 'Activity until today' => 'Attività fino a ieri', + 'Search by creator: ' => 'Ricerca per creatore: ', + 'Search by creation date: ' => 'Ricerca per data di creazione: ', + 'Search by task status: ' => 'Ricerca per stato del task: ', + 'Search by task title: ' => 'Ricerca per titolo del task: ', + 'Activity stream search' => 'Ricerca nel flusso attività', + 'Projects where "%s" is manager' => 'Progetti all\'interno dei quali "%s" è manager', + 'Projects where "%s" is member' => 'Progetti all\'interno dei quali "%s" è membro', + 'Open tasks assigned to "%s"' => 'Task aperti assegnati a "%s"', + 'Closed tasks assigned to "%s"' => 'Task chiusi assegnati a "%s"', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index e4b41c93..89769edd 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'すべてのプロジェクト', 'Add a new column' => 'カラムの追加', 'Title' => 'タイトル', - 'Nobody assigned' => '担当なし', 'Assigned to %s' => '%sが担当', 'Remove a column' => 'カラムの削除', 'Remove a column from a board' => 'ボードからカラムの削除', @@ -166,7 +165,6 @@ return array( 'Task count' => 'タスク数', 'User' => 'ユーザ', 'Comments' => 'コメント', - 'Write your text in Markdown' => 'Markdown 記法で書く', 'Leave a comment' => 'コメントを書く', 'Comment is required' => 'コメントを入力してください', 'Leave a description' => '説明を書く', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => '時間計測:', 'New sub-task' => '新しいサブタスク', 'New attachment added "%s"' => '添付ファイル「%s」が追加されました', - 'Comment updated' => 'コメントが更新されました', 'New comment posted by %s' => '「%s」の新しいコメントが追加されました', 'New attachment' => '新しい添付ファイル', 'New comment' => '新しいコメント', + 'Comment updated' => 'コメントが更新されました', 'New subtask' => '新しいサブタスク', 'Subtask updated' => 'サブタスクの更新', 'Task updated' => 'タスクの更新', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO フォーマットが入力できます(例: %s または %s)', 'New private project' => '非公開プロジェクトを作る', 'This project is private' => 'このプロジェクトは非公開です', - 'Type here to create a new sub-task' => 'サブタスクを追加するにはここに入力してください', 'Add' => '追加', 'Start date' => '開始時間', 'Time estimated' => '予想時間', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => '「%s」の日時プロジェクトサマリーの出力', 'Exports' => '出力', 'This export contains the number of tasks per column grouped per day.' => 'この出力は日時のカラムごとのタスク数を集計したものです', - 'Nothing to preview...' => 'プレビューがありません', - 'Preview' => 'プレビュー', - 'Write' => '書く', 'Active swimlanes' => 'アクティブなスイムレーン', 'Add a new swimlane' => '新しいスイムレーン', 'Change default swimlane' => 'デフォルトスイムレーンの変更', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'タスクの経過日数', 'Days in this column' => 'カラムでの経過日数', '%dd' => '%d 日', - 'Add a link' => 'リンクの追加', 'Add a new link' => '新しいリンクの追加', 'Do you really want to remove this link: "%s"?' => 'リンク「%s」を本当に削除しますか?', 'Do you really want to remove this link with task #%d?' => 'このリンクとタスク#%dを削除しますか?', @@ -749,8 +742,6 @@ return array( // 'My activity stream' => '', // 'My calendar' => '', // 'Search tasks' => '', - // 'Back to the calendar' => '', - // 'Filters' => '', // 'Reset filters' => '', // 'My tasks due tomorrow' => '', // 'Tasks due today' => '', @@ -850,7 +841,6 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', // 'Milestone' => '', @@ -902,7 +892,6 @@ return array( // 'Shared' => '', // 'Owner' => '', // 'Unread notifications' => '', - // 'My filters' => '', // 'Notification methods:' => '', // 'Import tasks from CSV file' => '', // 'Unable to read your file' => '', @@ -940,6 +929,7 @@ return array( // 'Usernames must be lowercase and unique' => '', // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', + // 'Link type' => '', // 'Assign automatically a category based on a link' => '', // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', @@ -1049,7 +1039,6 @@ return array( // 'Close a task when there is no activity' => '', // 'Duration in days' => '', // 'Send email when there is no activity on a task' => '', - // 'List of external links' => '', // 'Unable to fetch link information.' => '', // 'Daily background job for tasks' => '', // 'Auto' => '', @@ -1067,9 +1056,7 @@ return array( // 'External link' => '', // 'Copy and paste your link here...' => '', // 'URL' => '', - // 'There is no external link for the moment.' => '', // 'Internal links' => '', - // 'There is no internal link for the moment.' => '', // 'Assign to me' => '', // 'Me' => '', // 'Do not duplicate anything' => '', @@ -1077,7 +1064,6 @@ return array( // 'Users management' => '', // 'Groups management' => '', // 'Create from another project' => '', - // 'There is no subtask at the moment.' => '', // 'open' => '', // 'closed' => '', // 'Priority:' => '', @@ -1096,7 +1082,6 @@ return array( // 'Started:' => '', // 'Moved:' => '', // 'Task #%d' => '', - // 'Sub-tasks' => '', // 'Date and time format' => '', // 'Time format' => '', // 'Start date: ' => '', @@ -1137,7 +1122,6 @@ return array( // 'User filters' => '', // 'Category filters' => '', // 'Upload a file' => '', - // 'There is no attachment at the moment.' => '', // 'View file' => '', // 'Last activity' => '', // 'Change subtask position' => '', @@ -1151,4 +1135,36 @@ return array( // 'There is no action at the moment.' => '', // 'Import actions from another project' => '', // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/ko_KR/translations.php b/app/Locale/ko_KR/translations.php new file mode 100644 index 00000000..ed9e3b86 --- /dev/null +++ b/app/Locale/ko_KR/translations.php @@ -0,0 +1,1170 @@ +<?php + +return array( + // 'number.decimals_separator' => '', + // 'number.thousands_separator' => '', + 'None' => '없음', + 'edit' => '수정', + 'Edit' => '수정', + 'remove' => '삭제', + 'Remove' => '삭제', + 'Yes' => '예', + 'No' => '아니오', + 'cancel' => '취소', + 'or' => '또는', + 'Yellow' => '노랑', + 'Blue' => '파랑', + 'Green' => '초록', + 'Purple' => '보라', + 'Red' => '빨강', + 'Orange' => '주황', + 'Grey' => '회색', + 'Brown' => '브라운', + // 'Deep Orange' => '', + // 'Dark Grey' => '', + // 'Pink' => '', + // 'Teal' => '', + // 'Cyan' => '', + // 'Lime' => '', + // 'Light Green' => '', + // 'Amber' => '', + 'Save' => '저장', + 'Login' => '로그인', + 'Official website:' => '공식 웹사이트:', + 'Unassigned' => '담당자 없음', + 'View this task' => '이 할일 보기', + 'Remove user' => '사용자 삭제', + 'Do you really want to remove this user: "%s"?' => '사용자 "%s"를 정말로 삭제하시겠습니까?', + 'New user' => '사용자를 추가하는 ', + 'All users' => '모든 사용자', + 'Username' => '사용자 이름', + 'Password' => '패스워드', + 'Administrator' => '관리자', + 'Sign in' => '로그인', + 'Users' => '사용자', + 'No user' => '사용자가 없습니다', + 'Forbidden' => '접근 거부', + 'Access Forbidden' => '접속이 거부되었습니다', + 'Edit user' => '사용자를 변경하는 ', + 'Logout' => '로그아웃', + 'Bad username or password' => '사용자 이름 또는 패스워드가 다릅니다.', + 'Edit project' => '프로젝트 수정', + 'Name' => '이름', + 'Projects' => '프로젝트', + 'No project' => '프로젝트가 없습니다', + 'Project' => '프로젝트', + 'Status' => '상태', + 'Tasks' => '할일', + 'Board' => '보드', + 'Actions' => 'Actions', + 'Inactive' => '무효', + 'Active' => '유효', + '%d tasks on the board' => '%d개의 할일', + '%d tasks in total' => '총 %d개의 할일', + 'Unable to update this board.' => '보드를 갱신할 수 없었습니다', + 'Edit board' => '보드를 변경하는 ', + 'Disable' => '비활성화', + 'Enable' => '유효하게 한다', + 'New project' => '새 프로젝트', + // 'Do you really want to remove this project: "%s"?' => '', + 'Remove project' => '프로젝트의 삭제', + // 'Edit the board for "%s"' => '', + 'All projects' => '모든 프로젝트', + 'Add a new column' => '칼럼의 추가', + 'Title' => '제목', + 'Assigned to %s' => '담당자 %s', + 'Remove a column' => '칼럼 삭제', + 'Remove a column from a board' => '보드에서 칼럼 삭제', + 'Unable to remove this column.' => '(※)컬럼을 삭제할 수 없었습니다.', + // 'Do you really want to remove this column: "%s"?' => '', + 'This action will REMOVE ALL TASKS associated to this column!' => '이 조작은 이 컬럼에 할당된 『 모든 할일을 삭제 』합니다!', + 'Settings' => '설정', + 'Application settings' => '애플리케이션의 설정', + 'Language' => '언어', + 'Webhook token:' => 'Webhook토큰:', + 'API token:' => 'API토큰:', + 'Database size:' => '데이터베이스의 사이즈:', + 'Download the database' => '데이터베이스의 다운로드', + 'Optimize the database' => '데이터베이스 최적화', + '(VACUUM command)' => '(VACUUM명령)', + '(Gzip compressed Sqlite file)' => '(GZip명령으로 압축된 Sqlite파일)', + 'Close a task' => '할일 마치기', + 'Edit a task' => '할일 수정', + 'Column' => '칼럼', + 'Color' => '색', + 'Assignee' => '담당자', + 'Create another task' => '다른 할일 추가', + 'New task' => '새로운 할일', + 'Open a task' => '할일 열기', + // 'Do you really want to open this task: "%s"?' => '', + 'Back to the board' => '보드로 돌아가기', + 'There is nobody assigned' => '담당자가 없습니다', + 'Column on the board:' => '칼럼:', + 'Close this task' => '할일 마치기', + 'Open this task' => '할일을 열다', + 'There is no description.' => '설명이 없다', + 'Add a new task' => '할일을 추가하는 ', + 'The username is required' => '사용자 이름이 필요합니다', + // 'The maximum length is %d characters' => '', + // 'The minimum length is %d characters' => '', + 'The password is required' => '패스워드가 필요합니다', + 'This value must be an integer' => '정수로 입력하세요', + 'The username must be unique' => '사용자 이름이 이미 사용되고 있습니다', + 'The user id is required' => '사용자 ID가 필요합니다', + 'Passwords don\'t match' => '패스워드가 일치하지 않습니다', + 'The confirmation is required' => '확인용 패스워드를 입력하세요', + 'The project is required' => '프로젝트가 필요합니다', + 'The id is required' => 'ID가 필요합니다', + 'The project id is required' => '프로젝트 ID가 필요합니다', + 'The project name is required' => '프로젝트 이름이 필요합니다', + 'The title is required' => '제목이 필요합니다', + 'Settings saved successfully.' => '설정을 저장하였습니다', + 'Unable to save your settings.' => '설정의 보존에 실패했습니다.', + 'Database optimization done.' => '데이터베이스 최적화가 끝났습니다.', + 'Your project have been created successfully.' => '프로젝트를 작성했습니다.', + 'Unable to create your project.' => '프로젝트의 작성에 실패했습니다.', + 'Project updated successfully.' => '프로젝트를 갱신했습니다.', + 'Unable to update this project.' => '프로젝트의 갱신에 실패했습니다.', + 'Unable to remove this project.' => '프로젝트의 삭제에 실패했습니다.', + 'Project removed successfully.' => '프로젝트를 삭제했습니다.', + 'Project activated successfully.' => '프로젝트를 유효로 했습니다.', + 'Unable to activate this project.' => '프로젝트의 유효하게 못했어요.', + 'Project disabled successfully.' => '프로젝트를 무효로 했습니다.', + 'Unable to disable this project.' => '프로젝트의 무효화할 수 없었습니다.', + 'Unable to open this task.' => '할일의 오픈에 실패했습니다.', + 'Task opened successfully.' => '할일을 오픈했습니다.', + 'Unable to close this task.' => '할일의 클로즈에 실패했습니다.', + 'Task closed successfully.' => '할일을 마쳤습니다.', + 'Unable to update your task.' => '할일의 갱신에 실패했습니다.', + 'Task updated successfully.' => '할일을 갱신했습니다.', + 'Unable to create your task.' => '할일의 추가에 실패했습니다.', + 'Task created successfully.' => '할일을 추가했습니다.', + 'User created successfully.' => '사용자를 추가했습니다.', + 'Unable to create your user.' => '사용자의 추가에 실패했습니다.', + 'User updated successfully.' => '사용자를 갱신했습니다.', + 'Unable to update your user.' => '사용자의 갱신에 실패했습니다.', + 'User removed successfully.' => '사용자를 삭제했습니다.', + 'Unable to remove this user.' => '사용자 삭제에 실패했습니다.', + 'Board updated successfully.' => '보드를 갱신했습니다.', + 'Ready' => '준비완료', + 'Backlog' => '요구사항', + 'Work in progress' => '진행중', + 'Done' => '완료', + 'Application version:' => '애플리케이션의 버전:', + 'Id' => 'ID', + '%d closed tasks' => '%d개의 마친 할일', + 'No task for this project' => '이 프로젝트에 할일이 없습니다', + 'Public link' => '공개 접속 링크', + 'Change assignee' => '담당자 변경', + 'Change assignee for the task "%s"' => '할일 "%s"의 담당자를 변경', + 'Timezone' => '시간대', + 'Sorry, I didn\'t find this information in my database!' => '데이터베이스에서 정보가 발견되지 않았습니다!', + 'Page not found' => '페이지가 발견되지 않는다', + 'Complexity' => '복잡도', + 'Task limit' => '할일 수 제한', + 'Task count' => '할일 수', + 'User' => '사용자', + 'Comments' => '댓글', + 'Leave a comment' => '댓글 남기기', + 'Comment is required' => '댓글을 입력하세요', + 'Leave a description' => '설명을 입력하세요', + 'Comment added successfully.' => '의견을 추가했습니다.', + 'Unable to create your comment.' => '댓글의 추가에 실패했습니다.', + 'Edit this task' => '할일 수정', + 'Due Date' => '마감일', + 'Invalid date' => '날짜가 무효입니다', + 'Automatic actions' => '자동액션 관리', + 'Your automatic action have been created successfully.' => '자동 액션을 작성했습니다.', + 'Unable to create your automatic action.' => '자동 액션의 작성에 실패했습니다.', + 'Remove an action' => '자동 액션의 삭제', + 'Unable to remove this action.' => '자동 액션의 삭제에 실패했습니다.', + 'Action removed successfully.' => '자동 액션의 삭제에 성공했어요.', + // 'Automatic actions for the project "%s"' => '', + 'Add an action' => '자동 액션 추가', + 'Event name' => '이벤트 이름', + 'Action name' => '액션 이름', + 'Action parameters' => '액션의 바로미터', + 'Action' => '액션', + 'Event' => '이벤트', + 'When the selected event occurs execute the corresponding action.' => '선택된 이벤트가 발생했을 때 대응하는 액션을 실행한다.', + 'Next step' => '다음 단계', + 'Define action parameters' => '액션의 바로미터', + // 'Do you really want to remove this action: "%s"?' => '', + 'Remove an automatic action' => '자동 액션의 삭제', + 'Assign the task to a specific user' => '할일 담당자를 할당', + 'Assign the task to the person who does the action' => '액션을 일으킨 사용자를 담당자이자', + 'Duplicate the task to another project' => ' 다른 프로젝트에 할일을 복제하는 ', + 'Move a task to another column' => '할일을 다른 칼럼에 이동하는 ', + 'Task modification' => '할일 변경', + 'Task creation' => '할일을 만들', + 'Closing a task' => '할일을 닫혔다', + 'Assign a color to a specific user' => '색을 사용자에 할당', + 'Column title' => '칼럼의 제목', + 'Position' => '위치', + 'Duplicate to another project' => '다른 프로젝트에 복사', + 'Duplicate' => '복사', + 'link' => '링크', + 'Comment updated successfully.' => '댓글을 갱신했습니다.', + 'Unable to update your comment.' => '댓글의 갱신에 실패했습니다.', + 'Remove a comment' => '댓글 삭제', + 'Comment removed successfully.' => '댓글을 삭제했습니다.', + 'Unable to remove this comment.' => '댓글의 삭제에 실패했습니다.', + 'Do you really want to remove this comment?' => '댓글을 삭제합니까?', + 'Current password for the user "%s"' => '사용자 "%s"의 현재 패스워드', + 'The current password is required' => '현재의 패스워드를 입력하세요', + 'Wrong password' => '패스워드가 다릅니다', + 'Unknown' => '불명', + 'Last logins' => '마지막 로그인', + 'Login date' => '로그인 일시', + 'Authentication method' => '인증 방법', + 'IP address' => 'IP 주소', + 'User agent' => '사용자 에이전트', + 'Persistent connections' => '세션', + 'No session.' => '세션 없음', + 'Expiration date' => '유효기간', + 'Remember Me' => '자동 로그인', + 'Creation date' => '작성일', + 'Everybody' => '모두', + 'Open' => '열림', + 'Closed' => '닫힘', + 'Search' => '검색', + 'Nothing found.' => '결과가 없습니다', + 'Due date' => '마감일', + 'Others formats accepted: %s and %s' => ' 다른 서식: %s 또는 %s', + 'Description' => '설명', + '%d comments' => '%d개의 댓글', + '%d comment' => '%d개의 댓글', + 'Email address invalid' => '메일 주소가 올바르지 않습니다.', + // 'Your external account is not linked anymore to your profile.' => '', + // 'Unable to unlink your external account.' => '', + // 'External authentication failed' => '', + // 'Your external account is linked to your profile successfully.' => '', + 'Email' => '이메일', + 'Task removed successfully.' => '할일을 삭제했습니다.', + 'Unable to remove this task.' => '할일 삭제에 실패했습니다.', + 'Remove a task' => '할일 삭제', + // 'Do you really want to remove this task: "%s"?' => '', + 'Assign automatically a color based on a category' => '카테고리에 바탕을 두고 색을 바꾸고', + 'Assign automatically a category based on a color' => '색에 바탕을 두고 카테고리를 바꾸었다', + 'Task creation or modification' => '할일의 작성 또는 변경', + 'Category' => '카테고리', + 'Category:' => '카테고리:', + 'Categories' => '카테고리', + 'Category not found.' => '카테고리가 발견되지 않습니다', + 'Your category have been created successfully.' => '카테고리를 작성했습니다.', + 'Unable to create your category.' => '카테고리의 작성에 실패했습니다.', + 'Your category have been updated successfully.' => '카테고리를 갱신했습니다.', + 'Unable to update your category.' => '카테고리의 갱신에 실패했습니다.', + 'Remove a category' => '카테고리의 삭제', + 'Category removed successfully.' => '카테고리를 삭제했습니다.', + 'Unable to remove this category.' => '카테고리를 삭제할 수 없었습니다.', + // 'Category modification for the project "%s"' => '', + 'Category Name' => '카테고리 이름', + 'Add a new category' => '카테고리의 추가', + // 'Do you really want to remove this category: "%s"?' => '', + 'All categories' => '모든 카테고리', + 'No category' => '카테고리 없음', + 'The name is required' => '이름을 입력하십시오', + 'Remove a file' => '파일 삭제', + 'Unable to remove this file.' => '파일 삭제에 실패했습니다.', + 'File removed successfully.' => '파일을 삭제했습니다.', + 'Attach a document' => '문서 첨부', + 'Do you really want to remove this file: "%s"?' => '파일 "%s" 을 삭제할까요?', + 'Attachments' => '첨부', + 'Edit the task' => '할일 수정', + 'Edit the description' => '설명 수정', + 'Add a comment' => '댓글 추가', + 'Edit a comment' => '댓글 수정', + 'Summary' => '개요', + 'Time tracking' => '시간 추적', + 'Estimate:' => '예측:', + 'Spent:' => '경과:', + 'Do you really want to remove this sub-task?' => '서브 할일을 삭제합니까?', + 'Remaining:' => '나머지:', + 'hours' => '시간', + 'spent' => '경과', + 'estimated' => '예측', + 'Sub-Tasks' => '서브 할일', + 'Add a sub-task' => '서브 할일 추가', + 'Original estimate' => '최초 예측시간', + 'Create another sub-task' => '다음 서브 할일 추가', + 'Time spent' => '경과시간', + 'Edit a sub-task' => '서브 할일을 변경하는 ', + 'Remove a sub-task' => '서브 할일을 삭제하는 ', + 'The time must be a numeric value' => '시간은 숫자로 입력하세요', + 'Todo' => '할일 예정', + 'In progress' => '할일 중', + 'Sub-task removed successfully.' => '서브 할일을 삭제했습니다.', + 'Unable to remove this sub-task.' => '서브 할일의 삭제가 실패했습니다.', + 'Sub-task updated successfully.' => '서브 할일을 갱신했습니다.', + 'Unable to update your sub-task.' => '서브 할일의 경신에 실패했습니다.', + 'Unable to create your sub-task.' => '서브 할일의 추가에 실패했습니다.', + 'Sub-task added successfully.' => '서브 할일을 추가했습니다.', + 'Maximum size: ' => '최대: ', + 'Unable to upload the file.' => '파일 업로드에 실패했습니다.', + 'Display another project' => '프로젝트 보기', + 'Created by %s' => '작성자 %s', + 'Tasks Export' => '할일 내보내기', + // 'Tasks exportation for "%s"' => '', + 'Start Date' => '시작일', + 'End Date' => '종료일', + 'Execute' => '실행', + 'Task Id' => '할일 ID', + 'Creator' => '작성자', + 'Modification date' => '변경 일', + 'Completion date' => '완료일', + 'Clone' => '복사', + 'Project cloned successfully.' => '프로젝트를 복제했습니다.', + 'Unable to clone this project.' => '프로젝트의 복제에 실패했습니다.', + '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"' => '', + 'New comment posted by %s' => '"%s"님이 댓글을 추가하였습니다', + 'New attachment' => ' 새로운 첨부 파일', + 'New comment' => ' 새로운 댓글', + 'Comment updated' => '댓글가 갱신되었습니다', + 'New subtask' => ' 새로운 서브 할일', + 'Subtask updated' => '서브 할일 갱신', + 'Task updated' => '할일 업데이트', + 'Task closed' => '할일 마침', + 'Task opened' => '할일 시작', + 'I want to receive notifications only for those projects:' => '다음 프로젝트의 알림만 받겠습니다:', + 'view the task on Kanboard' => 'Kanboard에서 할일을 본다', + 'Public access' => '공개 접속 설정', + 'Active tasks' => '활성화된 할일', + 'Disable public access' => '공개 접속 비활성화', + 'Enable public access' => '공개 접속 활성화', + 'Public access disabled' => '공개 접속 불가', + // 'Do you really want to disable 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' => '비활성화', + 'Username:' => '사용자명', + 'Name:' => '이름:', + 'Email:' => '이메일:', + 'Notifications:' => '알림:', + 'Notifications' => '알림', + 'Account type:' => '계정종류:', + 'Edit profile' => '프로필 변경', + 'Change password' => '패스워드 변경', + 'Password modification' => '패스워드 변경', + 'External authentications' => '외부 인증', + 'Never connected.' => '접속기록없음', + 'No external authentication enabled.' => '외부 인증이 설정되어 있지 않습니다.', + 'Password modified successfully.' => '패스워드를 변경했습니다.', + 'Unable to change the password.' => '비밀 번호가 변경할 수 없었습니다.', + 'Change category for the task "%s"' => '할일 "%s"의 카테고리의 변경', + 'Change category' => '카테고리 수정', + '%s updated the task %s' => '%s이 할일 %s을 업데이트했습니다', + // '%s opened the task %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 updated a comment on the task %s' => '%s이 할일%s의 댓글을 수정했습니다', + '%s commented the task %s' => '%s이 할일%s에 댓글을 남겼습니다', + '%s\'s activity' => '%s의 활동', + 'RSS feed' => 'RSS피드', + // '%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이 할일#%d을 갱신했습니다', + '%s created the task #%d' => '%s이 할일#%d을 추가했습니다', + '%s closed the task #%d' => '%s이 할일#%d을 닫혔습니다', + '%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"' => '', + 'Activity' => '활동', + // 'Default values are "%s"' => '', + // 'Default columns for new projects (Comma-separated)' => '', + 'Task assignee change' => '담당자의 변경', + // '%s change the assignee of the task #%d to %s' => '', + '%s changed the assignee of the task %s to %s' => '%s이 할일 %s의 담당을 %s로 변경했습니다', + 'New password for the user "%s"' => '사용자 "%s"의 새로운 패스워드', + 'Choose an event' => '행사의 선택', + 'Create a task from an external provider' => '할일을 외부 서비스로부터 작성하는 ', + 'Change the assignee based on an external username' => '담당자를 외부 서비스에 바탕을 두고 변경하는 ', + 'Change the category based on an external label' => '카테고리를 외부 서비스에 바탕을 두고 변경하는 ', + 'Reference' => '참조', + 'Label' => '라벨', + 'Database' => '데이터베이스', + 'About' => '정보', + 'Database driver:' => '데이터베이스 드라이버:', + 'Board settings' => '기본 설정', + 'URL and token' => 'URL와 토큰', + 'Webhook settings' => 'Webhook의 설정', + 'URL for task creation:' => 'Task작성의 URL:', + 'Reset token' => '토큰 리셋', + 'API endpoint:' => 'API엔드 포인트:', + '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' => '애플리케이션의 URL', + 'Token regenerated.' => '토큰이 다시 생성되었습니다.', + 'Date format' => '데이터 포맷', + // 'ISO format is always accepted, example: "%s" and "%s"' => '', + 'New private project' => '새 비공개 프로젝트', + 'This project is private' => '이 프로젝트는 비공개입니다', + 'Add' => '추가', + 'Start date' => '시작시간', + 'Time estimated' => '예상시간', + 'There is nothing assigned to you.' => '할일이 없습니다. 옆사람의 일을 도와주면 어떨까요?', + 'My tasks' => '내 할일', + 'Activity stream' => '활동기록', + 'Dashboard' => '대시보드', + 'Confirmation' => '확인', + 'Allow everybody to access to this project' => '모든 사람이 이 프로젝트에 접근할 수 있도록 합니다', + 'Everybody have access to this project.' => '누구나 이 프로젝트에 액세스 할 수 있습니다', + 'Webhooks' => 'Webhook', + 'API' => 'API', + 'Create a comment from an external provider' => '외부 서비스로부터 의견을 작성한다', + '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.' => '(※)컬럼을 삭제했습니다', + 'Not enough data to show the graph.' => '그래프를 선묘화하려면 나왔지만 부족합니다', + 'Previous' => ' 돌아가', + 'The id must be an integer' => 'id은 숫자가 아니면 안 됩니다', + 'The project id must be an integer' => 'project id은 숫자가 아니면 안 됩니다', + 'The status must be an integer' => 'status는 숫자지 않으면 안 됩니다', + 'The subtask id is required' => 'subtask id가 필요합니다', + 'The subtask id must be an integer' => 'subtask id은 숫자가 아니면 안 됩니다', + 'The task id is required' => 'task id가 필요합니다', + 'The task id must be an integer' => 'task id은 숫자가 아니면 안 됩니다', + 'The user id must be an integer' => 'user id은 숫자가 아니면 안 됩니다', + '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.' => '이 출력은 날짜의 칼람별 할일 수를 집계한 것입니다', + 'Active swimlanes' => '액티브한 스윔레인', + 'Add a new swimlane' => ' 새로운 스윔레인', + 'Change default swimlane' => '기본 스윔레인의 변경', + 'Default swimlane' => '기본 스윔레인', + // 'Do you really want to remove this swimlane: "%s"?' => '', + 'Inactive swimlanes' => '인터랙티브한 스윔레인', + 'Remove a swimlane' => '스윔레인의 삭제', + '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 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)' => '', + 'Integrations' => '연계', + 'Integration with third-party services' => '외부 서비스 연계', + 'Subtask Id' => '서브 할일 Id', + 'Subtasks' => '서브 할일', + 'Subtasks Export' => '서브 할일 출력', + // 'Subtasks exportation for "%s"' => '', + 'Task Title' => '할일 제목', + 'Untitled' => '제목 없음', + 'Application default' => '애플리케이션 기본', + 'Language:' => '언어:', + 'Timezone:' => '시간대:', + 'All columns' => '모든 칼럼', + 'Calendar' => '달력', + 'Next' => '다음에 ', + '#%d' => '#%d', + 'All swimlanes' => '모든 스윔레인', + 'All colors' => '모든 색', + // 'Moved to column %s' => '', + 'Change description' => '설명 수정', + 'User dashboard' => '대시보드', + 'Allow only one subtask in progress at the same time for a user' => '한 사용자에 대한 하나의 할일만 진행 중에 가능합니다', + // 'Edit column "%s"' => '', + // 'Select the new status of the subtask: "%s"' => '', + 'Subtask timesheet' => '서브 할일 타임시트', + 'There is nothing to show.' => '기록이 없습니다', + 'Time Tracking' => '타임 트레킹', + 'You already have one subtask in progress' => '이미 진행 중인 서브 할일가 있습니다.', + 'Which parts of the project do you want to duplicate?' => '프로젝트의 무엇을 복제합니까?', + // 'Disallow login form' => '', + 'Start' => '시작', + 'End' => '종료', + 'Task age in days' => '할일이 생긴 시간', + 'Days in this column' => '이 칼럼에 있는 시간', + '%dd' => '%d일', + 'Add a new link' => ' 새로운 링크 추가', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + 'Field required' => '필드가 필요합니다', + 'Link added successfully.' => '링크를 추가했습니다.', + 'Link updated successfully.' => '링크를 갱신했습니다.', + 'Link removed successfully.' => '링크를 삭제했습니다.', + 'Link labels' => '링크 라벨', + 'Link modification' => '링크의 변경', + 'Links' => '링크', + 'Link settings' => '링크 설정', + 'Opposite label' => '반대의 라벨', + 'Remove a link' => '라벨의 삭제', + 'Task\'s links' => '할일의 라벨', + 'The labels must be different' => ' 다른 라벨을 지정하세요', + 'There is no link.' => '링크가 없습니다', + 'This label must be unique' => '라벨은 독특할 필요가 있습니다', + 'Unable to create your link.' => '링크를 작성할 수 없었습니다.', + 'Unable to update your link.' => '링크를 갱신할 수 없었습니다.', + 'Unable to remove this link.' => '링크를 삭제할 수 없었습니다.', + 'relates to' => '연관 링크', + 'blocks' => '다음을 딜레이하는', + 'is blocked by' => '다음 때문에 딜레이되는', + 'duplicates' => '다음과 중복하는', + 'is duplicated by' => '다음에 중복되는', + 'is a child of' => '다음의 하위 할일', + 'is a parent of' => '다음의 상위 할일', + 'targets milestone' => '다음의 이정표를 목표로 하는', + 'is a milestone of' => '다음의 이정표인', + 'fixes' => '다음을 수정하는', + 'is fixed by' => '다음에 의해 수정되는', + 'This task' => '이 할일의 ', + '<1h' => '<1시간', + '%dh' => '%d시간', + 'Expand tasks' => '할일 크게', + 'Collapse tasks' => '할일 작게', + 'Expand/collapse tasks' => '할일 크게/작게', + 'Close dialog box' => '다이얼로그를 닫습니다', + 'Submit a form' => '제출', + 'Board view' => '보드 뷰', + 'Keyboard shortcuts' => '키보드 숏 컷', + 'Open board switcher' => '보드 전환을 열', + 'Application' => '애플리케이션', + 'Compact view' => '컴팩트 뷰', + 'Horizontal scrolling' => '세로 스크롤', + 'Compact/wide view' => '컴팩트/와이드 뷰', + 'No results match:' => '결과가 일치하지 않았습니다', + 'Currency' => '통화', + 'Private project' => '개인 프로젝트', + // 'AUD - Australian Dollar' => '', + // 'CAD - Canadian Dollar' => '', + // 'CHF - Swiss Francs' => '', + 'Custom Stylesheet' => '커스텀 스타일 시트', + 'download' => '다운로드', + // 'EUR - Euro' => '', + // 'GBP - British Pound' => '', + // 'INR - Indian Rupee' => '', + // 'JPY - Japanese Yen' => '', + // 'NZD - New Zealand Dollar' => '', + // 'RSD - Serbian dinar' => '', + // 'USD - US Dollar' => '', + 'Destination column' => '이동 후 칼럼', + 'Move the task to another column when assigned to a user' => '사용자의 할당을 하면 할일을 다른 칼럼에 이동', + 'Move the task to another column when assignee is cleared' => '사용자의 할당이 없어지면 할일을 다른 칼럼에 이동', + 'Source column' => '이동 전 칼럼', + 'Transitions' => '이력', + 'Executer' => '실행자', + 'Time spent in the column' => '칼럼에 있던 시간', + 'Task transitions' => '할일 천이', + 'Task transitions export' => '할일 천이를 출력', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '이 리포트는 할일의 칼럼 간 이동을 시간, 유저, 경과 시간과 함께 기록한 것입니다.', + 'Currency rates' => '환율', + 'Rate' => '레이트', + 'Change reference currency' => '현재의 기축 통화', + 'Add a new currency rate' => ' 새로운 통화 환율을 추가', + 'Reference currency' => '기축 통화', + // 'The currency rate have been added successfully.' => '', + 'Unable to add this currency rate.' => '이 통화 환율을 추가할 수 없습니다.', + 'Webhook URL' => 'Webhook URL', + '%s remove the assignee of the task %s' => '%s이 할일 %s의 담당을 삭제했습니다', + 'Enable Gravatar images' => 'Gravatar이미지를 활성화', + 'Information' => '정보', + 'Check two factor authentication code' => '2단 인증을 체크한다', + 'The two factor authentication code is not valid.' => '2단 인증 코드는 무효입니다.', + 'The two factor authentication code is valid.' => '2단 인증 코드는 유효합니다.', + 'Code' => '코드', + 'Two factor authentication' => '2단 인증', + // 'This QR code contains the key URI: ' => '', + 'Check my code' => '코드 체크', + // 'Secret key: ' => '', + 'Test your device' => '디바이스 테스트', + // 'Assign a color when the task is moved to a specific column' => '', + '%s via Kanboard' => '%s via E-board', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', + 'Screenshot taken %s' => '스크린샷_%s', + 'Add a screenshot' => '스크린샷 추가', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '스크린샷을 CTRL+V 혹은 ⌘+V를 눌러 붙여넣기', + 'Screenshot uploaded successfully.' => '스크린샷을 업로드하였습니다', + // 'SEK - Swedish Krona' => '', + // 'Identifier' => '', + // 'Disable two factor authentication' => '', + // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', + // 'Edit link' => '', + 'Start to type task title...' => '할일 제목을 처음부터 입력해주세요', + // 'A task cannot be linked to itself' => '', + // 'The exact same link already exists' => '', + // 'Recurrent task is scheduled to be generated' => '', + // 'Score' => '', + // 'The identifier must be unique' => '', + // 'This linked task id doesn\'t exists' => '', + // 'This value must be alphanumeric' => '', + 'Edit recurrence' => '반복 수정', + 'Generate recurrent task' => '반복 할일 만들기', + 'Trigger to generate recurrent task' => '반복 할일 트리거 만들기', + 'Factor to calculate new due date' => '새로운 종료날짜 계산', + 'Timeframe to calculate new due date' => '종료날짜 계산 단위', + 'Base date to calculate new due date' => '새로운 기본 종료날짜 계산', + 'Action date' => '시작날짜', + 'Base date to calculate new due date: ' => '새로운 기본 종료날짜 계산: ', + // 'This task has created this child task: ' => '', + 'Day(s)' => '일', + 'Existing due date' => '기존 종료날짜', + 'Factor to calculate new due date: ' => '새로운 종료날짜 계산: ', + 'Month(s)' => '월', + // 'Recurrence' => '', + // 'This task has been created by: ' => '', + // 'Recurrent task has been generated:' => '', + 'Timeframe to calculate new due date: ' => '종료날짜 계산 단위', + // 'Trigger to generate recurrent task: ' => '', + 'When task is closed' => '할일을 마쳤을때', + 'When task is moved from first column' => '할일이 첫번째 칼럼으로 옮겨졌을때', + 'When task is moved to last column' => '할일이 마지막 칼럼으로 옮겨졌을때', + 'Year(s)' => '년', + // 'Calendar settings' => '', + // 'Project calendar view' => '', + 'Project settings' => '프로젝트 설정', + // 'Show subtasks based on the time tracking' => '', + // 'Show tasks based on the creation date' => '', + // 'Show tasks based on the start date' => '', + // 'Subtasks time tracking' => '', + // 'User calendar view' => '', + // 'Automatically update the start date' => '', + // 'iCal feed' => '', + // 'Preferences' => '', + // 'Security' => '', + 'Two factor authentication disabled' => '2단 인증 비활성화', + // 'Two factor authentication enabled' => '', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', + // 'User that will receive the email' => '', + 'Email subject' => '이메일 제목', + 'Date' => '날짜', + // 'Add a comment log when moving the task between columns' => '', + // 'Move the task to another column when the category is changed' => '', + 'Send a task by email to someone' => '할일을 이메일로 보내기', + 'Reopen a task' => '할일 다시 시작', + 'Column change' => '칼럼 이동', + 'Position change' => '위치 이동', + 'Swimlane change' => '스윔레인 변경', + 'Assignee change' => '담당자 변경', + '[%s] Overdue tasks' => '[%s] 마감시간 지남', + 'Notification' => '알림', + // '%s moved the task #%d to the first swimlane' => '', + // '%s moved the task #%d to the swimlane "%s"' => '', + 'Swimlane' => '스윔레인', + // 'Gravatar' => '', + // '%s moved the task %s to the first swimlane' => '', + // '%s moved the task %s to the swimlane "%s"' => '', + // 'This report contains all subtasks information for the given date range.' => '', + // 'This report contains all tasks information for the given date range.' => '', + // 'Project activities for %s' => '', + // 'view the board on Kanboard' => '', + // 'The task have been moved to the first swimlane' => '', + // 'The task have been moved to another swimlane:' => '', + // 'Overdue tasks for the project "%s"' => '', + 'New title: %s' => '제목 변경: %s', + 'The task is not assigned anymore' => '담당자 없음', + 'New assignee: %s' => '담당자 변경: %s', + 'There is no category now' => '카테고리 없음', + 'New category: %s' => '카테고리 변경: %s', + 'New color: %s' => '색깔 변경: %s', + 'New complexity: %d' => '복잡도 변경: %d', + 'The due date have been removed' => '마감날짜 삭제', + 'There is no description anymore' => '설명 없음', + 'Recurrence settings have been modified' => '반복할일 설정 수정', + 'Time spent changed: %sh' => '경과시간 변경: %s시간', + 'Time estimated changed: %sh' => '%s시간으로 예상시간 변경', + // 'The field "%s" have been updated' => '', + // 'The description has been modified:' => '', + 'Do you really want to close the task "%s" as well as all subtasks?' => '할일 "%s"과 서브 할일을 모두 마치시겠습니까?', + 'I want to receive notifications for:' => '다음의 알림을 받기를 원합니다:', + 'All tasks' => '모든 할일', + 'Only for tasks assigned to me' => '내가 담당자인 일', + 'Only for tasks created by me' => '내가 만든 일', + 'Only for tasks created by me and assigned to me' => '내가 만들었거나 내가 담당자인 일', + // '%%Y-%%m-%%d' => '', + // 'Total for all columns' => '', + // 'You need at least 2 days of data to show the chart.' => '', + '<15m' => '<15분', + '<30m' => '<30분', + // 'Stop timer' => '', + 'Start timer' => '타이머 시작', + // 'Add project member' => '', + 'Enable notifications' => '알림 켜기', + 'My activity stream' => '내 활동기록', + 'My calendar' => '내 캘린더', + // 'Search tasks' => '', + 'Reset filters' => '필터 리셋', + 'My tasks due tomorrow' => '내일까지 내 할일', + 'Tasks due today' => '오늘까지 할일', + 'Tasks due tomorrow' => '내일까지 할일', + 'Tasks due yesterday' => '어제까지 할일', + 'Closed tasks' => '닫힌 할일', + 'Open tasks' => '열린 할일', + 'Not assigned' => '담당자가 없는 일', + 'View advanced search syntax' => '추가 검색 문법보기', + 'Overview' => '개요', + // 'Board/Calendar/List view' => '', + // 'Switch to the board view' => '', + // 'Switch to the calendar view' => '', + // 'Switch to the list view' => '', + // 'Go to the search/filter box' => '', + 'There is no activity yet.' => '활동이 없습니다', + // 'No tasks found.' => '', + // 'Keyboard shortcut: "%s"' => '', + 'List' => '목록', + // 'Filter' => '', + 'Advanced search' => '검색 문법', + 'Example of query: ' => '문법 예제 ', + 'Search by project: ' => '프로젝트로 찾기 ', + 'Search by column: ' => '칼럼으로 찾기 ', + 'Search by assignee: ' => '담당자로 찾기 ', + 'Search by color: ' => '색깔로 찾기 ', + 'Search by category: ' => '카테고리로 찾기 ', + 'Search by description: ' => '설명으로 찾기 ', + 'Search by due date: ' => '마감날짜로 찾기 ', + // 'Lead and Cycle time for "%s"' => '', + // 'Average time spent into each column for "%s"' => '', + // 'Average time spent into each column' => '', + // 'Average time spent' => '', + // 'This chart show the average time spent into each column for the last %d tasks.' => '', + // 'Average Lead and Cycle time' => '', + // 'Average lead time: ' => '', + // 'Average cycle time: ' => '', + 'Cycle Time' => '사이클 타임', + 'Lead Time' => '리드 타임', + // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', + // 'Average time into each column' => '', + // 'Lead and cycle time' => '', + 'Lead time: ' => '리드 타임: ', + 'Cycle time: ' => '사이클 타임: ', + 'Time spent into each column' => '각 칼럼에서 걸린 시간', + // 'The lead time is the duration between the task creation and the completion.' => '', + // 'The cycle time is the duration between the start date and the completion.' => '', + // 'If the task is not closed the current time is used instead of the completion date.' => '', + // 'Set automatically the start date' => '', + 'Edit Authentication' => '계정 수정', + // 'Remote user' => '', + // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', + // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', + 'New remote user' => '새로운 원격유저', + 'New local user' => '새로운 유저', + // 'Default task color' => '', + 'This feature does not work with all browsers.' => '이 기능은 일부 브라우저에서 작동하지 않습니다', + // 'There is no destination project available.' => '', + // 'Trigger automatically subtask time tracking' => '', + // 'Include closed tasks in the cumulative flow diagram' => '', + // 'Current swimlane: %s' => '', + // 'Current column: %s' => '', + // 'Current category: %s' => '', + // 'no category' => '', + // 'Current assignee: %s' => '', + // 'not assigned' => '', + // 'Author:' => '', + // 'contributors' => '', + // 'License:' => '', + // 'License' => '', + // 'Enter the text below' => '', + // 'Gantt chart for %s' => '', + // 'Sort by position' => '', + // 'Sort by date' => '', + // 'Add task' => '', + // 'Start date:' => '', + // 'Due date:' => '', + // 'There is no start date or due date for this task.' => '', + // 'Moving or resizing a task will change the start and due date of the task.' => '', + // 'There is no task in your project.' => '', + 'Gantt chart' => '간트차트', + 'People who are project managers' => '프로젝트 매니저', + 'People who are project members' => '프로젝트 멤버', + // 'NOK - Norwegian Krone' => '', + 'Show this column' => '칼럼 보이기', + 'Hide this column' => '칼럼 숨기기', + 'open file' => '열기', + 'End date' => '종료 날짜', + 'Users overview' => '유저 전체보기', + 'Members' => '멤버', + // 'Shared project' => '', + 'Project managers' => '프로젝트 매니저', + // 'Gantt chart for all projects' => '', + 'Projects list' => '프로젝트 리스트', + 'Gantt chart for this project' => '이 프로젝트 간트차트', + 'Project board' => '프로젝트 보드', + // 'End date:' => '', + 'There is no start date or end date for this project.' => '이 프로젝트에는 시작날짜와 종료날짜가 없습니다', + 'Projects Gantt chart' => '프로젝트 간트차트', + // 'Change task color when using a specific task link' => '', + // 'Task link creation or modification' => '', + // 'Milestone' => '', + // 'Documentation: %s' => '', + // 'Switch to the Gantt chart view' => '', + // 'Reset the search/filter box' => '', + // 'Documentation' => '', + // 'Table of contents' => '', + 'Gantt' => '간트', + // 'Author' => '', + // 'Version' => '', + // 'Plugins' => '', + // 'There is no plugin loaded.' => '', + 'Set maximum column height' => '최대 칼럼 높이 제한하기', + 'Remove maximum column height' => '최대 칼럼 높이 없애기', + 'My notifications' => '내 알림', + 'Custom filters' => '사용자 정의 필터', + // 'Your custom filter have been created successfully.' => '', + // 'Unable to create your custom filter.' => '', + // 'Custom filter removed successfully.' => '', + // 'Unable to remove this custom filter.' => '', + // 'Edit custom filter' => '', + // 'Your custom filter have been updated successfully.' => '', + // 'Unable to update custom filter.' => '', + 'Web' => '웹', + // 'New attachment on task #%d: %s' => '', + 'New comment on task #%d' => '할일 #%d에 새로운 댓글이 달렸습니다', + 'Comment updated on task #%d' => '할일 #%d에 댓글이 업데이트되었습니다', + 'New subtask on task #%d' => '서브 할일 #%d이 업데이트되었습니다', + 'Subtask updated on task #%d' => '서브 할일 #%d가 업데이트되었습니다', + 'New task #%d: %s' => '할일 #%d: %s이 추가되었습니다', + 'Task updated #%d' => '할일 #%d이 업데이트되었습니다', + 'Task #%d closed' => '할일 #%d를 마쳤습니다', + // 'Task #%d opened' => '', + 'Column changed for task #%d' => '할일 #%d의 칼럼이 변경되었습니다', + 'New position for task #%d' => '할일 #%d이 새로운 위치에 등록되었습니다', + // 'Swimlane changed for task #%d' => '', + // 'Assignee changed on task #%d' => '', + // '%d overdue tasks' => '', + // 'Task #%d is overdue' => '', + 'No new notifications.' => '알림이 없습니다', + 'Mark all as read' => '모두 읽음', + 'Mark as read' => '읽음', + // 'Total number of tasks in this column across all swimlanes' => '', + // 'Collapse swimlane' => '', + // 'Expand swimlane' => '', + // 'Add a new filter' => '', + // 'Share with all project members' => '', + // 'Shared' => '', + // 'Owner' => '', + 'Unread notifications' => '읽지않은 알림', + 'Notification methods:' => '알림 방법', + // 'Import tasks from CSV file' => '', + // 'Unable to read your file' => '', + // '%d task(s) have been imported successfully.' => '', + // 'Nothing have been imported!' => '', + // 'Import users from CSV file' => '', + // '%d user(s) have been imported successfully.' => '', + // 'Comma' => '', + // 'Semi-colon' => '', + // 'Tab' => '', + // 'Vertical bar' => '', + // 'Double Quote' => '', + // 'Single Quote' => '', + // '%s attached a file to the task #%d' => '', + // 'There is no column or swimlane activated in your project!' => '', + // 'Append filter (instead of replacement)' => '', + // 'Append/Replace' => '', + // 'Append' => '', + // 'Replace' => '', + 'Import' => '가져오기', + // 'change sorting' => '', + // 'Tasks Importation' => '', + // 'Delimiter' => '', + // 'Enclosure' => '', + // 'CSV File' => '', + // 'Instructions' => '', + // 'Your file must use the predefined CSV format' => '', + // 'Your file must be encoded in UTF-8' => '', + // 'The first row must be the header' => '', + // 'Duplicates are not verified for you' => '', + // 'The due date must use the ISO format: YYYY-MM-DD' => '', + // 'Download CSV template' => '', + 'No external integration registered.' => '설정이 되어있지 않습니다', + // 'Duplicates are not imported' => '', + // 'Usernames must be lowercase and unique' => '', + // 'Passwords will be encrypted if present' => '', + '%s attached a new file to the task %s' => '%s이 새로운 파일을 할일 %s에 추가했습니다', + // 'Link type' => '', + // 'Assign automatically a category based on a link' => '', + // 'BAM - Konvertible Mark' => '', + // 'Assignee Username' => '', + // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + 'Manager' => '매니저', + 'Project Manager' => '프로젝트 매니저', + 'Project Member' => '프로젝트 멤버', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + 'View all groups' => '모든그룹보기', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + 'Add group member' => '멤버추가', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + 'Permissions' => '권한', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + 'Role' => '역할', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + 'Role:' => '역할: ', + 'Project members' => '프로젝트 멤버', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + 'You were mentioned in a comment on the task #%d' => '할일 #%d의 댓글에서 언급되었습니다', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + 'Time-based One-time Password Algorithm' => '시간에 기반한 1회용 패스워드 알고리즘', + 'Two-Factor Provider: ' => '2단 인증: ', + // 'Disable two-factor authentication' => '', + 'Enable two-factor authentication' => '2단 인증 활성화', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + 'Forgot password?' => '비밀번호 찾기', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + 'Last Password Reset' => '비밀번호 초기화', + 'The password has never been reinitialized.' => '비밀번호가 초기화되지 않았습니다', + // 'Creation' => '', + // 'Expiration' => '', + 'Password reset history' => '비밀번호 초기화 기록', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + 'Close all tasks of this column' => '칼럼의 모든 할일 마치기', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + 'My dashboard' => '대시보드', + 'My profile' => '프로필', + // 'Project owner: ' => '', + // 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => '', + // 'Project owner' => '', + // 'Those dates are useful for the project Gantt chart.' => '', + // 'Private projects do not have users and groups management.' => '', + // 'There is no project member.' => '', + // 'Priority' => '', + // 'Task priority' => '', + // 'General' => '', + // 'Dates' => '', + // 'Default priority' => '', + // 'Lowest priority' => '', + // 'Highest priority' => '', + // 'If you put zero to the low and high priority, this feature will be disabled.' => '', + // 'Close a task when there is no activity' => '', + // 'Duration in days' => '', + // 'Send email when there is no activity on a task' => '', + // 'Unable to fetch link information.' => '', + // 'Daily background job for tasks' => '', + // 'Auto' => '', + // 'Related' => '', + // 'Attachment' => '', + // 'Title not found' => '', + // 'Web Link' => '', + // 'External links' => '', + // 'Add external link' => '', + // 'Type' => '', + // 'Dependency' => '', + // 'Add internal link' => '', + // 'Add a new external link' => '', + // 'Edit external link' => '', + // 'External link' => '', + // 'Copy and paste your link here...' => '', + // 'URL' => '', + // 'Internal links' => '', + // 'Assign to me' => '', + // 'Me' => '', + // 'Do not duplicate anything' => '', + // 'Projects management' => '', + // 'Users management' => '', + // 'Groups management' => '', + // 'Create from another project' => '', + // 'open' => '', + // 'closed' => '', + // 'Priority:' => '', + // 'Reference:' => '', + // 'Complexity:' => '', + // 'Swimlane:' => '', + // 'Column:' => '', + // 'Position:' => '', + // 'Creator:' => '', + // 'Time estimated:' => '', + // '%s hours' => '', + // 'Time spent:' => '', + // 'Created:' => '', + // 'Modified:' => '', + // 'Completed:' => '', + // 'Started:' => '', + // 'Moved:' => '', + // 'Task #%d' => '', + // 'Date and time format' => '', + // 'Time format' => '', + // 'Start date: ' => '', + // 'End date: ' => '', + // 'New due date: ' => '', + // 'Start date changed: ' => '', + // 'Disable private projects' => '', + // 'Do you really want to remove this custom filter: "%s"?' => '', + // 'Remove a custom filter' => '', + // 'User activated successfully.' => '', + // 'Unable to enable this user.' => '', + // 'User disabled successfully.' => '', + // 'Unable to disable this user.' => '', + // 'All files have been uploaded successfully.' => '', + // 'View uploaded files' => '', + // 'The maximum allowed file size is %sB.' => '', + // 'Choose files again' => '', + // 'Drag and drop your files here' => '', + // 'choose files' => '', + // 'View profile' => '', + // 'Two Factor' => '', + // 'Disable user' => '', + // 'Do you really want to disable this user: "%s"?' => '', + // 'Enable user' => '', + // 'Do you really want to enable this user: "%s"?' => '', + // 'Download' => '', + // 'Uploaded: %s' => '', + // 'Size: %s' => '', + // 'Uploaded by %s' => '', + // 'Filename' => '', + // 'Size' => '', + // 'Column created successfully.' => '', + // 'Another column with the same name exists in the project' => '', + // 'Default filters' => '', + // 'Your board doesn\'t have any column!' => '', + // 'Change column position' => '', + // 'Switch to the project overview' => '', + // 'User filters' => '', + // 'Category filters' => '', + // 'Upload a file' => '', + // 'View file' => '', + // 'Last activity' => '', + // 'Change subtask position' => '', + // 'This value must be greater than %d' => '', + // 'Another swimlane with the same name exists in the project' => '', + // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', + // 'Actions duplicated successfully.' => '', + // 'Unable to duplicate actions.' => '', + // 'Add a new action' => '', + // 'Import from another project' => '', + // 'There is no action at the moment.' => '', + // 'Import actions from another project' => '', + // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', +); diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php index fc1447e9..4537f38c 100644 --- a/app/Locale/my_MY/translations.php +++ b/app/Locale/my_MY/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Semua projek', 'Add a new column' => 'Tambah kolom baru', 'Title' => 'Judul', - 'Nobody assigned' => 'Tidak ada yang ditugaskan', 'Assigned to %s' => 'Ditugaskan ke %s', 'Remove a column' => 'Hapus kolom', 'Remove a column from a board' => 'Hapus kolom dari papan', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Jumlah tugas', 'User' => 'Pengguna', 'Comments' => 'Komentar', - 'Write your text in Markdown' => 'Menulis teks anda didalam Markdown', 'Leave a comment' => 'Tinggalkan komentar', 'Comment is required' => 'Komentar diperlukan', 'Leave a description' => 'Tinggalkan deskripsi', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Pelacakan waktu :', 'New sub-task' => 'Sub-tugas baru', 'New attachment added "%s"' => 'Lampiran baru ditambahkan « %s »', - 'Comment updated' => 'Komentar diperbaharui', 'New comment posted by %s' => 'Komentar baru ditambahkan oleh « %s »', 'New attachment' => 'Lampirkan baru', 'New comment' => 'Komentar baru', + 'Comment updated' => 'Komentar diperbaharui', 'New subtask' => 'Sub-tugas baru', 'Subtask updated' => 'Sub-tugas diperbaharui', 'Task updated' => 'Tugas diperbaharui', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO selalunya diterima, contoh: « %s » et « %s »', 'New private project' => 'Projek peribadi baharu', 'This project is private' => 'projek ini adalah peribadi', - 'Type here to create a new sub-task' => 'Ketik disini untuk membuat sub-tugas baru', 'Add' => 'Tambah', 'Start date' => 'Tarikh mula', 'Time estimated' => 'Anggaran masa', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'Ekspor ringkasan projek harian untuk « %s »', 'Exports' => 'Ekspor', 'This export contains the number of tasks per column grouped per day.' => 'Ekspor ini berisi jumlah dari tugas per kolom dikelompokan perhari.', - 'Nothing to preview...' => 'Tiada yang dapat diintai...', - 'Preview' => 'Intai', - 'Write' => 'Tulis', 'Active swimlanes' => 'Swimlanes aktif', 'Add a new swimlane' => 'Tambah swimlane baharu', 'Change default swimlane' => 'Tukar piawai swimlane', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Usia tugas dalam bentuk harian', 'Days in this column' => 'Hari dalam kolom ini', '%dd' => '%dj', - 'Add a link' => 'Menambahkan pautan', 'Add a new link' => 'Tambah Pautan baru', 'Do you really want to remove this link: "%s"?' => 'Anda yakin akan menghapus Pautan ini : « %s » ?', 'Do you really want to remove this link with task #%d?' => 'Anda yakin akan menghapus Pautan ini dengan tugas n°%d ?', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'Aliran kegiatan saya', 'My calendar' => 'Kalender saya', 'Search tasks' => 'Cari tugas', - 'Back to the calendar' => 'Kembali ke kalender', - 'Filters' => 'Filter', 'Reset filters' => 'Reset ulang filter', 'My tasks due tomorrow' => 'Tugas saya yang berakhir besok', 'Tasks due today' => 'Tugas yang berakhir hari ini', @@ -850,7 +841,6 @@ return array( 'End date:' => 'Waktu berakhir :', 'There is no start date or end date for this project.' => 'Tidak ada waktu mula atau waktu berakhir pada projek ini', 'Projects Gantt chart' => 'projekkan carta Gantt', - 'Link type' => 'Jenis pautan', 'Change task color when using a specific task link' => 'Rubah warna tugas ketika menggunakan Pautan tugas yang spesifik', 'Task link creation or modification' => 'Pautan tugas pada penciptaan atau penyuntingan', 'Milestone' => 'Batu Tanda', @@ -902,7 +892,6 @@ return array( // 'Shared' => '', // 'Owner' => '', // 'Unread notifications' => '', - // 'My filters' => '', // 'Notification methods:' => '', // 'Import tasks from CSV file' => '', // 'Unable to read your file' => '', @@ -940,6 +929,7 @@ return array( // 'Usernames must be lowercase and unique' => '', // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', + 'Link type' => 'Jenis pautan', // 'Assign automatically a category based on a link' => '', // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', @@ -1049,7 +1039,6 @@ return array( // 'Close a task when there is no activity' => '', // 'Duration in days' => '', // 'Send email when there is no activity on a task' => '', - // 'List of external links' => '', // 'Unable to fetch link information.' => '', // 'Daily background job for tasks' => '', // 'Auto' => '', @@ -1067,9 +1056,7 @@ return array( // 'External link' => '', // 'Copy and paste your link here...' => '', // 'URL' => '', - // 'There is no external link for the moment.' => '', // 'Internal links' => '', - // 'There is no internal link for the moment.' => '', // 'Assign to me' => '', // 'Me' => '', // 'Do not duplicate anything' => '', @@ -1077,7 +1064,6 @@ return array( // 'Users management' => '', // 'Groups management' => '', // 'Create from another project' => '', - // 'There is no subtask at the moment.' => '', // 'open' => '', // 'closed' => '', // 'Priority:' => '', @@ -1096,7 +1082,6 @@ return array( // 'Started:' => '', // 'Moved:' => '', // 'Task #%d' => '', - // 'Sub-tasks' => '', // 'Date and time format' => '', // 'Time format' => '', // 'Start date: ' => '', @@ -1137,7 +1122,6 @@ return array( // 'User filters' => '', // 'Category filters' => '', // 'Upload a file' => '', - // 'There is no attachment at the moment.' => '', // 'View file' => '', // 'Last activity' => '', // 'Change subtask position' => '', @@ -1151,4 +1135,36 @@ return array( // 'There is no action at the moment.' => '', // 'Import actions from another project' => '', // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php index 003ca856..8c6a56f2 100644 --- a/app/Locale/nb_NO/translations.php +++ b/app/Locale/nb_NO/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Alle prosjekter', 'Add a new column' => 'Legg til en ny kolonne', 'Title' => 'Tittel', - 'Nobody assigned' => 'Ikke tildelt', 'Assigned to %s' => 'Tildelt: %s', 'Remove a column' => 'Fjern en kolonne', 'Remove a column from a board' => 'Fjern en kolonne fra et board', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Antall oppgaver', 'User' => 'Bruker', 'Comments' => 'Kommentarer', - 'Write your text in Markdown' => 'Skriv din tekst i markdown', 'Leave a comment' => 'Legg inn en kommentar', 'Comment is required' => 'Kommentar må legges inn', 'Leave a description' => 'Legg inn en beskrivelse...', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Tidsmåling:', 'New sub-task' => 'Ny deloppgave', 'New attachment added "%s"' => 'Nytt vedlegg er lagt tilet "%s"', - 'Comment updated' => 'Kommentar oppdatert', 'New comment posted by %s' => 'Ny kommentar fra %s', 'New attachment' => 'Nytt vedlegg', 'New comment' => 'Ny kommentar', + 'Comment updated' => 'Kommentar oppdatert', 'New subtask' => 'Ny deloppgave', 'Subtask updated' => 'Deloppgave oppdatert', 'Task updated' => 'Oppgave oppdatert', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO format er alltid akseptert, eksempelvis: "%s" og "%s"', 'New private project' => 'Nytt privat prosjekt', 'This project is private' => 'Dette projektet er privat', - 'Type here to create a new sub-task' => 'Skriv her for ø opprette en ny deloppgave', 'Add' => 'Legg til', 'Start date' => 'Start dato', 'Time estimated' => 'Tid estimert', @@ -483,9 +480,6 @@ return array( // 'Daily project summary export for "%s"' => '', 'Exports' => 'Eksporter', // 'This export contains the number of tasks per column grouped per day.' => '', - 'Nothing to preview...' => 'Ingenting å forhåndsvise', - 'Preview' => 'Forhåndsvisning', - 'Write' => 'Skriv', 'Active swimlanes' => 'Aktive svømmebaner', 'Add a new swimlane' => 'Legg til en ny svømmebane', 'Change default swimlane' => 'Endre standard svømmebane', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Dager siden oppgaven ble opprettet', 'Days in this column' => 'Dager siden oppgaven ble lagt i denne kolonnen', // '%dd' => '', - 'Add a link' => 'Legg til en relasjon', 'Add a new link' => 'Legg til en ny relasjon', // 'Do you really want to remove this link: "%s"?' => '', // 'Do you really want to remove this link with task #%d?' => '', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'Aktivitetslogg', 'My calendar' => 'Min kalender', 'Search tasks' => 'Søk oppgave', - 'Back to the calendar' => 'Tilbake til kalender', - 'Filters' => 'Filtere', 'Reset filters' => 'Nullstill filter', 'My tasks due tomorrow' => 'Mine oppgaver med frist i morgen', 'Tasks due today' => 'Oppgaver med frist i dag', @@ -850,7 +841,6 @@ return array( 'End date:' => 'Sluttdato:', // 'There is no start date or end date for this project.' => '', 'Projects Gantt chart' => 'Gantt skjema for prosjekter', - 'Link type' => 'Relasjonstype', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', 'Milestone' => 'Milepæl', @@ -902,7 +892,6 @@ return array( // 'Shared' => '', // 'Owner' => '', // 'Unread notifications' => '', - // 'My filters' => '', // 'Notification methods:' => '', // 'Import tasks from CSV file' => '', // 'Unable to read your file' => '', @@ -940,6 +929,7 @@ return array( // 'Usernames must be lowercase and unique' => '', // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', + 'Link type' => 'Relasjonstype', // 'Assign automatically a category based on a link' => '', // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', @@ -1049,7 +1039,6 @@ return array( // 'Close a task when there is no activity' => '', // 'Duration in days' => '', // 'Send email when there is no activity on a task' => '', - // 'List of external links' => '', // 'Unable to fetch link information.' => '', // 'Daily background job for tasks' => '', // 'Auto' => '', @@ -1067,9 +1056,7 @@ return array( // 'External link' => '', // 'Copy and paste your link here...' => '', // 'URL' => '', - // 'There is no external link for the moment.' => '', // 'Internal links' => '', - // 'There is no internal link for the moment.' => '', // 'Assign to me' => '', // 'Me' => '', // 'Do not duplicate anything' => '', @@ -1077,7 +1064,6 @@ return array( // 'Users management' => '', // 'Groups management' => '', // 'Create from another project' => '', - // 'There is no subtask at the moment.' => '', // 'open' => '', // 'closed' => '', // 'Priority:' => '', @@ -1096,7 +1082,6 @@ return array( // 'Started:' => '', // 'Moved:' => '', // 'Task #%d' => '', - // 'Sub-tasks' => '', // 'Date and time format' => '', // 'Time format' => '', // 'Start date: ' => '', @@ -1137,7 +1122,6 @@ return array( // 'User filters' => '', // 'Category filters' => '', // 'Upload a file' => '', - // 'There is no attachment at the moment.' => '', // 'View file' => '', // 'Last activity' => '', // 'Change subtask position' => '', @@ -1151,4 +1135,36 @@ return array( // 'There is no action at the moment.' => '', // 'Import actions from another project' => '', // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index 4fce1c33..18155816 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Alle projecten', 'Add a new column' => 'Kolom toevoegen', 'Title' => 'Titel', - 'Nobody assigned' => 'Niemand toegewezen', 'Assigned to %s' => 'Toegewezen aan %s', 'Remove a column' => 'Kolom verwijderen', 'Remove a column from a board' => 'Kolom verwijderen van het bord', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Aantal taken', 'User' => 'Gebruiker', 'Comments' => 'Commentaar', - 'Write your text in Markdown' => 'Schrijf uw tekst in Markdown', 'Leave a comment' => 'Schrijf een commentaar', 'Comment is required' => 'Commentaar is verplicht', 'Leave a description' => 'Schrijf een omschrijving', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Tijdschrijven :', 'New sub-task' => 'Nieuwe subtaak', 'New attachment added "%s"' => 'Nieuwe bijlage toegevoegd « %s »', - 'Comment updated' => 'Commentaar aangepast', 'New comment posted by %s' => 'Nieuw commentaar geplaatst door « %s »', 'New attachment' => 'Nieuwe bijlage', 'New comment' => 'Nieuw commentaar', + 'Comment updated' => 'Commentaar aangepast', 'New subtask' => 'Nieuwe subtaak', 'Subtask updated' => 'Subtaak aangepast', 'Task updated' => 'Taak aangepast', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formaat is altijd geaccepteerd, bijvoorbeeld : « %s » et « %s »', 'New private project' => 'Nieuw privé project', 'This project is private' => 'Dit project is privé', - 'Type here to create a new sub-task' => 'Typ hier om een nieuwe subtaak aan te maken', 'Add' => 'Toevoegen', 'Start date' => 'Startdatum', 'Time estimated' => 'Geschatte tijd', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'Dagelijkse project samenvatting voor « %s »', 'Exports' => 'Exports', 'This export contains the number of tasks per column grouped per day.' => 'Dit rapport bevat het aantal taken per kolom gegroupeerd per dag.', - 'Nothing to preview...' => 'Niets om te previewen...', - 'Preview' => 'Preview', - 'Write' => 'Schrijf', 'Active swimlanes' => 'Actieve swinlanes', 'Add a new swimlane' => 'Nieuwe swimlane toevoegen', 'Change default swimlane' => 'Standaard swimlane aapassen', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Leeftijd taak in dagen', 'Days in this column' => 'Dagen in deze kolom', '%dd' => '%dj', - 'Add a link' => 'Link toevoegen', 'Add a new link' => 'Nieuwe link toevoegen', 'Do you really want to remove this link: "%s"?' => 'Weet u zeker dat u deze link wil verwijderen : « %s » ?', 'Do you really want to remove this link with task #%d?' => 'Weet u zeker dat u deze link met taak %d wil verwijderen?', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'Mijn activiteiten', 'My calendar' => 'Mijn kalender', 'Search tasks' => 'Zoek taken', - 'Back to the calendar' => 'Terug naar de kalender', - 'Filters' => 'Filters', 'Reset filters' => 'Reset filters', // 'My tasks due tomorrow' => '', // 'Tasks due today' => '', @@ -850,7 +841,6 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', 'Milestone' => 'Mijlpaal', @@ -902,7 +892,6 @@ return array( // 'Shared' => '', // 'Owner' => '', // 'Unread notifications' => '', - // 'My filters' => '', // 'Notification methods:' => '', // 'Import tasks from CSV file' => '', // 'Unable to read your file' => '', @@ -940,6 +929,7 @@ return array( // 'Usernames must be lowercase and unique' => '', // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', + // 'Link type' => '', // 'Assign automatically a category based on a link' => '', // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', @@ -1049,7 +1039,6 @@ return array( // 'Close a task when there is no activity' => '', // 'Duration in days' => '', // 'Send email when there is no activity on a task' => '', - // 'List of external links' => '', // 'Unable to fetch link information.' => '', // 'Daily background job for tasks' => '', // 'Auto' => '', @@ -1067,9 +1056,7 @@ return array( // 'External link' => '', // 'Copy and paste your link here...' => '', // 'URL' => '', - // 'There is no external link for the moment.' => '', // 'Internal links' => '', - // 'There is no internal link for the moment.' => '', // 'Assign to me' => '', // 'Me' => '', // 'Do not duplicate anything' => '', @@ -1077,7 +1064,6 @@ return array( // 'Users management' => '', // 'Groups management' => '', // 'Create from another project' => '', - // 'There is no subtask at the moment.' => '', // 'open' => '', // 'closed' => '', // 'Priority:' => '', @@ -1096,7 +1082,6 @@ return array( // 'Started:' => '', // 'Moved:' => '', // 'Task #%d' => '', - // 'Sub-tasks' => '', // 'Date and time format' => '', // 'Time format' => '', // 'Start date: ' => '', @@ -1137,7 +1122,6 @@ return array( // 'User filters' => '', // 'Category filters' => '', // 'Upload a file' => '', - // 'There is no attachment at the moment.' => '', // 'View file' => '', // 'Last activity' => '', // 'Change subtask position' => '', @@ -1151,4 +1135,36 @@ return array( // 'There is no action at the moment.' => '', // 'Import actions from another project' => '', // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index 87201bd0..d9427d80 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Wszystkie projekty', 'Add a new column' => 'Dodaj nową kolumnę', 'Title' => 'Nazwa', - 'Nobody assigned' => 'Nikt nie przypisany', 'Assigned to %s' => 'Przypisane do %s', 'Remove a column' => 'Usuń kolumnę', 'Remove a column from a board' => 'Usuń kolumnę z tablicy', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Liczba zadań', 'User' => 'Użytkownik', 'Comments' => 'Komentarze', - 'Write your text in Markdown' => 'Zobacz jak formatować tekst z użyciem Markdown', 'Leave a comment' => 'Wstaw komentarz', 'Comment is required' => 'Komentarz jest wymagany', 'Leave a description' => 'Dodaj opis', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Śledzenie czasu: ', 'New sub-task' => 'Nowe Pod-zadanie', 'New attachment added "%s"' => 'Nowy załącznik dodany "%s"', - 'Comment updated' => 'Komentarz zaktualizowany', 'New comment posted by %s' => 'Nowy komentarz dodany przez %s', 'New attachment' => 'Nowy załącznik', 'New comment' => 'Nowy Komentarz', + 'Comment updated' => 'Komentarz zaktualizowany', 'New subtask' => 'Nowe pod-zadanie', 'Subtask updated' => 'Zaktualizowane pod-zadanie', 'Task updated' => 'Zaktualizowane zadanie', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO jest zawsze akceptowany, przykłady: "%s", "%s"', 'New private project' => 'Nowy projekt prywatny', 'This project is private' => 'Ten projekt jest prywatny', - 'Type here to create a new sub-task' => 'Nazwa podzadania', 'Add' => 'Dodaj', 'Start date' => 'Data rozpoczęcia', 'Time estimated' => 'Szacowany czas', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'Wygeneruj dzienny raport dla projektu: "%s"', 'Exports' => 'Eksporty', 'This export contains the number of tasks per column grouped per day.' => 'Ten eksport zawiera ilość zadań zgrupowanych w kolumnach na dzień', - 'Nothing to preview...' => 'Nic do podejrzenia...', - 'Preview' => 'Podgląd', - 'Write' => 'Edycja', 'Active swimlanes' => 'Aktywne procesy', 'Add a new swimlane' => 'Dodaj proces', 'Change default swimlane' => 'Zmień domyślny proces', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Wiek zadania w dniach', 'Days in this column' => 'Dni w tej kolumnie', // '%dd' => '', - 'Add a link' => 'Dodaj link', 'Add a new link' => 'Dodaj nowy link', 'Do you really want to remove this link: "%s"?' => 'Czy na pewno chcesz usunąć ten link: "%s"?', 'Do you really want to remove this link with task #%d?' => 'Czy na pewno chcesz usunąć ten link razem z zadaniem nr %d?', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'Moja aktywność', 'My calendar' => 'Mój kalendarz', 'Search tasks' => 'Szukaj zadań', - 'Back to the calendar' => 'Wróć do kalendarza', - 'Filters' => 'Filtry', 'Reset filters' => 'Resetuj zastosowane filtry', 'My tasks due tomorrow' => 'Moje zadania do jutra', 'Tasks due today' => 'Zadania do dzisiaj', @@ -850,7 +841,6 @@ return array( 'End date:' => 'Data zakończenia:', 'There is no start date or end date for this project.' => 'Nie zdefiniowano czasu trwania projektu', 'Projects Gantt chart' => 'Wykres Gantta dla projektów', - 'Link type' => 'Rodzaj link\'u', 'Change task color when using a specific task link' => 'Zmień kolor zadania używając specjalnego adresu URL', 'Task link creation or modification' => 'Adres URL do utworzenia zadania lub modyfikacji', 'Milestone' => 'Kamień milowy', @@ -902,7 +892,6 @@ return array( // 'Shared' => '', 'Owner' => 'Właściciel', 'Unread notifications' => 'Nieprzeczytane powiadomienia', - 'My filters' => 'Moje filtry', 'Notification methods:' => 'Metody powiadomień:', // 'Import tasks from CSV file' => '', // 'Unable to read your file' => '', @@ -940,6 +929,7 @@ return array( // 'Usernames must be lowercase and unique' => '', // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', + 'Link type' => 'Rodzaj link\'u', // 'Assign automatically a category based on a link' => '', // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', @@ -1049,7 +1039,6 @@ return array( // 'Close a task when there is no activity' => '', // 'Duration in days' => '', // 'Send email when there is no activity on a task' => '', - // 'List of external links' => '', // 'Unable to fetch link information.' => '', // 'Daily background job for tasks' => '', 'Auto' => 'Automatyczny', @@ -1067,9 +1056,7 @@ return array( 'External link' => 'Link zewnętrzny', 'Copy and paste your link here...' => 'Skopiuj i wklej link tutaj ...', // 'URL' => '', - 'There is no external link for the moment.' => 'Brak linków zewnętrznych.', 'Internal links' => 'Linki do innych zadań', - 'There is no internal link for the moment.' => 'Brak powiązań do innych zadań.', // 'Assign to me' => '', 'Me' => 'JA', 'Do not duplicate anything' => 'Nie kopiuj żadnego projektu', @@ -1077,7 +1064,6 @@ return array( 'Users management' => 'Zarządzanie użytkownikami', 'Groups management' => 'Zarządzanie grupami', 'Create from another project' => 'Utwórz na podstawie innego projektu', - 'There is no subtask at the moment.' => 'Brak podzadań.', 'open' => 'otwarty', 'closed' => 'zamknięty', 'Priority:' => 'Priorytet:', @@ -1096,7 +1082,6 @@ return array( 'Started:' => 'Rozpoczęte:', 'Moved:' => 'Przesunięcie:', 'Task #%d' => 'Zadanie #%d', - 'Sub-tasks' => 'Podzadania', // 'Date and time format' => '', // 'Time format' => '', // 'Start date: ' => '', @@ -1137,7 +1122,6 @@ return array( // 'User filters' => '', // 'Category filters' => '', 'Upload a file' => 'Prześlij plik', - 'There is no attachment at the moment.' => 'Brak załączników.', 'View file' => 'Wyświetl plik', 'Last activity' => 'Ostatnia aktywność', // 'Change subtask position' => '', @@ -1151,4 +1135,36 @@ return array( // 'There is no action at the moment.' => '', // 'Import actions from another project' => '', // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index 2c9412b6..e0cdb17d 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Todos os projetos', 'Add a new column' => 'Adicionar uma nova coluna', 'Title' => 'Título', - '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 board', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Número de tarefas', 'User' => 'Usuário', 'Comments' => 'Comentários', - 'Write your text in Markdown' => 'Escreva seu texto em Markdown', 'Leave a comment' => 'Deixe um comentário', 'Comment is required' => 'Comentário é obrigatório', 'Leave a description' => 'Deixe uma descrição', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Controle de tempo:', 'New sub-task' => 'Nova subtarefa', 'New attachment added "%s"' => 'Novo anexo adicionado "%s"', - 'Comment updated' => 'Comentário atualizado', 'New comment posted by %s' => 'Novo comentário postado por %s', 'New attachment' => 'Novo anexo', 'New comment' => 'Novo comentário', + 'Comment updated' => 'Comentário atualizado', 'New subtask' => 'Nova subtarefa', 'Subtask updated' => 'Subtarefa alterada', 'Task updated' => 'Tarefa alterada', @@ -432,7 +430,6 @@ return array( '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 subtarefa', 'Add' => 'Adicionar', 'Start date' => 'Data de início', 'Time estimated' => 'Tempo estimado', @@ -483,9 +480,6 @@ return array( '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.' => 'Esta exportação contém o número de tarefas por coluna agrupada por dia.', - 'Nothing to preview...' => 'Nada para pré-visualizar...', - 'Preview' => 'Pré-visualizar', - 'Write' => 'Escrever', 'Active swimlanes' => 'Ativar swimlanes', 'Add a new swimlane' => 'Adicionar swimlane', 'Change default swimlane' => 'Alterar swimlane padrão', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Idade da tarefa em dias', 'Days in this column' => 'Dias nesta coluna', '%dd' => '%dd', - 'Add a link' => 'Adicionar uma associação', 'Add a new link' => 'Adicionar uma nova associação', 'Do you really want to remove this link: "%s"?' => 'Você realmente deseja remover esta associação: "%s"?', 'Do you really want to remove this link with task #%d?' => 'Você realmente deseja remover esta associação com a tarefa #%d?', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'Meu feed de atividades', 'My calendar' => 'Minha agenda', 'Search tasks' => 'Pesquisar tarefas', - 'Back to the calendar' => 'Voltar ao calendário', - 'Filters' => 'Filtros', 'Reset filters' => 'Redefinir os filtros', 'My tasks due tomorrow' => 'Minhas tarefas que expiram amanhã', 'Tasks due today' => 'Tarefas que expiram hoje', @@ -850,7 +841,6 @@ return array( 'End date:' => 'Data de término:', 'There is no start date or end date for this project.' => 'Não há data de início ou data de término para este projeto.', 'Projects Gantt chart' => 'Gráfico de Gantt dos projetos', - 'Link type' => 'Tipo de link', 'Change task color when using a specific task link' => 'Mudar a cor da tarefa quando um link específico é utilizado', 'Task link creation or modification' => 'Criação ou modificação de um link em uma tarefa', 'Milestone' => 'Milestone', @@ -902,7 +892,6 @@ return array( 'Shared' => 'Compartilhado', 'Owner' => 'Líder', 'Unread notifications' => 'Notificações não lidas', - 'My filters' => 'Meus filtros', 'Notification methods:' => 'Métodos de notificação:', 'Import tasks from CSV file' => 'Importar tarefas a partir de arquivo CSV', 'Unable to read your file' => 'Não foi possível ler seu arquivo', @@ -940,6 +929,7 @@ return array( 'Usernames must be lowercase and unique' => 'Nomes de usuário devem ser únicos e em letras minúsculas', 'Passwords will be encrypted if present' => 'Senhas serão encriptadas, se presentes', '%s attached a new file to the task %s' => '%s anexou um novo arquivo a tarefa %s', + 'Link type' => 'Tipo de link', 'Assign automatically a category based on a link' => 'Atribuir automaticamente uma categoria baseada num link', 'BAM - Konvertible Mark' => 'BAM - Mark conversível', 'Assignee Username' => 'Usuário designado', @@ -1049,7 +1039,6 @@ return array( 'Close a task when there is no activity' => 'Fechar uma tarefa sem atividade', 'Duration in days' => 'Duração em dias', 'Send email when there is no activity on a task' => 'Enviar um e-mail quando não há nenhuma atividade em uma tarefa', - 'List of external links' => 'Lista dos links externos', 'Unable to fetch link information.' => 'Não foi possível obter informações sobre o link.', 'Daily background job for tasks' => 'Tarefa agendada diariamente para as tarefas', 'Auto' => 'Auto', @@ -1061,94 +1050,121 @@ return array( 'Add external link' => 'Adicionar um link externo', 'Type' => 'Tipo', 'Dependency' => 'Dependência', - // 'Add internal link' => '', - // 'Add a new external link' => '', - // 'Edit external link' => '', - // 'External link' => '', - // 'Copy and paste your link here...' => '', - // 'URL' => '', - // 'There is no external link for the moment.' => '', - // 'Internal links' => '', - // 'There is no internal link for the moment.' => '', - // 'Assign to me' => '', - // 'Me' => '', - // 'Do not duplicate anything' => '', - // 'Projects management' => '', - // 'Users management' => '', - // 'Groups management' => '', - // 'Create from another project' => '', - // 'There is no subtask at the moment.' => '', - // 'open' => '', - // 'closed' => '', - // 'Priority:' => '', - // 'Reference:' => '', - // 'Complexity:' => '', - // 'Swimlane:' => '', - // 'Column:' => '', - // 'Position:' => '', - // 'Creator:' => '', - // 'Time estimated:' => '', - // '%s hours' => '', - // 'Time spent:' => '', - // 'Created:' => '', - // 'Modified:' => '', - // 'Completed:' => '', - // 'Started:' => '', - // 'Moved:' => '', - // 'Task #%d' => '', - // 'Sub-tasks' => '', - // 'Date and time format' => '', - // 'Time format' => '', - // 'Start date: ' => '', - // 'End date: ' => '', - // 'New due date: ' => '', - // 'Start date changed: ' => '', - // 'Disable private projects' => '', - // 'Do you really want to remove this custom filter: "%s"?' => '', - // 'Remove a custom filter' => '', - // 'User activated successfully.' => '', - // 'Unable to enable this user.' => '', - // 'User disabled successfully.' => '', - // 'Unable to disable this user.' => '', - // 'All files have been uploaded successfully.' => '', - // 'View uploaded files' => '', - // 'The maximum allowed file size is %sB.' => '', - // 'Choose files again' => '', - // 'Drag and drop your files here' => '', - // 'choose files' => '', - // 'View profile' => '', - // 'Two Factor' => '', - // 'Disable user' => '', - // 'Do you really want to disable this user: "%s"?' => '', - // 'Enable user' => '', - // 'Do you really want to enable this user: "%s"?' => '', - // 'Download' => '', - // 'Uploaded: %s' => '', - // 'Size: %s' => '', - // 'Uploaded by %s' => '', - // 'Filename' => '', - // 'Size' => '', - // 'Column created successfully.' => '', - // 'Another column with the same name exists in the project' => '', - // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', - // 'Change column position' => '', - // 'Switch to the project overview' => '', - // 'User filters' => '', - // 'Category filters' => '', - // 'Upload a file' => '', - // 'There is no attachment at the moment.' => '', - // 'View file' => '', - // 'Last activity' => '', - // 'Change subtask position' => '', - // 'This value must be greater than %d' => '', - // 'Another swimlane with the same name exists in the project' => '', - // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', - // 'Actions duplicated successfully.' => '', - // 'Unable to duplicate actions.' => '', - // 'Add a new action' => '', - // 'Import from another project' => '', - // 'There is no action at the moment.' => '', - // 'Import actions from another project' => '', - // 'There is no available project.' => '', + 'Add internal link' => 'Adicionar um link interno', + 'Add a new external link' => 'Adicionar um novo link externo', + 'Edit external link' => 'Editar um link externo', + 'External link' => 'Link externo', + 'Copy and paste your link here...' => 'Copie e cole o link aqui...', + 'URL' => 'URL', + 'Internal links' => 'Link interno', + 'Assign to me' => 'Atribuir-me', + 'Me' => 'Eu', + 'Do not duplicate anything' => 'Não duplique nada', + 'Projects management' => 'Gestão de projetos', + 'Users management' => 'Gestão dos usuários', + 'Groups management' => 'Gestão dos grupos', + 'Create from another project' => 'Criar a partir de outro projeto', + 'open' => 'aberto', + 'closed' => 'fechado', + 'Priority:' => 'Prioridade:', + 'Reference:' => 'Referência:', + 'Complexity:' => 'Complexidade:', + 'Swimlane:' => 'Swimlane:', + 'Column:' => 'Coluna:', + 'Position:' => 'Posição:', + 'Creator:' => 'Criador:', + 'Time estimated:' => 'Tempo estimado:', + '%s hours' => '%s horas', + 'Time spent:' => 'Tempo gasto:', + 'Created:' => 'Criado:', + 'Modified:' => 'Modificado:', + 'Completed:' => 'Completado:', + 'Started:' => 'Começado:', + 'Moved:' => 'Movido:', + 'Task #%d' => 'Tarefa #%d', + 'Date and time format' => 'Formato da hora e da data', + 'Time format' => 'Formato da hora', + 'Start date: ' => 'Data de início: ', + 'End date: ' => 'Data final: ', + 'New due date: ' => 'Nova data limite: ', + 'Start date changed: ' => 'Data de início alterada: ', + 'Disable private projects' => 'Desativar os projetos privados', + 'Do you really want to remove this custom filter: "%s"?' => 'Você realmente quer remover este filtro personalizado: "%s"?', + 'Remove a custom filter' => 'Remover um filtro personalizado', + 'User activated successfully.' => 'Usuário ativado com sucesso.', + 'Unable to enable this user.' => 'Impossível de ativar esse usuário.', + 'User disabled successfully.' => 'Usuário desactivado com sucesso.', + 'Unable to disable this user.' => 'Impossível de desativar esse usuário.', + 'All files have been uploaded successfully.' => 'Todos os arquivos foram enviados com sucesso.', + 'View uploaded files' => 'Ver os arquivos enviados', + 'The maximum allowed file size is %sB.' => 'O tamanho máximo dos arquivos é %sB.', + 'Choose files again' => 'Selecionar novamente arquivos', + 'Drag and drop your files here' => 'Arraste e solte os arquivos aqui', + 'choose files' => 'selecione os arquivos', + 'View profile' => 'Ver o perfil', + 'Two Factor' => 'Dois fatores', + 'Disable user' => 'Desativar o usuário', + 'Do you really want to disable this user: "%s"?' => 'Você realmente quer desativar este usuário: "%s"?', + 'Enable user' => 'Ativar um usuário', + 'Do you really want to enable this user: "%s"?' => 'Você realmente quer ativar este usuário: "%s"?', + 'Download' => 'Baixar', + 'Uploaded: %s' => 'Enviado: %s', + 'Size: %s' => 'Tamanho: %s', + 'Uploaded by %s' => 'Enviado por %s', + 'Filename' => 'Nome do arquivo', + 'Size' => 'Tamanho', + 'Column created successfully.' => 'A coluna criada com sucesso.', + 'Another column with the same name exists in the project' => 'Uma outra coluna com o mesmo nome já existe no projeto', + 'Default filters' => 'Filtros padrão', + 'Your board doesn\'t have any column!' => 'O seu painel não tem nenhuma coluna', + 'Change column position' => 'Alterar a posição da coluna', + 'Switch to the project overview' => 'Mudar para a vista geral do projeto', + 'User filters' => 'Filtros dos usuários', + 'Category filters' => 'Filtros das categorias', + 'Upload a file' => 'Enviar um arquivo', + 'View file' => 'Ver um arquivo', + 'Last activity' => 'Últimas atividades', + 'Change subtask position' => 'Alterar a posição da sub-tarefa', + 'This value must be greater than %d' => 'Este valor deve ser maior que %d', + 'Another swimlane with the same name exists in the project' => 'Outra Swimlane existe com o mesmo nome no projeto', + 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Exemplo: http://exemple.kanboard.net/ (usado para gerar URLs absolutos)', + 'Actions duplicated successfully.' => 'Ações duplicadas com sucesso.', + 'Unable to duplicate actions.' => 'Não foi possível duplicar as ações.', + 'Add a new action' => 'Adicionar uma nova ação', + 'Import from another project' => 'Importar a partir de outro projeto', + 'There is no action at the moment.' => 'Não há nenhuma ação actualmente.', + 'Import actions from another project' => 'Importar ações a partir de outro projeto', + 'There is no available project.' => 'Não há projetos disponíveis.', + 'Local File' => 'Arquivo local', + 'Configuration' => 'Configuração', + 'PHP version:' => 'Versão do PHP:', + 'PHP SAPI:' => 'PHP SAPI:', + 'OS version:' => 'Versão do sistema operacional:', + 'Database version:' => 'Versão do banco de dados:', + 'Browser:' => 'Browser:', + 'Task view' => 'Vista detalhada de uma tarefa', + 'Edit task' => 'Editar a tarefa', + 'Edit description' => 'Editar a descrição', + 'New internal link' => 'Novo link interno', + 'Display list of keyboard shortcuts' => 'Ver a lista dos atalhos de teclado', + 'Menu' => 'Menu', + 'Set start date' => 'Definir a data de início', + 'Avatar' => 'Avatar', + 'Upload my avatar image' => 'Enviar a minha imagem de avatar', + 'Remove my image' => 'Remover a minha imagem', + 'The OAuth2 state parameter is invalid' => 'O parâmetro "state" de OAuth2 não é válido', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php index 31a56882..aa51534b 100644 --- a/app/Locale/pt_PT/translations.php +++ b/app/Locale/pt_PT/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Todos os projectos', 'Add a new column' => 'Adicionar uma nova coluna', 'Title' => 'Título', - 'Nobody assigned' => 'Ninguém assignado', 'Assigned to %s' => 'Designado para %s', 'Remove a column' => 'Remover uma coluna', 'Remove a column from a board' => 'Remover uma coluna do quadro', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Número de tarefas', 'User' => 'Utilizador', 'Comments' => 'Comentários', - 'Write your text in Markdown' => 'Escreva o seu texto em Markdown', 'Leave a comment' => 'Deixe um comentário', 'Comment is required' => 'Comentário é obrigatório', 'Leave a description' => 'Deixe uma descrição', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Controle de tempo:', 'New sub-task' => 'Nova subtarefa', 'New attachment added "%s"' => 'Novo anexo adicionado "%s"', - 'Comment updated' => 'Comentário actualizado', 'New comment posted by %s' => 'Novo comentário por %s', 'New attachment' => 'Novo anexo', 'New comment' => 'Novo comentário', + 'Comment updated' => 'Comentário actualizado', 'New subtask' => 'Nova subtarefa', 'Subtask updated' => 'Subtarefa alterada', 'Task updated' => 'Tarefa alterada', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'O formato ISO é sempre aceite, exemplo: "%s" e "%s"', 'New private project' => 'Novo projecto privado', 'This project is private' => 'Este projecto é privado', - 'Type here to create a new sub-task' => 'Escreva aqui para criar uma nova subtarefa', 'Add' => 'Adicionar', 'Start date' => 'Data de início', 'Time estimated' => 'Tempo estimado', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'Exportação diária do resumo do projecto para "%s"', 'Exports' => 'Exportar', 'This export contains the number of tasks per column grouped per day.' => 'Esta exportação contém o número de tarefas por coluna agrupada por dia.', - 'Nothing to preview...' => 'Nada para pré-visualizar...', - 'Preview' => 'Pré-visualizar', - 'Write' => 'Escrever', 'Active swimlanes' => 'Activar swimlanes', 'Add a new swimlane' => 'Adicionar novo swimlane', 'Change default swimlane' => 'Alterar swimlane padrão', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Idade da tarefa em dias', 'Days in this column' => 'Dias nesta coluna', // '%dd' => '', - 'Add a link' => 'Adicionar uma associação', 'Add a new link' => 'Adicionar uma nova associação', 'Do you really want to remove this link: "%s"?' => 'Tem a certeza que quer remover esta associação: "%s"?', 'Do you really want to remove this link with task #%d?' => 'Tem a certeza que quer remover esta associação com a tarefa n°%d?', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'O meu feed de actividade', 'My calendar' => 'A minha agenda', 'Search tasks' => 'Pesquisar tarefas', - 'Back to the calendar' => 'Voltar ao calendário', - 'Filters' => 'Filtros', 'Reset filters' => 'Redefinir os filtros', 'My tasks due tomorrow' => 'A minhas tarefas que expiram amanhã', 'Tasks due today' => 'Tarefas que expiram hoje', @@ -850,7 +841,6 @@ return array( 'End date:' => 'Data de fim:', 'There is no start date or end date for this project.' => 'Não existe data de inicio ou fim para este projecto.', 'Projects Gantt chart' => 'Gráfico de Gantt dos projectos', - 'Link type' => 'Tipo de ligação', 'Change task color when using a specific task link' => 'Alterar cor da tarefa quando se usar um tipo especifico de ligação de tarefa', 'Task link creation or modification' => 'Criação ou modificação de ligação de tarefa', 'Milestone' => 'Objectivo', @@ -902,7 +892,6 @@ return array( 'Shared' => 'Partilhado', 'Owner' => 'Dono', 'Unread notifications' => 'Notificações por ler', - 'My filters' => 'Os meus filtros', 'Notification methods:' => 'Métodos de notificação:', 'Import tasks from CSV file' => 'Importar tarefas de um ficheiro CSV', 'Unable to read your file' => 'Não foi possivel ler o ficheiro', @@ -940,6 +929,7 @@ return array( 'Usernames must be lowercase and unique' => 'Utilizadores tem de estar em letra pequena e ser unicos', 'Passwords will be encrypted if present' => 'Senhas serão encriptadas se presentes', '%s attached a new file to the task %s' => '%s anexou um novo ficheiro à tarefa %s', + 'Link type' => 'Tipo de ligação', 'Assign automatically a category based on a link' => 'Assignar automáticamente a categoria baseada num link', 'BAM - Konvertible Mark' => 'BAM - Marca Conversível', 'Assignee Username' => 'Utilizador do Assignado', @@ -1049,7 +1039,6 @@ return array( 'Close a task when there is no activity' => 'Fechar tarefa quando não há actividade', 'Duration in days' => 'Duração em dias', 'Send email when there is no activity on a task' => 'Enviar email quando não há actividade numa tarefa', - 'List of external links' => 'Lista de ligações externas', 'Unable to fetch link information.' => 'Impossivel obter informação da ligação.', 'Daily background job for tasks' => 'Trabalho diário em segundo plano para tarefas', 'Auto' => 'Auto', @@ -1067,9 +1056,7 @@ return array( 'External link' => 'Ligação externa', 'Copy and paste your link here...' => 'Copie e cole a sua ligação aqui...', 'URL' => 'URL', - 'There is no external link for the moment.' => 'De momento não existe nenhuma ligação externa.', 'Internal links' => 'Ligações internas', - 'There is no internal link for the moment.' => 'De momento não existe nenhuma ligação interna.', 'Assign to me' => 'Assignar a mim', 'Me' => 'Eu', 'Do not duplicate anything' => 'Não duplicar nada', @@ -1077,7 +1064,6 @@ return array( 'Users management' => 'Gestão de utilizadores', 'Groups management' => 'Gestão de grupos', 'Create from another project' => 'Criar apartir de outro projecto', - 'There is no subtask at the moment.' => 'De momento não existe sub-tarefa.', 'open' => 'aberto', 'closed' => 'fechado', 'Priority:' => 'Prioridade:', @@ -1096,7 +1082,6 @@ return array( 'Started:' => 'Iniciado:', 'Moved:' => 'Movido:', 'Task #%d' => 'Tarefa #%d', - 'Sub-tasks' => 'Sub-tarefa', 'Date and time format' => 'Formato tempo e data', 'Time format' => 'Formato tempo', 'Start date: ' => 'Data inicio: ', @@ -1137,7 +1122,6 @@ return array( 'User filters' => 'Filtros de utilizador', 'Category filters' => 'Filtros de categoria', 'Upload a file' => 'Enviar um ficheiro', - 'There is no attachment at the moment.' => 'De momento não existe anexo.', 'View file' => 'Ver ficheiro', 'Last activity' => 'Ultima actividade', 'Change subtask position' => 'Mudar posição da sub-tarefa', @@ -1151,4 +1135,36 @@ return array( 'There is no action at the moment.' => 'De momento não existe acção.', 'Import actions from another project' => 'Importar acções de outro projecto', 'There is no available project.' => 'Não existe projecto disponivel.', + 'Local File' => 'Ficheiro Local', + 'Configuration' => 'Configuração', + 'PHP version:' => 'Versão PHP:', + 'PHP SAPI:' => 'SAPI PHP:', + 'OS version:' => 'Versão SO:', + 'Database version:' => 'Versão base de dados:', + 'Browser:' => 'Navegador:', + 'Task view' => 'Vista de Tarefas', + 'Edit task' => 'Editar tarefa', + 'Edit description' => 'Editar descrição', + 'New internal link' => 'Nova ligação interna', + 'Display list of keyboard shortcuts' => 'Mostrar lista de atalhos do teclado', + 'Menu' => 'Menu', + 'Set start date' => 'Definir data de inicio', + 'Avatar' => 'Avatar', + 'Upload my avatar image' => 'Enviar a minha imagem de avatar', + 'Remove my image' => 'Remover a minha imagem', + 'The OAuth2 state parameter is invalid' => 'O parametro de estado do OAuth2 é inválido', + 'User not found.' => 'Utilizador não encontrado.', + 'Search in activity stream' => 'Procurar no fluxo de atividade', + 'My activities' => 'Minhas actividades', + 'Activity until yesterday' => 'Actividade até ontem', + 'Activity until today' => 'Actividade até hoje', + 'Search by creator: ' => 'Procurar por criador: ', + 'Search by creation date: ' => 'Procurar por data de criação: ', + 'Search by task status: ' => 'Procurar por estado da tarefa: ', + 'Search by task title: ' => 'Procurar por titulo da tarefa: ', + 'Activity stream search' => 'Procurar fluxo de actividade', + 'Projects where "%s" is manager' => 'Projectos onde "%s" é gestor', + 'Projects where "%s" is member' => 'Projectos onde "%s" é membro', + 'Open tasks assigned to "%s"' => 'Tarefas abertas assignadas a "%s"', + 'Closed tasks assigned to "%s"' => 'Tarefas fechadas assignadas a "%s"', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index 74f1deb8..bf2bc559 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Все проекты', 'Add a new column' => 'Добавить новую колонку', 'Title' => 'Название', - 'Nobody assigned' => 'Никто не назначен', 'Assigned to %s' => 'Назначено %s', 'Remove a column' => 'Удалить колонку', 'Remove a column from a board' => 'Удалить колонку с доски', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Количество задач', 'User' => 'Пользователь', 'Comments' => 'Комментарии', - 'Write your text in Markdown' => 'Справка по синтаксису Markdown', 'Leave a comment' => 'Оставить комментарий', 'Comment is required' => 'Нужен комментарий', 'Leave a description' => 'Напишите описание', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Отслеживание времени:', 'New sub-task' => 'Новая подзадача', 'New attachment added "%s"' => 'Добавлено вложение « %s »', - 'Comment updated' => 'Комментарий обновлен', 'New comment posted by %s' => 'Новый комментарий написан « %s »', 'New attachment' => 'Новое вложение', 'New comment' => 'Новый комментарий', + 'Comment updated' => 'Комментарий обновлен', 'New subtask' => 'Новая подзадача', 'Subtask updated' => 'Подзадача обновлена', 'Task updated' => 'Задача обновлена', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'Время должно быть в ISO-формате, например: "%s" или "%s"', 'New private project' => 'Новый проект с ограниченным доступом', 'This project is private' => 'Это проект с ограниченным доступом', - 'Type here to create a new sub-task' => 'Печатайте сюда чтобы создать подзадачу', 'Add' => 'Добавить', 'Start date' => 'Дата начала', 'Time estimated' => 'Запланировано', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'Экспорт ежедневного резюме проекта "%s"', 'Exports' => 'Экспорт', 'This export contains the number of tasks per column grouped per day.' => 'Этот экспорт содержит ряд задач в колонках, сгруппированные по дням.', - 'Nothing to preview...' => 'Нет данных для предпросмотра...', - 'Preview' => 'Предпросмотр', - 'Write' => 'Написание', 'Active swimlanes' => 'Активные дорожки', 'Add a new swimlane' => 'Добавить новую дорожку', 'Change default swimlane' => 'Сменить стандартную дорожку', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Возраст задачи в днях', 'Days in this column' => 'Дней в этой колонке', '%dd' => '%dd', - 'Add a link' => 'Добавить ссылку на другие задачи', 'Add a new link' => 'Добавление новой ссылки', 'Do you really want to remove this link: "%s"?' => 'Вы уверены что хотите удалить ссылку: "%s"?', 'Do you really want to remove this link with task #%d?' => 'Вы уверены что хотите удалить ссылку вместе с задачей #%d?', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'Лента моей активности', 'My calendar' => 'Мой календарь', 'Search tasks' => 'Поиск задачи', - 'Back to the calendar' => 'Вернуться в календарь', - 'Filters' => 'Фильтры', 'Reset filters' => 'Сбросить фильтры', 'My tasks due tomorrow' => 'Мои задачи на завтра', 'Tasks due today' => 'Задачи, завершающиеся сегодня', @@ -850,7 +841,6 @@ return array( 'End date:' => 'Дата завершения:', 'There is no start date or end date for this project.' => 'В проекте не указаны дата начала или завершения.', 'Projects Gantt chart' => 'Диаграмма Ганта проектов', - 'Link type' => 'Тип ссылки', 'Change task color when using a specific task link' => 'Изменение цвета задач при использовании ссылки на определенные задачи', 'Task link creation or modification' => 'Ссылка на создание или модификацию задачи', 'Milestone' => 'Веха', @@ -902,7 +892,6 @@ return array( 'Shared' => 'Общие', 'Owner' => 'Владелец', 'Unread notifications' => 'Непрочитанные уведомления', - 'My filters' => 'Мои фильтры', 'Notification methods:' => 'Способы уведомления:', 'Import tasks from CSV file' => 'Импорт задач из CSV-файла', 'Unable to read your file' => 'Невозможно прочитать файл', @@ -940,6 +929,7 @@ return array( 'Usernames must be lowercase and unique' => 'Логины пользователей должны быть строчными и уникальными', 'Passwords will be encrypted if present' => 'Пароли будут зашифрованы (если указаны)', '%s attached a new file to the task %s' => '%s добавил новый файл к задаче %s', + 'Link type' => 'Тип ссылки', 'Assign automatically a category based on a link' => 'Автоматически назначать категории на основе ссылки', 'BAM - Konvertible Mark' => 'BAM - Конвертируемая марка', 'Assignee Username' => 'Логин назначенного', @@ -1049,7 +1039,6 @@ return array( 'Close a task when there is no activity' => 'Закрывать задачу, когда нет активности', 'Duration in days' => 'Длительность в днях', 'Send email when there is no activity on a task' => 'Отправлять email, когда активность по задаче отсутствует', - 'List of external links' => 'Список внешних ссылок', 'Unable to fetch link information.' => 'Не удалось получить информацию о ссылке', 'Daily background job for tasks' => 'Ежедневные фоновые работы для задач', 'Auto' => 'Авто', @@ -1067,9 +1056,7 @@ return array( 'External link' => 'Внешняя ссылка', 'Copy and paste your link here...' => 'Скопируйте и вставьте вашу ссылку здесь', 'URL' => 'URL', - 'There is no external link for the moment.' => 'На данный момент внешние ссылки отсутствуют', 'Internal links' => 'Внутренние ссылки', - 'There is no internal link for the moment.' => 'На данные момент внутреннии ссылки отсутствуют', 'Assign to me' => 'Связать со мной', 'Me' => 'Мне', 'Do not duplicate anything' => 'Не дублировать ничего', @@ -1077,7 +1064,6 @@ return array( 'Users management' => 'Управление пользователями', 'Groups management' => 'Управление группами', 'Create from another project' => 'Создать из другого проекта', - 'There is no subtask at the moment.' => 'На данный момент подзадачи отсутствуют', 'open' => 'открыто', 'closed' => 'закрыто', 'Priority:' => 'Приоритет:', @@ -1096,7 +1082,6 @@ return array( 'Started:' => 'Начата:', 'Moved:' => 'Перемещена:', 'Task #%d' => 'Задача #%d', - 'Sub-tasks' => 'Подзадачи', 'Date and time format' => 'Формат даты и времени', 'Time format' => 'Формат времени', 'Start date: ' => 'Дата начала:', @@ -1137,7 +1122,6 @@ return array( 'User filters' => 'Фильтры по пользователям', 'Category filters' => 'Фильтры по категориям', 'Upload a file' => 'Загрузить файл', - 'There is no attachment at the moment.' => 'Вложения на данный момент отсутствуют', 'View file' => 'Просмотр файла', 'Last activity' => 'Последняя активность', 'Change subtask position' => 'Смена позиции подзадачи', @@ -1151,4 +1135,36 @@ return array( 'There is no action at the moment.' => 'Действия на данный момент отсутствуют', 'Import actions from another project' => 'Импорт действий из другого проекта', 'There is no available project.' => 'Нет доступного проекта', + 'Local File' => 'Локальный файл', + 'Configuration' => 'Конфигурация', + 'PHP version:' => 'Версия PHP:', + 'PHP SAPI:' => 'PHP SAPI:', + 'OS version:' => 'Версия ОС:', + 'Database version:' => 'Версия БД:', + 'Browser:' => 'Браузер:', + 'Task view' => 'Просмотр задачи', + 'Edit task' => 'Изменение задачи', + 'Edit description' => 'Изменение описания', + 'New internal link' => 'Новая внутренняя ссылка', + 'Display list of keyboard shortcuts' => 'Показать список клавиатурных сокращений', + 'Menu' => 'Меню', + 'Set start date' => 'Установить дату начала', + 'Avatar' => 'Аватар', + 'Upload my avatar image' => 'Загрузить моё изображение для аватара', + 'Remove my image' => 'Удалить моё изображение', + 'The OAuth2 state parameter is invalid' => 'Параметр состояние OAuth2 неправильный', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index e63a106c..0399530e 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Svi projekti', 'Add a new column' => 'Dodaj novu kolonu', 'Title' => 'Naslov', - 'Nobody assigned' => 'Niko nije dodeljen', 'Assigned to %s' => 'Dodeljen korisniku %s', 'Remove a column' => 'Ukloni kolonu', 'Remove a column from a board' => 'Ukloni kolonu sa table', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Broj zadataka', 'User' => 'Korisnik', 'Comments' => 'Komentari', - 'Write your text in Markdown' => 'Pisanje teksta pomoću Markdown', 'Leave a comment' => 'Ostavi komentar', 'Comment is required' => 'Komentar je obavezan', 'Leave a description' => 'Dodaj opis', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Praćenje vremena: ', 'New sub-task' => 'Novi Pod-zadatak', 'New attachment added "%s"' => 'Novi prilog ubačen "%s"', - 'Comment updated' => 'Komentar izmenjen', 'New comment posted by %s' => 'Novi komentar ostavio %s', // 'New attachment' => '', // 'New comment' => '', + 'Comment updated' => 'Komentar izmenjen', // 'New subtask' => '', // 'Subtask updated' => '', // 'Task updated' => '', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO je uvek prihvatljiv, primer: "%s", "%s"', 'New private project' => 'Novi privatni projekat', 'This project is private' => 'Ovaj projekat je privatan', - 'Type here to create a new sub-task' => 'Kucaj ovde za kreiranje novog pod-zadatka', 'Add' => 'Dodaj', 'Start date' => 'Datum početka', 'Time estimated' => 'Procenjeno vreme', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'Izvoz zbirnig pregleda po danima za "%s"', 'Exports' => 'Izvoz', // 'This export contains the number of tasks per column grouped per day.' => '', - 'Nothing to preview...' => 'Ništa za prikazivanje...', - 'Preview' => 'Pregled', - 'Write' => 'Piši', 'Active swimlanes' => 'Aktivni razdelnik', 'Add a new swimlane' => 'Dodaj razdelnik', 'Change default swimlane' => 'Zameni osnovni razdelnik', @@ -539,7 +533,6 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', - 'Add a link' => 'Dodaj link', // 'Add a new link' => '', // 'Do you really want to remove this link: "%s"?' => '', // 'Do you really want to remove this link with task #%d?' => '', @@ -749,8 +742,6 @@ return array( // 'My activity stream' => '', // 'My calendar' => '', // 'Search tasks' => '', - // 'Back to the calendar' => '', - // 'Filters' => '', // 'Reset filters' => '', // 'My tasks due tomorrow' => '', // 'Tasks due today' => '', @@ -850,7 +841,6 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', // 'Milestone' => '', @@ -902,7 +892,6 @@ return array( // 'Shared' => '', // 'Owner' => '', // 'Unread notifications' => '', - // 'My filters' => '', // 'Notification methods:' => '', // 'Import tasks from CSV file' => '', // 'Unable to read your file' => '', @@ -940,6 +929,7 @@ return array( // 'Usernames must be lowercase and unique' => '', // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', + // 'Link type' => '', // 'Assign automatically a category based on a link' => '', // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', @@ -1049,7 +1039,6 @@ return array( // 'Close a task when there is no activity' => '', // 'Duration in days' => '', // 'Send email when there is no activity on a task' => '', - // 'List of external links' => '', // 'Unable to fetch link information.' => '', // 'Daily background job for tasks' => '', // 'Auto' => '', @@ -1067,9 +1056,7 @@ return array( // 'External link' => '', // 'Copy and paste your link here...' => '', // 'URL' => '', - // 'There is no external link for the moment.' => '', // 'Internal links' => '', - // 'There is no internal link for the moment.' => '', // 'Assign to me' => '', // 'Me' => '', // 'Do not duplicate anything' => '', @@ -1077,7 +1064,6 @@ return array( // 'Users management' => '', // 'Groups management' => '', // 'Create from another project' => '', - // 'There is no subtask at the moment.' => '', // 'open' => '', // 'closed' => '', // 'Priority:' => '', @@ -1096,7 +1082,6 @@ return array( // 'Started:' => '', // 'Moved:' => '', // 'Task #%d' => '', - // 'Sub-tasks' => '', // 'Date and time format' => '', // 'Time format' => '', // 'Start date: ' => '', @@ -1137,7 +1122,6 @@ return array( // 'User filters' => '', // 'Category filters' => '', // 'Upload a file' => '', - // 'There is no attachment at the moment.' => '', // 'View file' => '', // 'Last activity' => '', // 'Change subtask position' => '', @@ -1151,4 +1135,36 @@ return array( // 'There is no action at the moment.' => '', // 'Import actions from another project' => '', // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index 26952355..7e738e70 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Alla projekt', 'Add a new column' => 'Lägg till ny kolumn', 'Title' => 'Titel', - 'Nobody assigned' => 'Ingen tilldelad', 'Assigned to %s' => 'Tilldelad %s', 'Remove a column' => 'Ta bort en kolumn', 'Remove a column from a board' => 'Ta bort en kolumn från tavlan', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Antal uppgifter', 'User' => 'Användare', 'Comments' => 'Kommentarer', - 'Write your text in Markdown' => 'Exempelsyntax för text', 'Leave a comment' => 'Lämna en kommentar', 'Comment is required' => 'En kommentar måste lämnas', 'Leave a description' => 'Lämna en beskrivning', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Tidsspårning', 'New sub-task' => 'Ny deluppgift', 'New attachment added "%s"' => 'Ny bifogning tillagd "%s"', - 'Comment updated' => 'Kommentaren har uppdaterats', 'New comment posted by %s' => 'Ny kommentar postad av %s', 'New attachment' => 'Ny bifogning', 'New comment' => 'Ny kommentar', + 'Comment updated' => 'Kommentaren har uppdaterats', 'New subtask' => 'Ny deluppgift', 'Subtask updated' => 'Deluppgiften har uppdaterats', 'Task updated' => 'Uppgiften har uppdaterats', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO-format är alltid tillåtet, exempel: "%s" och "%s"', 'New private project' => 'Nytt privat projekt', 'This project is private' => 'Det här projektet är privat', - 'Type here to create a new sub-task' => 'Skriv här för att skapa en ny deluppgift', 'Add' => 'Lägg till', 'Start date' => 'Startdatum', 'Time estimated' => 'Uppskattad tid', @@ -483,9 +480,6 @@ return array( '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', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Uppgiftsålder i dagar', 'Days in this column' => 'Dagar i denna kolumn', '%dd' => '%dd', - 'Add a link' => 'Lägg till länk', 'Add a new link' => 'Lägg till ny länk', 'Do you really want to remove this link: "%s"?' => 'Vill du verkligen ta bort länken: "%s"?', 'Do you really want to remove this link with task #%d?' => 'Vill du verkligen ta bort länken till uppgiften #%d?', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'Min aktivitetsström', 'My calendar' => 'Min kalender', 'Search tasks' => 'Sök uppgifter', - 'Back to the calendar' => 'Tillbaka till kalendern', - 'Filters' => 'Filter', 'Reset filters' => 'Återställ filter', 'My tasks due tomorrow' => 'Mina uppgifter förfaller imorgon', 'Tasks due today' => 'Uppgifter förfaller idag', @@ -850,7 +841,6 @@ return array( // 'End date:' => '', // 'There is no start date or end date for this project.' => '', // 'Projects Gantt chart' => '', - // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', // 'Milestone' => '', @@ -902,7 +892,6 @@ return array( // 'Shared' => '', // 'Owner' => '', // 'Unread notifications' => '', - // 'My filters' => '', // 'Notification methods:' => '', // 'Import tasks from CSV file' => '', // 'Unable to read your file' => '', @@ -940,6 +929,7 @@ return array( // 'Usernames must be lowercase and unique' => '', // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', + // 'Link type' => '', // 'Assign automatically a category based on a link' => '', // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', @@ -1049,7 +1039,6 @@ return array( // 'Close a task when there is no activity' => '', // 'Duration in days' => '', // 'Send email when there is no activity on a task' => '', - // 'List of external links' => '', // 'Unable to fetch link information.' => '', // 'Daily background job for tasks' => '', // 'Auto' => '', @@ -1067,9 +1056,7 @@ return array( // 'External link' => '', // 'Copy and paste your link here...' => '', // 'URL' => '', - // 'There is no external link for the moment.' => '', // 'Internal links' => '', - // 'There is no internal link for the moment.' => '', // 'Assign to me' => '', // 'Me' => '', // 'Do not duplicate anything' => '', @@ -1077,7 +1064,6 @@ return array( // 'Users management' => '', // 'Groups management' => '', // 'Create from another project' => '', - // 'There is no subtask at the moment.' => '', // 'open' => '', // 'closed' => '', // 'Priority:' => '', @@ -1096,7 +1082,6 @@ return array( // 'Started:' => '', // 'Moved:' => '', // 'Task #%d' => '', - // 'Sub-tasks' => '', // 'Date and time format' => '', // 'Time format' => '', // 'Start date: ' => '', @@ -1137,7 +1122,6 @@ return array( // 'User filters' => '', // 'Category filters' => '', // 'Upload a file' => '', - // 'There is no attachment at the moment.' => '', // 'View file' => '', // 'Last activity' => '', // 'Change subtask position' => '', @@ -1151,4 +1135,36 @@ return array( // 'There is no action at the moment.' => '', // 'Import actions from another project' => '', // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index 12f374b6..6765e8ea 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'โปรเจคทั้งหมด', 'Add a new column' => 'เพิ่มคอลัมน์ใหม่', 'Title' => 'หัวเรื่อง', - 'Nobody assigned' => 'ไม่กำหนดใคร', 'Assigned to %s' => 'กำหนดให้ %s', 'Remove a column' => 'ลบคอลัมน์', 'Remove a column from a board' => 'ลบคอลัมน์ออกจากบอร์ด', @@ -166,7 +165,6 @@ return array( 'Task count' => 'นับงาน', 'User' => 'ผู้ใช้', 'Comments' => 'ความคิดเห็น', - 'Write your text in Markdown' => 'เขียนข้อความในรูปแบบ Markdown', 'Leave a comment' => 'ออกความคิดเห็น', 'Comment is required' => 'ต้องการความคิดเห็น', 'Leave a description' => 'แสดงคำอธิบาย', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'การติดตามเวลา:', 'New sub-task' => 'งานย่อยใหม่', 'New attachment added "%s"' => 'เพิ่มการแนบใหม่ "%s"', - 'Comment updated' => 'ปรับปรุงความคิดเห็น', 'New comment posted by %s' => 'ความคิดเห็นใหม่จาก %s', 'New attachment' => 'การแนบใหม่', 'New comment' => 'ความคิดเห็นใหม่', + 'Comment updated' => 'ปรับปรุงความคิดเห็น', 'New subtask' => 'งานย่อยใหม่', 'Subtask updated' => 'ปรับปรุงงานย่อยแล้ว', 'Task updated' => 'ปรับปรุงงานแล้ว', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'ยอมรับรูปแบบ ISO ตัวอย่าง: "%s" และ "%s"', 'New private project' => 'เพิ่มโปรเจคส่วนตัวใหม่', 'This project is private' => 'โปรเจคนี้เป็นโปรเจคส่วนตัว', - 'Type here to create a new sub-task' => 'พิมพ์ที่นี้เพื่อสร้างงานย่อยใหม่', 'Add' => 'เพิ่ม', 'Start date' => 'เริ่มวันที่', 'Time estimated' => 'เวลาโดยประมาณ', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => 'ส่งออกสรุปโปรเจครายวันสำหรับ "%s"', 'Exports' => 'ส่งออก', 'This export contains the number of tasks per column grouped per day.' => 'การส่งออกนี้เป็นการนับจำนวนงานในแต่ละคอลัมน์ในแต่ละวัน', - 'Nothing to preview...' => 'ไม่มีพรีวิว...', - 'Preview' => 'พรีวิว', - 'Write' => 'เขียน', 'Active swimlanes' => 'สวิมเลนพร้อมใช้งาน', 'Add a new swimlane' => 'เพิ่มสวิมเลนใหม่', 'Change default swimlane' => 'เปลี่ยนสวิมเลนเริ่มต้น', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'อายุงาน', 'Days in this column' => 'วันในคอลัมน์นี้', '%dd' => '%d วัน', - 'Add a link' => 'เพิ่มลิงค์', 'Add a new link' => 'เพิ่มลิงค์ใหม่', 'Do you really want to remove this link: "%s"?' => 'คุณต้องการลบลิงค์นี้: "%s"?', 'Do you really want to remove this link with task #%d?' => 'คุณต้องการลบลิงค์นี้ของงาน #%d?', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'กิจกรรมที่เกิดขึ้นของฉัน', 'My calendar' => 'ปฎิทินของฉัน', 'Search tasks' => 'ค้นหางาน', - 'Back to the calendar' => 'กลับไปที่ปฎิทิน', - 'Filters' => 'ตัวกรอง', 'Reset filters' => 'ล้างตัวกรอง', 'My tasks due tomorrow' => 'งานถึงกำหนดของฉันวันพรุ่งนี้', 'Tasks due today' => 'งานถึงกำหนดวันนี้', @@ -850,7 +841,6 @@ return array( 'End date:' => 'วันที่จบ:', 'There is no start date or end date for this project.' => 'ไม่มีวันที่เริ่มหรือวันที่จบของโปรเจคนี้', 'Projects Gantt chart' => 'แผนภูมิแกรน์ของโปรเจค', - 'Link type' => 'ประเภทลิงค์', 'Change task color when using a specific task link' => 'เปลี่ยนสีงานเมื่อมีการใช้การเชื่อมโยงงาน', 'Task link creation or modification' => 'การสร้างการเชื่อมโยงงานหรือการปรับเปลี่ยน', 'Milestone' => 'ขั้น', @@ -902,7 +892,6 @@ return array( 'Shared' => 'แชร์', 'Owner' => 'เจ้าของ', 'Unread notifications' => 'การแจ้งเตือนยังไม่ได้อ่าน', - 'My filters' => 'ตัวกรองของฉัน', 'Notification methods:' => 'ลักษณะการแจ้งเตือน:', 'Import tasks from CSV file' => 'นำเข้างานจากไฟล์ CSV', 'Unable to read your file' => 'ไม่สามารถอ่านไฟล์ของคุณ', @@ -940,6 +929,7 @@ return array( 'Usernames must be lowercase and unique' => 'ชื่อผู้ใช้ต้องเป็นตัวพิมพ์เล็กและไม่ซ้ำ', // 'Passwords will be encrypted if present' => '', '%s attached a new file to the task %s' => '%s แนบไฟล์ใหม่ในงาน %s', + 'Link type' => 'ประเภทลิงค์', 'Assign automatically a category based on a link' => 'กำหนดหมวดอัตโนมัติตามลิงค์', // 'BAM - Konvertible Mark' => '', 'Assignee Username' => 'กำหนดชื่อผู้ใช้', @@ -1049,7 +1039,6 @@ return array( 'Close a task when there is no activity' => 'ปิดงานเมื่อไม่มีกิจกกรมเกิดขึ้น', 'Duration in days' => 'ระยะเวลาวันที่', 'Send email when there is no activity on a task' => 'ส่งอีเมลเมื่อไม่มีกิจกรรมเกิดขึ้นในงาน', - 'List of external links' => 'รายการเชื่อมโยงภายนอก', 'Unable to fetch link information.' => 'ไม่สามารถดึงข้อมูลการเชื่อมโยง', // 'Daily background job for tasks' => '', 'Auto' => 'อัตโนมัติ', @@ -1067,9 +1056,7 @@ return array( 'External link' => 'เชื่อมโยงภายนอก', 'Copy and paste your link here...' => 'คัดลอกและวางลิงค์ของคุณที่นี้...', 'URL' => 'URL', - 'There is no external link for the moment.' => 'ขณะนี้ไม่มีการเชื่อมโยงภายนอก', 'Internal links' => 'เชื่อมโยงภายใน', - 'There is no internal link for the moment.' => 'ขณะนี้ไม่มีการเชื่อมโยงภายใน', 'Assign to me' => 'ฉันรับผิดชอบ', 'Me' => 'ฉัน', // 'Do not duplicate anything' => '', @@ -1077,7 +1064,6 @@ return array( 'Users management' => 'การจัดการผู้ใช้', 'Groups management' => 'การจัดการกลุ่ม', 'Create from another project' => 'สร้างโปรเจคอื่น', - 'There is no subtask at the moment.' => 'ขณะนี้ไม่มีงานย่อย', 'open' => 'เปิด', 'closed' => 'ปิด', 'Priority:' => 'ความสำคัญ:', @@ -1096,7 +1082,6 @@ return array( 'Started:' => 'เริ่ม:', 'Moved:' => 'ย้าย:', 'Task #%d' => 'งานที่ #%d', - 'Sub-tasks' => 'งานย่อย', 'Date and time format' => 'รูปแบบของวันเวลา', 'Time format' => 'รูปแบบของเวลา', 'Start date: ' => 'เริ่มวันที่:', @@ -1137,7 +1122,6 @@ return array( // 'User filters' => '', // 'Category filters' => '', // 'Upload a file' => '', - // 'There is no attachment at the moment.' => '', // 'View file' => '', // 'Last activity' => '', // 'Change subtask position' => '', @@ -1151,4 +1135,36 @@ return array( // 'There is no action at the moment.' => '', // 'Import actions from another project' => '', // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index 2dc611e4..f771b106 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => 'Tüm projeler', 'Add a new column' => 'Yeni sütun ekle', 'Title' => 'Başlık', - 'Nobody assigned' => 'Kullanıcı atanmamış', 'Assigned to %s' => '%s kullanıcısına atanmış', 'Remove a column' => 'Bir sütunu sil', 'Remove a column from a board' => 'Tablodan bir sütunu sil', @@ -166,7 +165,6 @@ return array( 'Task count' => 'Görev sayısı', 'User' => 'Kullanıcı', 'Comments' => 'Yorumlar', - 'Write your text in Markdown' => 'Yazınızı Markdown ile yazın', 'Leave a comment' => 'Bir yorum ekle', 'Comment is required' => 'Yorum gerekli', 'Leave a description' => 'Açıklama ekleyin', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => 'Zaman takibi', 'New sub-task' => 'Yeni alt görev', 'New attachment added "%s"' => 'Yeni dosya "%s" eklendi.', - 'Comment updated' => 'Yorum güncellendi', 'New comment posted by %s' => '%s tarafından yeni yorum eklendi', 'New attachment' => 'Yeni dosya eki', 'New comment' => 'Yeni yorum', + 'Comment updated' => 'Yorum güncellendi', 'New subtask' => 'Yeni alt görev', 'Subtask updated' => 'Alt görev güncellendi', 'Task updated' => 'Görev güncellendi', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formatı her zaman kabul edilir, örneğin: "%s" ve "%s"', 'New private project' => 'Yeni özel proje', 'This project is private' => 'Bu proje özel', - 'Type here to create a new sub-task' => 'Yeni bir alt görev oluşturmak için buraya yazın', 'Add' => 'Ekle', 'Start date' => 'Başlangıç tarihi', 'Time estimated' => 'Tahmini süre', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => '"%s" için günlük proje özetinin dışa', 'Exports' => 'Dışa aktarımlar', 'This export contains the number of tasks per column grouped per day.' => 'Bu dışa aktarım günlük gruplanmış olarak her sütundaki görev sayısını içerir.', - 'Nothing to preview...' => 'Önizleme yapılacak bir şey yok ...', - 'Preview' => 'Önizleme', - 'Write' => 'Değiştir', 'Active swimlanes' => 'Aktif Kulvar', 'Add a new swimlane' => 'Yeni bir Kulvar ekle', 'Change default swimlane' => 'Varsayılan Kulvarı değiştir', @@ -539,7 +533,6 @@ return array( 'Task age in days' => 'Görev yaşı gün olarak', 'Days in this column' => 'Bu sütunda geçirilen gün', '%dd' => '%dG', - 'Add a link' => 'Link ekle', 'Add a new link' => 'Yeni link ekle', 'Do you really want to remove this link: "%s"?' => '"%s" linkini gerçekten silmek istiyor musunuz?', 'Do you really want to remove this link with task #%d?' => '#%d numaralı görev ile linki gerçekten silmek istiyor musunuz?', @@ -749,8 +742,6 @@ return array( 'My activity stream' => 'Olay akışım', 'My calendar' => 'Takvimim', 'Search tasks' => 'Görevleri ara', - 'Back to the calendar' => 'Takvime geri dön', - 'Filters' => 'Filtreler', 'Reset filters' => 'Filtreleri sıfırla', 'My tasks due tomorrow' => 'Yarına tamamlanması gereken görevlerim', 'Tasks due today' => 'Bugün tamamlanması gereken görevler', @@ -850,7 +841,6 @@ return array( 'End date:' => 'Bitiş tarihi:', 'There is no start date or end date for this project.' => 'Bu proje için başlangıç veya bitiş tarihi yok.', 'Projects Gantt chart' => 'Projeler Gantt diyagramı', - 'Link type' => 'Bağlantı türü', 'Change task color when using a specific task link' => 'Belirli bir görev bağlantısı kullanıldığında görevin rengini değiştir', 'Task link creation or modification' => 'Görev bağlantısı oluşturulması veya değiştirilmesi', 'Milestone' => 'Kilometre taşı', @@ -902,7 +892,6 @@ return array( 'Shared' => 'Paylaşılan', 'Owner' => 'Sahibi', 'Unread notifications' => 'Okunmamış bildirimler', - 'My filters' => 'Filtrelerim', 'Notification methods:' => 'Bildirim yöntemleri:', 'Import tasks from CSV file' => 'CSV dosyasından görevleri içeri aktar', 'Unable to read your file' => 'Dosya okunamıyor', @@ -940,6 +929,7 @@ return array( 'Usernames must be lowercase and unique' => 'Kullanıcı adları küçük harf ve tekil olmalı', 'Passwords will be encrypted if present' => 'Şifreler (eğer varsa) kriptolanır', '%s attached a new file to the task %s' => '%s, %s görevine yeni dosya ekledi', + 'Link type' => 'Bağlantı türü', 'Assign automatically a category based on a link' => 'Bir bağlantıya göre otomatik olarak kategori ata', 'BAM - Konvertible Mark' => 'BAM - Konvertible Mark', 'Assignee Username' => 'Atanan kullanıcı adı', @@ -1049,7 +1039,6 @@ return array( // 'Close a task when there is no activity' => '', // 'Duration in days' => '', // 'Send email when there is no activity on a task' => '', - // 'List of external links' => '', // 'Unable to fetch link information.' => '', // 'Daily background job for tasks' => '', // 'Auto' => '', @@ -1067,9 +1056,7 @@ return array( // 'External link' => '', // 'Copy and paste your link here...' => '', // 'URL' => '', - // 'There is no external link for the moment.' => '', // 'Internal links' => '', - // 'There is no internal link for the moment.' => '', // 'Assign to me' => '', // 'Me' => '', // 'Do not duplicate anything' => '', @@ -1077,7 +1064,6 @@ return array( // 'Users management' => '', // 'Groups management' => '', // 'Create from another project' => '', - // 'There is no subtask at the moment.' => '', // 'open' => '', // 'closed' => '', // 'Priority:' => '', @@ -1096,7 +1082,6 @@ return array( // 'Started:' => '', // 'Moved:' => '', // 'Task #%d' => '', - // 'Sub-tasks' => '', // 'Date and time format' => '', // 'Time format' => '', // 'Start date: ' => '', @@ -1137,7 +1122,6 @@ return array( // 'User filters' => '', // 'Category filters' => '', // 'Upload a file' => '', - // 'There is no attachment at the moment.' => '', // 'View file' => '', // 'Last activity' => '', // 'Change subtask position' => '', @@ -1151,4 +1135,36 @@ return array( // 'There is no action at the moment.' => '', // 'Import actions from another project' => '', // 'There is no available project.' => '', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index dadedc5b..baa7693a 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -72,7 +72,6 @@ return array( 'All projects' => '所有项目', 'Add a new column' => '添加新栏目', 'Title' => '标题', - 'Nobody assigned' => '无人被指派', 'Assigned to %s' => '指派给 %s', 'Remove a column' => '移除一个栏目', 'Remove a column from a board' => '从看板移除一个栏目', @@ -166,7 +165,6 @@ return array( 'Task count' => '任务数', 'User' => '用户', 'Comments' => '评论', - 'Write your text in Markdown' => '用Markdown格式编写', 'Leave a comment' => '留言', 'Comment is required' => '必须得有评论', 'Leave a description' => '给一个描述', @@ -329,10 +327,10 @@ return array( 'Time tracking:' => '时间记录', 'New sub-task' => '新建子任务', 'New attachment added "%s"' => '新附件已添加"%s"', - 'Comment updated' => '更新了评论', 'New comment posted by %s' => '%s 的新评论', 'New attachment' => '新建附件', 'New comment' => '新建评论', + 'Comment updated' => '更新了评论', 'New subtask' => '新建子任务', 'Subtask updated' => '子任务更新', 'Task updated' => '任务更新', @@ -432,7 +430,6 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO 格式总是允许的,例如:"%s" 和 "%s"', 'New private project' => '新建私有项目', 'This project is private' => '此项目为私有项目', - 'Type here to create a new sub-task' => '要创建新的子任务,请在此输入', 'Add' => '添加', 'Start date' => '启动日期', 'Time estimated' => '预计时间', @@ -483,9 +480,6 @@ return array( 'Daily project summary export for "%s"' => '导出项目"%s"的每日汇总', 'Exports' => '导出', 'This export contains the number of tasks per column grouped per day.' => '此导出包含每列的任务数,按天分组', - 'Nothing to preview...' => '没有需要预览的内容', - 'Preview' => '预览', - 'Write' => '书写', 'Active swimlanes' => '活动里程碑', 'Add a new swimlane' => '添加新里程碑', 'Change default swimlane' => '修改默认里程碑', @@ -539,7 +533,6 @@ return array( 'Task age in days' => '任务存在天数', 'Days in this column' => '在此栏目的天数', '%dd' => '%d天', - 'Add a link' => '添加一个关联', 'Add a new link' => '添加一个新关联', 'Do you really want to remove this link: "%s"?' => '确认要删除此关联吗:"%s"?', 'Do you really want to remove this link with task #%d?' => '确认要删除到任务 #%d 的关联吗?', @@ -749,8 +742,6 @@ return array( 'My activity stream' => '我的活动流', 'My calendar' => '我的日程表', 'Search tasks' => '搜索任务', - 'Back to the calendar' => '返回日程', - 'Filters' => '过滤器', 'Reset filters' => '重置过滤器', 'My tasks due tomorrow' => '我的明天到期的任务', 'Tasks due today' => '今天到期的任务', @@ -850,7 +841,6 @@ return array( 'End date:' => '结束日期', 'There is no start date or end date for this project.' => '当前项目没有开始或结束日期', 'Projects Gantt chart' => '项目甘特图', - 'Link type' => '关联类型', 'Change task color when using a specific task link' => '当任务关联到指定任务时改变颜色', 'Task link creation or modification' => '任务链接创建或更新时间', 'Milestone' => '里程碑', @@ -902,7 +892,6 @@ return array( 'Shared' => '共享', 'Owner' => '所有人', 'Unread notifications' => '未读通知', - 'My filters' => '我的过滤器', 'Notification methods:' => '通知提醒方式:', 'Import tasks from CSV file' => '从CSV文件导入任务', 'Unable to read your file' => '无法读取文件', @@ -940,6 +929,7 @@ return array( 'Usernames must be lowercase and unique' => '用户名必须小写且唯一', 'Passwords will be encrypted if present' => '密码将被加密', '%s attached a new file to the task %s' => '"%s"添加了附件到任务"%s"', + 'Link type' => '关联类型', 'Assign automatically a category based on a link' => '基于链接自动关联分类', 'BAM - Konvertible Mark' => '波斯尼亚马克', 'Assignee Username' => '指派用户名', @@ -1049,7 +1039,6 @@ return array( 'Close a task when there is no activity' => '当任务没有活动记录时关闭任务', 'Duration in days' => '持续天数', 'Send email when there is no activity on a task' => '当任务没有活动记录时发送邮件', - 'List of external links' => '列出外部关联', 'Unable to fetch link information.' => '无法获取关联信息', 'Daily background job for tasks' => '每日后台任务', 'Auto' => '自动', @@ -1067,9 +1056,7 @@ return array( 'External link' => '外部关联', 'Copy and paste your link here...' => '复制并粘贴链接到当前位置...', 'URL' => 'URL', - 'There is no external link for the moment.' => '当前没有外部关联。', 'Internal links' => '内部关联', - 'There is no internal link for the moment.' => '当前没有内部关联。', 'Assign to me' => '指派给我', 'Me' => '我', 'Do not duplicate anything' => '不再重复', @@ -1077,7 +1064,6 @@ return array( 'Users management' => '用户管理', 'Groups management' => '用户组管理', 'Create from another project' => '从另一个项目中创建', - 'There is no subtask at the moment.' => '当前没有子任务。', 'open' => '打开', 'closed' => '已关闭', 'Priority:' => '优先级:', @@ -1096,7 +1082,6 @@ return array( 'Started:' => '已开始:', 'Moved:' => '已移走', 'Task #%d' => '任务#%d', - 'Sub-tasks' => '子任务', 'Date and time format' => '时间和日期格式', 'Time format' => '时间格式', 'Start date: ' => '开始时间:', @@ -1137,7 +1122,6 @@ return array( 'User filters' => '用户过滤器', 'Category filters' => '分类过滤器', 'Upload a file' => '上传文件', - 'There is no attachment at the moment.' => '当前没有附件。', 'View file' => '查看文件', 'Last activity' => '最后活动', 'Change subtask position' => '更改子任务位置', @@ -1151,4 +1135,36 @@ return array( 'There is no action at the moment.' => '当前没有动作。', 'Import actions from another project' => '从另一个项目中导入动作', 'There is no available project.' => '当前没有可用项目', + // 'Local File' => '', + // 'Configuration' => '', + // 'PHP version:' => '', + // 'PHP SAPI:' => '', + // 'OS version:' => '', + // 'Database version:' => '', + // 'Browser:' => '', + // 'Task view' => '', + // 'Edit task' => '', + // 'Edit description' => '', + // 'New internal link' => '', + // 'Display list of keyboard shortcuts' => '', + // 'Menu' => '', + // 'Set start date' => '', + // 'Avatar' => '', + // 'Upload my avatar image' => '', + // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', ); diff --git a/app/Model/AvatarFile.php b/app/Model/AvatarFile.php new file mode 100644 index 00000000..c49f9fd5 --- /dev/null +++ b/app/Model/AvatarFile.php @@ -0,0 +1,112 @@ +<?php + +namespace Kanboard\Model; + +use Exception; + +/** + * Avatar File + * + * @package model + * @author Frederic Guillot + */ +class AvatarFile extends Base +{ + /** + * Path prefix + * + * @var string + */ + const PATH_PREFIX = 'avatars'; + + /** + * Get image filename + * + * @access public + * @param integer $user_id + * @return string + */ + public function getFilename($user_id) + { + return $this->db->table(User::TABLE)->eq('id', $user_id)->findOneColumn('avatar_path'); + } + + /** + * Add avatar in the user profile + * + * @access public + * @param integer $user_id Foreign key + * @param string $path Path on the disk + * @return bool + */ + public function create($user_id, $path) + { + $result = $this->db->table(User::TABLE)->eq('id', $user_id)->update(array( + 'avatar_path' => $path, + )); + + $this->userSession->refresh($user_id); + + return $result; + } + + /** + * Remove avatar from the user profile + * + * @access public + * @param integer $user_id Foreign key + * @return bool + */ + public function remove($user_id) + { + try { + $this->objectStorage->remove($this->getFilename($user_id)); + $result = $this->db->table(User::TABLE)->eq('id', $user_id)->update(array('avatar_path' => '')); + $this->userSession->refresh($user_id); + return $result; + } catch (Exception $e) { + $this->logger->error($e->getMessage()); + return false; + } + } + + /** + * Upload avatar image + * + * @access public + * @param integer $user_id + * @param array $file + * @return boolean + */ + public function uploadFile($user_id, array $file) + { + try { + if ($file['error'] == UPLOAD_ERR_OK && $file['size'] > 0) { + $destination_filename = $this->generatePath($user_id, $file['name']); + $this->objectStorage->moveUploadedFile($file['tmp_name'], $destination_filename); + $this->create($user_id, $destination_filename); + } else { + throw new Exception('File not uploaded: '.var_export($file['error'], true)); + } + + } catch (Exception $e) { + $this->logger->error($e->getMessage()); + return false; + } + + return true; + } + + /** + * Generate the path for a new filename + * + * @access public + * @param integer $user_id + * @param string $filename + * @return string + */ + public function generatePath($user_id, $filename) + { + return implode(DIRECTORY_SEPARATOR, array(self::PATH_PREFIX, $user_id, hash('sha1', $filename.time()))); + } +} diff --git a/app/Model/Base.php b/app/Model/Base.php index 714b4308..a27560c8 100644 --- a/app/Model/Base.php +++ b/app/Model/Base.php @@ -31,28 +31,4 @@ abstract class Base extends \Kanboard\Core\Base return (int) $db->getLastId(); }); } - - /** - * Build SQL condition for a given time range - * - * @access protected - * @param string $start_time Start timestamp - * @param string $end_time End timestamp - * @param string $start_column Start column name - * @param string $end_column End column name - * @return string - */ - protected function getCalendarCondition($start_time, $end_time, $start_column, $end_column) - { - $start_column = $this->db->escapeIdentifier($start_column); - $end_column = $this->db->escapeIdentifier($end_column); - - $conditions = array( - "($start_column >= '$start_time' AND $start_column <= '$end_time')", - "($start_column <= '$start_time' AND $end_column >= '$start_time')", - "($start_column <= '$start_time' AND ($end_column = '0' OR $end_column IS NULL))", - ); - - return $start_column.' IS NOT NULL AND '.$start_column.' > 0 AND ('.implode(' OR ', $conditions).')'; - } } diff --git a/app/Model/Comment.php b/app/Model/Comment.php index 6eb4a1e5..c5091d89 100644 --- a/app/Model/Comment.php +++ b/app/Model/Comment.php @@ -48,7 +48,8 @@ class Comment extends Base self::TABLE.'.comment', User::TABLE.'.username', User::TABLE.'.name', - User::TABLE.'.email' + User::TABLE.'.email', + User::TABLE.'.avatar_path' ) ->join(User::TABLE, 'id', 'user_id') ->orderBy(self::TABLE.'.date_creation', $sorting) @@ -75,7 +76,9 @@ class Comment extends Base self::TABLE.'.comment', self::TABLE.'.reference', User::TABLE.'.username', - User::TABLE.'.name' + User::TABLE.'.name', + User::TABLE.'.email', + User::TABLE.'.avatar_path' ) ->join(User::TABLE, 'id', 'user_id') ->eq(self::TABLE.'.id', $comment_id) diff --git a/app/Model/Config.php b/app/Model/Config.php index 7b254c8d..0c363fb0 100644 --- a/app/Model/Config.php +++ b/app/Model/Config.php @@ -90,6 +90,7 @@ class Config extends Setting 'fi_FI' => 'Suomi', 'sv_SE' => 'Svenska', 'tr_TR' => 'Türkçe', + 'ko_KR' => '한국어', 'zh_CN' => '中文(简体)', 'ja_JP' => '日本語', 'th_TH' => 'ไทย', @@ -129,6 +130,7 @@ class Config extends Setting 'fi_FI' => 'fi', 'sv_SE' => 'sv', 'tr_TR' => 'tr', + 'ko_KR' => 'ko', 'zh_CN' => 'zh-cn', 'ja_JP' => 'ja', 'th_TH' => 'th', diff --git a/app/Model/File.php b/app/Model/File.php index 03ea691d..e383235c 100644 --- a/app/Model/File.php +++ b/app/Model/File.php @@ -3,8 +3,8 @@ namespace Kanboard\Model; use Exception; +use Kanboard\Core\Thumbnail; use Kanboard\Event\FileEvent; -use Kanboard\Core\Tool; use Kanboard\Core\ObjectStorage\ObjectStorageException; /** @@ -315,15 +315,15 @@ abstract class File extends Base */ public function generateThumbnailFromData($destination_filename, &$data) { - $temp_filename = tempnam(sys_get_temp_dir(), 'datafile'); + $blob = Thumbnail::createFromString($data) + ->resize() + ->toString(); - file_put_contents($temp_filename, $data); - $this->generateThumbnailFromFile($temp_filename, $destination_filename); - unlink($temp_filename); + $this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob); } /** - * Generate thumbnail from a blob + * Generate thumbnail from a local file * * @access public * @param string $uploaded_filename @@ -331,8 +331,10 @@ abstract class File extends Base */ public function generateThumbnailFromFile($uploaded_filename, $destination_filename) { - $thumbnail_filename = tempnam(sys_get_temp_dir(), 'thumbnail'); - Tool::generateThumbnail($uploaded_filename, $thumbnail_filename); - $this->objectStorage->moveFile($thumbnail_filename, $this->getThumbnailPath($destination_filename)); + $blob = Thumbnail::createFromFile($uploaded_filename) + ->resize() + ->toString(); + + $this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob); } } diff --git a/app/Model/Project.php b/app/Model/Project.php index d2e5b7ce..6e3c2326 100644 --- a/app/Model/Project.php +++ b/app/Model/Project.php @@ -35,6 +35,20 @@ class Project extends Base const INACTIVE = 0; /** + * Value for private project + * + * @var integer + */ + const TYPE_PRIVATE = 1; + + /** + * Value for team project + * + * @var integer + */ + const TYPE_TEAM = 0; + + /** * Get a project by the id * * @access public diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php index 74df26a1..d993015b 100644 --- a/app/Model/ProjectActivity.php +++ b/app/Model/ProjectActivity.php @@ -2,6 +2,8 @@ namespace Kanboard\Model; +use PicoDb\Table; + /** * Project activity model * @@ -51,113 +53,26 @@ class ProjectActivity extends Base } /** - * Get all events for the given project - * - * @access public - * @param integer $project_id Project id - * @param integer $limit Maximum events number - * @param integer $start Timestamp of earliest activity - * @param integer $end Timestamp of latest activity - * @return array - */ - public function getProject($project_id, $limit = 50, $start = null, $end = null) - { - return $this->getProjects(array($project_id), $limit, $start, $end); - } - - /** - * Get all events for the given projects list + * Get query * * @access public - * @param integer[] $project_ids Projects id - * @param integer $limit Maximum events number - * @param integer $start Timestamp of earliest activity - * @param integer $end Timestamp of latest activity - * @return array + * @return Table */ - public function getProjects(array $project_ids, $limit = 50, $start = null, $end = null) + public function getQuery() { - if (empty($project_ids)) { - return array(); - } - - $query = $this - ->db - ->table(self::TABLE) - ->columns( - self::TABLE.'.*', - User::TABLE.'.username AS author_username', - User::TABLE.'.name AS author_name', - User::TABLE.'.email' - ) - ->in('project_id', $project_ids) - ->join(User::TABLE, 'id', 'creator_id') - ->desc(self::TABLE.'.id') - ->limit($limit); - - return $this->getEvents($query, $start, $end); - } - - /** - * Get all events for the given task - * - * @access public - * @param integer $task_id Task id - * @param integer $limit Maximum events number - * @param integer $start Timestamp of earliest activity - * @param integer $end Timestamp of latest activity - * @return array - */ - public function getTask($task_id, $limit = 50, $start = null, $end = null) - { - $query = $this - ->db - ->table(self::TABLE) - ->columns( - self::TABLE.'.*', - User::TABLE.'.username AS author_username', - User::TABLE.'.name AS author_name', - User::TABLE.'.email' - ) - ->eq('task_id', $task_id) - ->join(User::TABLE, 'id', 'creator_id') - ->desc(self::TABLE.'.id') - ->limit($limit); - - return $this->getEvents($query, $start, $end); - } - - /** - * Common function to return events - * - * @access public - * @param \PicoDb\Table $query PicoDb Query - * @param integer $start Timestamp of earliest activity - * @param integer $end Timestamp of latest activity - * @return array - */ - private function getEvents(\PicoDb\Table $query, $start, $end) - { - if (! is_null($start)) { - $query->gte('date_creation', $start); - } - - if (! is_null($end)) { - $query->lte('date_creation', $end); - } - - $events = $query->findAll(); - - foreach ($events as &$event) { - $event += $this->decode($event['data']); - unset($event['data']); - - $event['author'] = $event['author_name'] ?: $event['author_username']; - $event['event_title'] = $this->notification->getTitleWithAuthor($event['author'], $event['event_name'], $event); - $event['event_content'] = $this->getContent($event); - } - - return $events; + return $this + ->db + ->table(ProjectActivity::TABLE) + ->columns( + ProjectActivity::TABLE.'.*', + 'uc.username AS author_username', + 'uc.name AS author_name', + 'uc.email', + 'uc.avatar_path' + ) + ->join(Task::TABLE, 'id', 'task_id') + ->join(Project::TABLE, 'id', 'project_id') + ->left(User::TABLE, 'uc', 'id', ProjectActivity::TABLE, 'creator_id'); } /** @@ -175,35 +90,4 @@ class ProjectActivity extends Base $this->db->table(self::TABLE)->in('id', $ids)->remove(); } } - - /** - * Get the event html content - * - * @access public - * @param array $params Event properties - * @return string - */ - public function getContent(array $params) - { - return $this->template->render( - 'event/'.str_replace('.', '_', $params['event_name']), - $params - ); - } - - /** - * 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/app/Model/ProjectGroupRoleFilter.php b/app/Model/ProjectGroupRoleFilter.php deleted file mode 100644 index 989d3073..00000000 --- a/app/Model/ProjectGroupRoleFilter.php +++ /dev/null @@ -1,89 +0,0 @@ -<?php - -namespace Kanboard\Model; - -/** - * Project Group Role Filter - * - * @package model - * @author Frederic Guillot - */ -class ProjectGroupRoleFilter extends Base -{ - /** - * Query - * - * @access protected - * @var \PicoDb\Table - */ - protected $query; - - /** - * Initialize filter - * - * @access public - * @return UserFilter - */ - public function create() - { - $this->query = $this->db->table(ProjectGroupRole::TABLE); - return $this; - } - - /** - * Get all results of the filter - * - * @access public - * @param string $column - * @return array - */ - public function findAll($column = '') - { - if ($column !== '') { - return $this->query->asc($column)->findAllByColumn($column); - } - - return $this->query->findAll(); - } - - /** - * Get the PicoDb query - * - * @access public - * @return \PicoDb\Table - */ - public function getQuery() - { - return $this->query; - } - - /** - * Filter by project id - * - * @access public - * @param integer $project_id - * @return ProjectUserRoleFilter - */ - public function filterByProjectId($project_id) - { - $this->query->eq(ProjectGroupRole::TABLE.'.project_id', $project_id); - return $this; - } - - /** - * Filter by username - * - * @access public - * @param string $input - * @return ProjectUserRoleFilter - */ - public function startWithUsername($input) - { - $this->query - ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE) - ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE) - ->ilike(User::TABLE.'.username', $input.'%'); - - return $this; - } -} diff --git a/app/Model/ProjectPermission.php b/app/Model/ProjectPermission.php index db1573ae..59af2b58 100644 --- a/app/Model/ProjectPermission.php +++ b/app/Model/ProjectPermission.php @@ -3,6 +3,10 @@ namespace Kanboard\Model; use Kanboard\Core\Security\Role; +use Kanboard\Filter\ProjectGroupRoleProjectFilter; +use Kanboard\Filter\ProjectGroupRoleUsernameFilter; +use Kanboard\Filter\ProjectUserRoleProjectFilter; +use Kanboard\Filter\ProjectUserRoleUsernameFilter; /** * Project Permission @@ -53,8 +57,18 @@ class ProjectPermission extends Base */ public function findUsernames($project_id, $input) { - $userMembers = $this->projectUserRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username'); - $groupMembers = $this->projectGroupRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username'); + $userMembers = $this->projectUserRoleQuery + ->withFilter(new ProjectUserRoleProjectFilter($project_id)) + ->withFilter(new ProjectUserRoleUsernameFilter($input)) + ->getQuery() + ->findAllByColumn('username'); + + $groupMembers = $this->projectGroupRoleQuery + ->withFilter(new ProjectGroupRoleProjectFilter($project_id)) + ->withFilter(new ProjectGroupRoleUsernameFilter($input)) + ->getQuery() + ->findAllByColumn('username'); + $members = array_unique(array_merge($userMembers, $groupMembers)); sort($members); diff --git a/app/Model/ProjectUserRole.php b/app/Model/ProjectUserRole.php index 56da679c..2956c524 100644 --- a/app/Model/ProjectUserRole.php +++ b/app/Model/ProjectUserRole.php @@ -251,8 +251,8 @@ class ProjectUserRole extends Base /** * Copy user access from a project to another one * - * @param integer $project_src_id Project Template - * @return integer $project_dst_id Project that receives the copy + * @param integer $project_src_id + * @param integer $project_dst_id * @return boolean */ public function duplicate($project_src_id, $project_dst_id) diff --git a/app/Model/ProjectUserRoleFilter.php b/app/Model/ProjectUserRoleFilter.php deleted file mode 100644 index 64403643..00000000 --- a/app/Model/ProjectUserRoleFilter.php +++ /dev/null @@ -1,88 +0,0 @@ -<?php - -namespace Kanboard\Model; - -/** - * Project User Role Filter - * - * @package model - * @author Frederic Guillot - */ -class ProjectUserRoleFilter extends Base -{ - /** - * Query - * - * @access protected - * @var \PicoDb\Table - */ - protected $query; - - /** - * Initialize filter - * - * @access public - * @return UserFilter - */ - public function create() - { - $this->query = $this->db->table(ProjectUserRole::TABLE); - return $this; - } - - /** - * Get all results of the filter - * - * @access public - * @param string $column - * @return array - */ - public function findAll($column = '') - { - if ($column !== '') { - return $this->query->asc($column)->findAllByColumn($column); - } - - return $this->query->findAll(); - } - - /** - * Get the PicoDb query - * - * @access public - * @return \PicoDb\Table - */ - public function getQuery() - { - return $this->query; - } - - /** - * Filter by project id - * - * @access public - * @param integer $project_id - * @return ProjectUserRoleFilter - */ - public function filterByProjectId($project_id) - { - $this->query->eq(ProjectUserRole::TABLE.'.project_id', $project_id); - return $this; - } - - /** - * Filter by username - * - * @access public - * @param string $input - * @return ProjectUserRoleFilter - */ - public function startWithUsername($input) - { - $this->query - ->join(User::TABLE, 'id', 'user_id') - ->ilike(User::TABLE.'.username', $input.'%'); - - return $this; - } -} diff --git a/app/Model/Setting.php b/app/Model/Setting.php index f98d7ce1..c5a4765c 100644 --- a/app/Model/Setting.php +++ b/app/Model/Setting.php @@ -22,6 +22,7 @@ abstract class Setting extends Base * * @abstract * @access public + * @param array $values * @return array */ abstract public function prepare(array $values); diff --git a/app/Model/SubtaskTimeTracking.php b/app/Model/SubtaskTimeTracking.php index b766b542..be04ee1b 100644 --- a/app/Model/SubtaskTimeTracking.php +++ b/app/Model/SubtaskTimeTracking.php @@ -146,94 +146,6 @@ class SubtaskTimeTracking extends Base } /** - * Get user calendar events - * - * @access public - * @param integer $user_id - * @param string $start ISO-8601 format - * @param string $end - * @return array - */ - public function getUserCalendarEvents($user_id, $start, $end) - { - $hook = 'model:subtask-time-tracking:calendar:events'; - $events = $this->getUserQuery($user_id) - ->addCondition($this->getCalendarCondition( - $this->dateParser->getTimestampFromIsoFormat($start), - $this->dateParser->getTimestampFromIsoFormat($end), - 'start', - 'end' - )) - ->findAll(); - - if ($this->hook->exists($hook)) { - $events = $this->hook->first($hook, array( - 'user_id' => $user_id, - 'events' => $events, - 'start' => $start, - 'end' => $end, - )); - } - - return $this->toCalendarEvents($events); - } - - /** - * Get project calendar events - * - * @access public - * @param integer $project_id - * @param integer $start - * @param integer $end - * @return array - */ - public function getProjectCalendarEvents($project_id, $start, $end) - { - $result = $this - ->getProjectQuery($project_id) - ->addCondition($this->getCalendarCondition( - $this->dateParser->getTimestampFromIsoFormat($start), - $this->dateParser->getTimestampFromIsoFormat($end), - 'start', - 'end' - )) - ->findAll(); - - return $this->toCalendarEvents($result); - } - - /** - * Convert a record set to calendar events - * - * @access private - * @param array $rows - * @return array - */ - private function toCalendarEvents(array $rows) - { - $events = array(); - - foreach ($rows as $row) { - $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : ''; - - $events[] = array( - 'id' => $row['id'], - 'subtask_id' => $row['subtask_id'], - 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user, - 'start' => date('Y-m-d\TH:i:s', $row['start']), - 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()), - 'backgroundColor' => $this->color->getBackgroundColor($row['color_id']), - 'borderColor' => $this->color->getBorderColor($row['color_id']), - 'textColor' => 'black', - 'url' => $this->helper->url->to('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])), - 'editable' => false, - ); - } - - return $events; - } - - /** * Return true if a timer is started for this use and subtask * * @access public diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php deleted file mode 100644 index 1883298d..00000000 --- a/app/Model/TaskFilter.php +++ /dev/null @@ -1,745 +0,0 @@ -<?php - -namespace Kanboard\Model; - -/** - * Task Filter - * - * @package model - * @author Frederic Guillot - */ -class TaskFilter extends Base -{ - /** - * Filters mapping - * - * @access private - * @var array - */ - private $filters = array( - 'T_ASSIGNEE' => 'filterByAssignee', - 'T_COLOR' => 'filterByColors', - 'T_DUE' => 'filterByDueDate', - 'T_UPDATED' => 'filterByModificationDate', - 'T_CREATED' => 'filterByCreationDate', - 'T_TITLE' => 'filterByTitle', - 'T_STATUS' => 'filterByStatusName', - 'T_DESCRIPTION' => 'filterByDescription', - 'T_CATEGORY' => 'filterByCategoryName', - 'T_PROJECT' => 'filterByProjectName', - 'T_COLUMN' => 'filterByColumnName', - 'T_REFERENCE' => 'filterByReference', - 'T_SWIMLANE' => 'filterBySwimlaneName', - 'T_LINK' => 'filterByLinkName', - ); - - /** - * Query - * - * @access public - * @var \PicoDb\Table - */ - public $query; - - /** - * Apply filters according to the search input - * - * @access public - * @param string $input - * @return TaskFilter - */ - public function search($input) - { - $tree = $this->lexer->map($this->lexer->tokenize($input)); - $this->query = $this->taskFinder->getExtendedQuery(); - - if (empty($tree)) { - $this->filterByTitle($input); - } - - foreach ($tree as $filter => $value) { - $method = $this->filters[$filter]; - $this->$method($value); - } - - return $this; - } - - /** - * Create a new query - * - * @access public - * @return TaskFilter - */ - public function create() - { - $this->query = $this->db->table(Task::TABLE); - $this->query->left(User::TABLE, 'ua', 'id', Task::TABLE, 'owner_id'); - $this->query->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id'); - - $this->query->columns( - Task::TABLE.'.*', - 'ua.email AS assignee_email', - 'ua.name AS assignee_name', - 'ua.username AS assignee_username', - 'uc.email AS creator_email', - 'uc.username AS creator_username' - ); - - return $this; - } - - /** - * Create a new subtask query - * - * @access public - * @return \PicoDb\Table - */ - public function createSubtaskQuery() - { - return $this->db->table(Subtask::TABLE) - ->columns( - Subtask::TABLE.'.user_id', - Subtask::TABLE.'.task_id', - User::TABLE.'.name', - User::TABLE.'.username' - ) - ->join(User::TABLE, 'id', 'user_id', Subtask::TABLE) - ->neq(Subtask::TABLE.'.status', Subtask::STATUS_DONE); - } - - /** - * Create a new link query - * - * @access public - * @return \PicoDb\Table - */ - public function createLinkQuery() - { - return $this->db->table(TaskLink::TABLE) - ->columns( - TaskLink::TABLE.'.task_id', - Link::TABLE.'.label' - ) - ->join(Link::TABLE, 'id', 'link_id', TaskLink::TABLE); - } - - /** - * Clone the filter - * - * @access public - * @return TaskFilter - */ - public function copy() - { - $filter = new static($this->container); - $filter->query = clone($this->query); - $filter->query->condition = clone($this->query->condition); - return $filter; - } - - /** - * Exclude a list of task_id - * - * @access public - * @param integer[] $task_ids - * @return TaskFilter - */ - public function excludeTasks(array $task_ids) - { - $this->query->notin(Task::TABLE.'.id', $task_ids); - return $this; - } - - /** - * Filter by id - * - * @access public - * @param integer $task_id - * @return TaskFilter - */ - public function filterById($task_id) - { - if ($task_id > 0) { - $this->query->eq(Task::TABLE.'.id', $task_id); - } - - return $this; - } - - /** - * Filter by reference - * - * @access public - * @param string $reference - * @return TaskFilter - */ - public function filterByReference($reference) - { - if (! empty($reference)) { - $this->query->eq(Task::TABLE.'.reference', $reference); - } - - return $this; - } - - /** - * Filter by title - * - * @access public - * @param string $title - * @return TaskFilter - */ - public function filterByDescription($title) - { - $this->query->ilike(Task::TABLE.'.description', '%'.$title.'%'); - return $this; - } - - /** - * Filter by title or id if the string is like #123 or an integer - * - * @access public - * @param string $title - * @return TaskFilter - */ - public function filterByTitle($title) - { - if (ctype_digit($title) || (strlen($title) > 1 && $title{0} === '#' && ctype_digit(substr($title, 1)))) { - $this->query->beginOr(); - $this->query->eq(Task::TABLE.'.id', str_replace('#', '', $title)); - $this->query->ilike(Task::TABLE.'.title', '%'.$title.'%'); - $this->query->closeOr(); - } else { - $this->query->ilike(Task::TABLE.'.title', '%'.$title.'%'); - } - - return $this; - } - - /** - * Filter by a list of project id - * - * @access public - * @param array $project_ids - * @return TaskFilter - */ - public function filterByProjects(array $project_ids) - { - $this->query->in(Task::TABLE.'.project_id', $project_ids); - return $this; - } - - /** - * Filter by project id - * - * @access public - * @param integer $project_id - * @return TaskFilter - */ - public function filterByProject($project_id) - { - if ($project_id > 0) { - $this->query->eq(Task::TABLE.'.project_id', $project_id); - } - - return $this; - } - - /** - * Filter by project name - * - * @access public - * @param array $values List of project name - * @return TaskFilter - */ - public function filterByProjectName(array $values) - { - $this->query->beginOr(); - - foreach ($values as $project) { - if (ctype_digit($project)) { - $this->query->eq(Task::TABLE.'.project_id', $project); - } else { - $this->query->ilike(Project::TABLE.'.name', $project); - } - } - - $this->query->closeOr(); - } - - /** - * Filter by swimlane name - * - * @access public - * @param array $values List of swimlane name - * @return TaskFilter - */ - public function filterBySwimlaneName(array $values) - { - $this->query->beginOr(); - - foreach ($values as $swimlane) { - if ($swimlane === 'default') { - $this->query->eq(Task::TABLE.'.swimlane_id', 0); - } else { - $this->query->ilike(Swimlane::TABLE.'.name', $swimlane); - $this->query->addCondition(Task::TABLE.'.swimlane_id=0 AND '.Project::TABLE.'.default_swimlane '.$this->db->getDriver()->getOperator('ILIKE')." '$swimlane'"); - } - } - - $this->query->closeOr(); - } - - /** - * Filter by category id - * - * @access public - * @param integer $category_id - * @return TaskFilter - */ - public function filterByCategory($category_id) - { - if ($category_id >= 0) { - $this->query->eq(Task::TABLE.'.category_id', $category_id); - } - - return $this; - } - - /** - * Filter by category - * - * @access public - * @param array $values List of assignees - * @return TaskFilter - */ - public function filterByCategoryName(array $values) - { - $this->query->beginOr(); - - foreach ($values as $category) { - if ($category === 'none') { - $this->query->eq(Task::TABLE.'.category_id', 0); - } else { - $this->query->eq(Category::TABLE.'.name', $category); - } - } - - $this->query->closeOr(); - } - - /** - * Filter by assignee - * - * @access public - * @param integer $owner_id - * @return TaskFilter - */ - public function filterByOwner($owner_id) - { - if ($owner_id >= 0) { - $this->query->eq(Task::TABLE.'.owner_id', $owner_id); - } - - return $this; - } - - /** - * Filter by assignee names - * - * @access public - * @param array $values List of assignees - * @return TaskFilter - */ - public function filterByAssignee(array $values) - { - $this->query->beginOr(); - - foreach ($values as $assignee) { - switch ($assignee) { - case 'me': - $this->query->eq(Task::TABLE.'.owner_id', $this->userSession->getId()); - break; - case 'nobody': - $this->query->eq(Task::TABLE.'.owner_id', 0); - break; - default: - $this->query->ilike(User::TABLE.'.username', '%'.$assignee.'%'); - $this->query->ilike(User::TABLE.'.name', '%'.$assignee.'%'); - } - } - - $this->filterBySubtaskAssignee($values); - - $this->query->closeOr(); - - return $this; - } - - /** - * Filter by subtask assignee names - * - * @access public - * @param array $values List of assignees - * @return TaskFilter - */ - public function filterBySubtaskAssignee(array $values) - { - $subtaskQuery = $this->createSubtaskQuery(); - $subtaskQuery->beginOr(); - - foreach ($values as $assignee) { - if ($assignee === 'me') { - $subtaskQuery->eq(Subtask::TABLE.'.user_id', $this->userSession->getId()); - } else { - $subtaskQuery->ilike(User::TABLE.'.username', '%'.$assignee.'%'); - $subtaskQuery->ilike(User::TABLE.'.name', '%'.$assignee.'%'); - } - } - - $subtaskQuery->closeOr(); - - $this->query->in(Task::TABLE.'.id', $subtaskQuery->findAllByColumn('task_id')); - - return $this; - } - - /** - * Filter by color - * - * @access public - * @param string $color_id - * @return TaskFilter - */ - public function filterByColor($color_id) - { - if ($color_id !== '') { - $this->query->eq(Task::TABLE.'.color_id', $color_id); - } - - return $this; - } - - /** - * Filter by colors - * - * @access public - * @param array $colors - * @return TaskFilter - */ - public function filterByColors(array $colors) - { - $this->query->beginOr(); - - foreach ($colors as $color) { - $this->filterByColor($this->color->find($color)); - } - - $this->query->closeOr(); - - return $this; - } - - /** - * Filter by column - * - * @access public - * @param integer $column_id - * @return TaskFilter - */ - public function filterByColumn($column_id) - { - if ($column_id >= 0) { - $this->query->eq(Task::TABLE.'.column_id', $column_id); - } - - return $this; - } - - /** - * Filter by column name - * - * @access public - * @param array $values List of column name - * @return TaskFilter - */ - public function filterByColumnName(array $values) - { - $this->query->beginOr(); - - foreach ($values as $project) { - $this->query->ilike(Column::TABLE.'.title', $project); - } - - $this->query->closeOr(); - } - - /** - * Filter by swimlane - * - * @access public - * @param integer $swimlane_id - * @return TaskFilter - */ - public function filterBySwimlane($swimlane_id) - { - if ($swimlane_id >= 0) { - $this->query->eq(Task::TABLE.'.swimlane_id', $swimlane_id); - } - - return $this; - } - - /** - * Filter by status name - * - * @access public - * @param string $status - * @return TaskFilter - */ - public function filterByStatusName($status) - { - if ($status === 'open' || $status === 'closed') { - $this->filterByStatus($status === 'open' ? Task::STATUS_OPEN : Task::STATUS_CLOSED); - } - - return $this; - } - - /** - * Filter by status - * - * @access public - * @param integer $is_active - * @return TaskFilter - */ - public function filterByStatus($is_active) - { - if ($is_active >= 0) { - $this->query->eq(Task::TABLE.'.is_active', $is_active); - } - - return $this; - } - - /** - * Filter by link - * - * @access public - * @param array $values List of links - * @return TaskFilter - */ - public function filterByLinkName(array $values) - { - $this->query->beginOr(); - - $link_query = $this->createLinkQuery()->in(Link::TABLE.'.label', $values); - $matching_task_ids = $link_query->findAllByColumn('task_id'); - if (empty($matching_task_ids)) { - $this->query->eq(Task::TABLE.'.id', 0); - } else { - $this->query->in(Task::TABLE.'.id', $matching_task_ids); - } - - $this->query->closeOr(); - - return $this; - } - - /** - * Filter by due date - * - * @access public - * @param string $date ISO8601 date format - * @return TaskFilter - */ - public function filterByDueDate($date) - { - $this->query->neq(Task::TABLE.'.date_due', 0); - $this->query->notNull(Task::TABLE.'.date_due'); - return $this->filterWithOperator(Task::TABLE.'.date_due', $date, true); - } - - /** - * Filter by due date (range) - * - * @access public - * @param string $start - * @param string $end - * @return TaskFilter - */ - public function filterByDueDateRange($start, $end) - { - $this->query->gte('date_due', $this->dateParser->getTimestampFromIsoFormat($start)); - $this->query->lte('date_due', $this->dateParser->getTimestampFromIsoFormat($end)); - - return $this; - } - - /** - * Filter by start date (range) - * - * @access public - * @param string $start - * @param string $end - * @return TaskFilter - */ - public function filterByStartDateRange($start, $end) - { - $this->query->addCondition($this->getCalendarCondition( - $this->dateParser->getTimestampFromIsoFormat($start), - $this->dateParser->getTimestampFromIsoFormat($end), - 'date_started', - 'date_completed' - )); - - return $this; - } - - /** - * Filter by creation date - * - * @access public - * @param string $date ISO8601 date format - * @return TaskFilter - */ - public function filterByCreationDate($date) - { - if ($date === 'recently') { - return $this->filterRecentlyDate(Task::TABLE.'.date_creation'); - } - - return $this->filterWithOperator(Task::TABLE.'.date_creation', $date, true); - } - - /** - * Filter by creation date - * - * @access public - * @param string $start - * @param string $end - * @return TaskFilter - */ - public function filterByCreationDateRange($start, $end) - { - $this->query->addCondition($this->getCalendarCondition( - $this->dateParser->getTimestampFromIsoFormat($start), - $this->dateParser->getTimestampFromIsoFormat($end), - 'date_creation', - 'date_completed' - )); - - return $this; - } - - /** - * Filter by modification date - * - * @access public - * @param string $date ISO8601 date format - * @return TaskFilter - */ - public function filterByModificationDate($date) - { - if ($date === 'recently') { - return $this->filterRecentlyDate(Task::TABLE.'.date_modification'); - } - - return $this->filterWithOperator(Task::TABLE.'.date_modification', $date, true); - } - - /** - * Get all results of the filter - * - * @access public - * @return array - */ - public function findAll() - { - return $this->query->asc(Task::TABLE.'.id')->findAll(); - } - - /** - * Get the PicoDb query - * - * @access public - * @return \PicoDb\Table - */ - public function getQuery() - { - return $this->query; - } - - /** - * Get swimlanes and tasks to display the board - * - * @access public - * @return array - */ - public function getBoard($project_id) - { - $tasks = $this->filterByProject($project_id)->query->asc(Task::TABLE.'.position')->findAll(); - - return $this->board->getBoard($project_id, function ($project_id, $column_id, $swimlane_id) use ($tasks) { - return array_filter($tasks, function (array $task) use ($column_id, $swimlane_id) { - return $task['column_id'] == $column_id && $task['swimlane_id'] == $swimlane_id; - }); - }); - } - - /** - * Filter with an operator - * - * @access public - * @param string $field - * @param string $value - * @param boolean $is_date - * @return TaskFilter - */ - private function filterWithOperator($field, $value, $is_date) - { - $operators = array( - '<=' => 'lte', - '>=' => 'gte', - '<' => 'lt', - '>' => 'gt', - ); - - foreach ($operators as $operator => $method) { - if (strpos($value, $operator) === 0) { - $value = substr($value, strlen($operator)); - $this->query->$method($field, $is_date ? $this->dateParser->getTimestampFromIsoFormat($value) : $value); - return $this; - } - } - - if ($is_date) { - $timestamp = $this->dateParser->getTimestampFromIsoFormat($value); - $this->query->gte($field, $timestamp); - $this->query->lte($field, $timestamp + 86399); - } else { - $this->query->eq($field, $value); - } - - return $this; - } - - /** - * Use the board_highlight_period for the "recently" keyword - * - * @access private - * @param string $field - * @return TaskFilter - */ - private function filterRecentlyDate($field) - { - $duration = $this->config->get('board_highlight_period', 0); - - if ($duration > 0) { - $this->query->gte($field, time() - $duration); - } - - return $this; - } -} diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php index d67372cc..d406b794 100644 --- a/app/Model/TaskFinder.php +++ b/app/Model/TaskFinder.php @@ -128,6 +128,7 @@ class TaskFinder extends Base User::TABLE.'.username AS assignee_username', User::TABLE.'.name AS assignee_name', User::TABLE.'.email AS assignee_email', + User::TABLE.'.avatar_path AS assignee_avatar_path', Category::TABLE.'.name AS category_name', Category::TABLE.'.description AS category_description', Column::TABLE.'.title AS column_name', @@ -137,6 +138,7 @@ class TaskFinder extends Base Project::TABLE.'.name AS project_name' ) ->join(User::TABLE, 'id', 'owner_id', Task::TABLE) + ->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id') ->join(Category::TABLE, 'id', 'category_id', Task::TABLE) ->join(Column::TABLE, 'id', 'column_id', Task::TABLE) ->join(Swimlane::TABLE, 'id', 'swimlane_id', Task::TABLE) @@ -362,6 +364,27 @@ class TaskFinder extends Base } /** + * Get iCal query + * + * @access public + * @return \PicoDb\Table + */ + public function getICalQuery() + { + return $this->db->table(Task::TABLE) + ->left(User::TABLE, 'ua', 'id', Task::TABLE, 'owner_id') + ->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id') + ->columns( + Task::TABLE.'.*', + 'ua.email AS assignee_email', + 'ua.name AS assignee_name', + 'ua.username AS assignee_username', + 'uc.email AS creator_email', + 'uc.username AS creator_username' + ); + } + + /** * Count all tasks for a given project and status * * @access public diff --git a/app/Model/User.php b/app/Model/User.php index 0e11422b..57993002 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -283,12 +283,7 @@ class User extends Base { $this->prepare($values); $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); - - // If the user is connected refresh his session - if ($this->userSession->getId() == $values['id']) { - $this->userSession->initialize($this->getById($this->userSession->getId())); - } - + $this->userSession->refresh($values['id']); return $result; } @@ -325,6 +320,8 @@ class User extends Base */ public function remove($user_id) { + $this->avatarFile->remove($user_id); + return $this->db->transaction(function (Database $db) use ($user_id) { // All assigned tasks are now unassigned (no foreign key) diff --git a/app/Model/UserFilter.php b/app/Model/UserFilter.php deleted file mode 100644 index ff546e96..00000000 --- a/app/Model/UserFilter.php +++ /dev/null @@ -1,80 +0,0 @@ -<?php - -namespace Kanboard\Model; - -/** - * User Filter - * - * @package model - * @author Frederic Guillot - */ -class UserFilter extends Base -{ - /** - * Search query - * - * @access private - * @var string - */ - private $input; - - /** - * Query - * - * @access protected - * @var \PicoDb\Table - */ - protected $query; - - /** - * Initialize filter - * - * @access public - * @param string $input - * @return UserFilter - */ - public function create($input) - { - $this->query = $this->db->table(User::TABLE); - $this->input = $input; - return $this; - } - - /** - * Filter users by name or username - * - * @access public - * @return UserFilter - */ - public function filterByUsernameOrByName() - { - $this->query->beginOr() - ->ilike('username', '%'.$this->input.'%') - ->ilike('name', '%'.$this->input.'%') - ->closeOr(); - - return $this; - } - - /** - * Get all results of the filter - * - * @access public - * @return array - */ - public function findAll() - { - return $this->query->findAll(); - } - - /** - * Get the PicoDb query - * - * @access public - * @return \PicoDb\Table - */ - public function getQuery() - { - return $this->query; - } -} diff --git a/app/Model/UserNotification.php b/app/Model/UserNotification.php index e8a967ac..7795da2e 100644 --- a/app/Model/UserNotification.php +++ b/app/Model/UserNotification.php @@ -117,23 +117,20 @@ class UserNotification extends Base */ public function saveSettings($user_id, array $values) { - $this->db->startTransaction(); + $types = empty($values['notification_types']) ? array() : array_keys($values['notification_types']); - if (isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1) { + if (! empty($types)) { $this->enableNotification($user_id); - - $filter = empty($values['notifications_filter']) ? UserNotificationFilter::FILTER_BOTH : $values['notifications_filter']; - $projects = empty($values['notification_projects']) ? array() : array_keys($values['notification_projects']); - $types = empty($values['notification_types']) ? array() : array_keys($values['notification_types']); - - $this->userNotificationFilter->saveFilter($user_id, $filter); - $this->userNotificationFilter->saveSelectedProjects($user_id, $projects); - $this->userNotificationType->saveSelectedTypes($user_id, $types); } else { $this->disableNotification($user_id); } - $this->db->closeTransaction(); + $filter = empty($values['notifications_filter']) ? UserNotificationFilter::FILTER_BOTH : $values['notifications_filter']; + $project_ids = empty($values['notification_projects']) ? array() : array_keys($values['notification_projects']); + + $this->userNotificationFilter->saveFilter($user_id, $filter); + $this->userNotificationFilter->saveSelectedProjects($user_id, $project_ids); + $this->userNotificationType->saveSelectedTypes($user_id, $types); } /** diff --git a/app/Model/UserNotificationFilter.php b/app/Model/UserNotificationFilter.php index d4afd278..780ddfc7 100644 --- a/app/Model/UserNotificationFilter.php +++ b/app/Model/UserNotificationFilter.php @@ -61,10 +61,11 @@ class UserNotificationFilter extends Base * @access public * @param integer $user_id * @param string $filter + * @return boolean */ public function saveFilter($user_id, $filter) { - $this->db->table(User::TABLE)->eq('id', $user_id)->update(array( + return $this->db->table(User::TABLE)->eq('id', $user_id)->update(array( 'notifications_filter' => $filter, )); } @@ -87,17 +88,21 @@ class UserNotificationFilter extends Base * @access public * @param integer $user_id * @param integer[] $project_ids + * @return boolean */ public function saveSelectedProjects($user_id, array $project_ids) { + $results = array(); $this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->remove(); foreach ($project_ids as $project_id) { - $this->db->table(self::PROJECT_TABLE)->insert(array( + $results[] = $this->db->table(self::PROJECT_TABLE)->insert(array( 'user_id' => $user_id, 'project_id' => $project_id, )); } + + return !in_array(false, $results, true); } /** diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 9bfe6649..934b063f 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,23 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 108; +const VERSION = 110; + +function version_110(PDO $pdo) +{ + $pdo->exec("ALTER TABLE user_has_notifications DROP FOREIGN KEY `user_has_notifications_ibfk_1`"); + $pdo->exec("ALTER TABLE user_has_notifications DROP FOREIGN KEY `user_has_notifications_ibfk_2`"); + $pdo->exec("DROP INDEX `project_id` ON user_has_notifications"); + $pdo->exec("ALTER TABLE user_has_notifications DROP KEY `user_id`"); + $pdo->exec("CREATE UNIQUE INDEX `user_has_notifications_unique_idx` ON `user_has_notifications` (`user_id`, `project_id`)"); + $pdo->exec("ALTER TABLE user_has_notifications ADD CONSTRAINT user_has_notifications_ibfk_1 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE"); + $pdo->exec("ALTER TABLE user_has_notifications ADD CONSTRAINT user_has_notifications_ibfk_2 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE"); +} + +function version_109(PDO $pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN avatar_path VARCHAR(255)"); +} function version_108(PDO $pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index 28c563de..3ef49498 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,12 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 88; +const VERSION = 89; + +function version_89(PDO $pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN avatar_path VARCHAR(255)"); +} function version_88(PDO $pdo) { diff --git a/app/Schema/Sql/mysql.sql b/app/Schema/Sql/mysql.sql index b5620400..ce2374f0 100644 --- a/app/Schema/Sql/mysql.sql +++ b/app/Schema/Sql/mysql.sql @@ -273,6 +273,8 @@ CREATE TABLE `project_has_metadata` ( `project_id` int(11) NOT NULL, `name` varchar(50) NOT NULL, `value` varchar(255) DEFAULT '', + `changed_by` int(11) NOT NULL DEFAULT '0', + `changed_on` int(11) NOT NULL DEFAULT '0', UNIQUE KEY `project_id` (`project_id`,`name`), CONSTRAINT `project_has_metadata_ibfk_1` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -357,6 +359,8 @@ DROP TABLE IF EXISTS `settings`; CREATE TABLE `settings` ( `option` varchar(100) NOT NULL, `value` varchar(255) DEFAULT '', + `changed_by` int(11) NOT NULL DEFAULT '0', + `changed_on` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`option`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; @@ -469,6 +473,8 @@ CREATE TABLE `task_has_metadata` ( `task_id` int(11) NOT NULL, `name` varchar(50) NOT NULL, `value` varchar(255) DEFAULT '', + `changed_by` int(11) NOT NULL DEFAULT '0', + `changed_on` int(11) NOT NULL DEFAULT '0', UNIQUE KEY `task_id` (`task_id`,`name`), CONSTRAINT `task_has_metadata_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -548,6 +554,8 @@ CREATE TABLE `user_has_metadata` ( `user_id` int(11) NOT NULL, `name` varchar(50) NOT NULL, `value` varchar(255) DEFAULT '', + `changed_by` int(11) NOT NULL DEFAULT '0', + `changed_on` int(11) NOT NULL DEFAULT '0', UNIQUE KEY `user_id` (`user_id`,`name`), CONSTRAINT `user_has_metadata_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -570,8 +578,8 @@ DROP TABLE IF EXISTS `user_has_notifications`; CREATE TABLE `user_has_notifications` ( `user_id` int(11) NOT NULL, `project_id` int(11) NOT NULL, - UNIQUE KEY `project_id` (`project_id`,`user_id`), - KEY `user_id` (`user_id`), + UNIQUE KEY `user_has_notifications_unique_idx` (`user_id`,`project_id`), + KEY `user_has_notifications_ibfk_2` (`project_id`), CONSTRAINT `user_has_notifications_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, CONSTRAINT `user_has_notifications_ibfk_2` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -615,6 +623,7 @@ CREATE TABLE `users` ( `gitlab_id` int(11) DEFAULT NULL, `role` varchar(25) NOT NULL DEFAULT 'app-user', `is_active` tinyint(1) DEFAULT '1', + `avatar_path` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `users_username_idx` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -638,7 +647,7 @@ CREATE TABLE `users` ( LOCK TABLES `settings` WRITE; /*!40000 ALTER TABLE `settings` DISABLE KEYS */; -INSERT INTO `settings` VALUES ('api_token','cd9c46c6bdaa6afc49b3385dabe0b78c059bc124b1f72c2f47c9ca604cf1'),('application_currency','USD'),('application_date_format','m/d/Y'),('application_language','en_US'),('application_stylesheet',''),('application_timezone','UTC'),('application_url',''),('board_columns',''),('board_highlight_period','172800'),('board_private_refresh_interval','10'),('board_public_refresh_interval','60'),('calendar_project_tasks','date_started'),('calendar_user_subtasks_time_tracking','0'),('calendar_user_tasks','date_started'),('cfd_include_closed_tasks','1'),('default_color','yellow'),('integration_gravatar','0'),('password_reset','1'),('project_categories',''),('subtask_restriction','0'),('subtask_time_tracking','1'),('webhook_token','32387b121de8fe6031a6b71b7b1b9cae411a909539aa9d494cf69ac5f2ee'),('webhook_url',''); +INSERT INTO `settings` VALUES ('api_token','9c55053ae1d523893efc820e2e8338c4cf47f5c6c2c26861fec637eba62b',0,0),('application_currency','USD',0,0),('application_date_format','m/d/Y',0,0),('application_language','en_US',0,0),('application_stylesheet','',0,0),('application_timezone','UTC',0,0),('application_url','',0,0),('board_columns','',0,0),('board_highlight_period','172800',0,0),('board_private_refresh_interval','10',0,0),('board_public_refresh_interval','60',0,0),('calendar_project_tasks','date_started',0,0),('calendar_user_subtasks_time_tracking','0',0,0),('calendar_user_tasks','date_started',0,0),('cfd_include_closed_tasks','1',0,0),('default_color','yellow',0,0),('integration_gravatar','0',0,0),('password_reset','1',0,0),('project_categories','',0,0),('subtask_restriction','0',0,0),('subtask_time_tracking','1',0,0),('webhook_token','aaed762f4f6b0860902af0e2a87e5ad3427d24ff9e3ce8a2e0b005b58dfc',0,0),('webhook_url','',0,0); /*!40000 ALTER TABLE `settings` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; @@ -667,4 +676,4 @@ UNLOCK TABLES; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$tyByY1dfUO9S.2wpJcSMEO4UU9H.yCwf/pmzo430DM2C4QZ/K3Kt2', 'app-admin');INSERT INTO schema_version VALUES ('107'); +INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$e.SftITKuBvXeNbxtmTKS.KAbIy4Mx09t254BAiEAuWOxkuS4xfLG', 'app-admin');INSERT INTO schema_version VALUES ('110'); diff --git a/app/Schema/Sql/postgres.sql b/app/Schema/Sql/postgres.sql index c613ddb4..48a269d3 100644 --- a/app/Schema/Sql/postgres.sql +++ b/app/Schema/Sql/postgres.sql @@ -512,7 +512,9 @@ CREATE TABLE project_has_groups ( CREATE TABLE project_has_metadata ( project_id integer NOT NULL, name character varying(50) NOT NULL, - value character varying(255) DEFAULT ''::character varying + value character varying(255) DEFAULT ''::character varying, + changed_by integer DEFAULT 0 NOT NULL, + changed_on integer DEFAULT 0 NOT NULL ); @@ -652,7 +654,9 @@ CREATE TABLE schema_version ( CREATE TABLE settings ( option character varying(100) NOT NULL, - value character varying(255) DEFAULT ''::character varying + value character varying(255) DEFAULT ''::character varying, + changed_by integer DEFAULT 0 NOT NULL, + changed_on integer DEFAULT 0 NOT NULL ); @@ -847,7 +851,9 @@ ALTER SEQUENCE task_has_links_id_seq OWNED BY task_has_links.id; CREATE TABLE task_has_metadata ( task_id integer NOT NULL, name character varying(50) NOT NULL, - value character varying(255) DEFAULT ''::character varying + value character varying(255) DEFAULT ''::character varying, + changed_by integer DEFAULT 0 NOT NULL, + changed_on integer DEFAULT 0 NOT NULL ); @@ -969,7 +975,9 @@ ALTER SEQUENCE transitions_id_seq OWNED BY transitions.id; CREATE TABLE user_has_metadata ( user_id integer NOT NULL, name character varying(50) NOT NULL, - value character varying(255) DEFAULT ''::character varying + value character varying(255) DEFAULT ''::character varying, + changed_by integer DEFAULT 0 NOT NULL, + changed_on integer DEFAULT 0 NOT NULL ); @@ -1070,7 +1078,8 @@ CREATE TABLE users ( lock_expiration_date bigint DEFAULT 0, gitlab_id integer, role character varying(25) DEFAULT 'app-user'::character varying NOT NULL, - is_active boolean DEFAULT true + is_active boolean DEFAULT true, + avatar_path character varying(255) ); @@ -2141,29 +2150,29 @@ SET search_path = public, pg_catalog; -- Data for Name: settings; Type: TABLE DATA; Schema: public; Owner: postgres -- -INSERT INTO settings (option, value) VALUES ('board_highlight_period', '172800'); -INSERT INTO settings (option, value) VALUES ('board_public_refresh_interval', '60'); -INSERT INTO settings (option, value) VALUES ('board_private_refresh_interval', '10'); -INSERT INTO settings (option, value) VALUES ('board_columns', ''); -INSERT INTO settings (option, value) VALUES ('webhook_token', 'c7caaf8f87ad391800e3989d7abfd98a6066a6f801fc151012bb5c4ee3cb'); -INSERT INTO settings (option, value) VALUES ('api_token', 'b0a6f56fe236fc9639fc6914e92365aa627d95cd790aa7e0c5a3ebebf844'); -INSERT INTO settings (option, value) VALUES ('application_language', 'en_US'); -INSERT INTO settings (option, value) VALUES ('application_timezone', 'UTC'); -INSERT INTO settings (option, value) VALUES ('application_url', ''); -INSERT INTO settings (option, value) VALUES ('application_date_format', 'm/d/Y'); -INSERT INTO settings (option, value) VALUES ('project_categories', ''); -INSERT INTO settings (option, value) VALUES ('subtask_restriction', '0'); -INSERT INTO settings (option, value) VALUES ('application_stylesheet', ''); -INSERT INTO settings (option, value) VALUES ('application_currency', 'USD'); -INSERT INTO settings (option, value) VALUES ('integration_gravatar', '0'); -INSERT INTO settings (option, value) VALUES ('calendar_user_subtasks_time_tracking', '0'); -INSERT INTO settings (option, value) VALUES ('calendar_user_tasks', 'date_started'); -INSERT INTO settings (option, value) VALUES ('calendar_project_tasks', 'date_started'); -INSERT INTO settings (option, value) VALUES ('webhook_url', ''); -INSERT INTO settings (option, value) VALUES ('default_color', 'yellow'); -INSERT INTO settings (option, value) VALUES ('subtask_time_tracking', '1'); -INSERT INTO settings (option, value) VALUES ('cfd_include_closed_tasks', '1'); -INSERT INTO settings (option, value) VALUES ('password_reset', '1'); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_highlight_period', '172800', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_public_refresh_interval', '60', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_private_refresh_interval', '10', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_columns', '', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('webhook_token', '67545fef6a0a3f43d60c7d57632d6e4af9930f064c12e72266b1c9b42381', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('api_token', 'c16b1c5896b258409a5eb344152b5b33c8ef4c58902bc543fc1348c37975', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_language', 'en_US', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_timezone', 'UTC', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_url', '', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_date_format', 'm/d/Y', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('project_categories', '', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('subtask_restriction', '0', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_stylesheet', '', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_currency', 'USD', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('integration_gravatar', '0', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('calendar_user_subtasks_time_tracking', '0', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('calendar_user_tasks', 'date_started', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('calendar_project_tasks', 'date_started', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('webhook_url', '', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('default_color', 'yellow', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('subtask_time_tracking', '1', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('cfd_include_closed_tasks', '1', 0, 0); +INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('password_reset', '1', 0, 0); -- @@ -2211,4 +2220,4 @@ SELECT pg_catalog.setval('links_id_seq', 11, true); -- PostgreSQL database dump complete -- -INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$tyByY1dfUO9S.2wpJcSMEO4UU9H.yCwf/pmzo430DM2C4QZ/K3Kt2', 'app-admin');INSERT INTO schema_version VALUES ('87'); +INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$e.SftITKuBvXeNbxtmTKS.KAbIy4Mx09t254BAiEAuWOxkuS4xfLG', 'app-admin');INSERT INTO schema_version VALUES ('89'); diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index c6f60332..9ded7ed9 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,12 @@ use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; use PDO; -const VERSION = 100; +const VERSION = 101; + +function version_101(PDO $pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN avatar_path TEXT"); +} function version_100(PDO $pdo) { diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php index d59ffd9e..776e65d5 100644 --- a/app/ServiceProvider/AuthenticationProvider.php +++ b/app/ServiceProvider/AuthenticationProvider.php @@ -125,6 +125,7 @@ class AuthenticationProvider implements ServiceProviderInterface $acl->add('Board', 'readonly', Role::APP_PUBLIC); $acl->add('Ical', '*', Role::APP_PUBLIC); $acl->add('Feed', '*', Role::APP_PUBLIC); + $acl->add('AvatarFile', 'show', Role::APP_PUBLIC); $acl->add('Config', '*', Role::APP_ADMIN); $acl->add('Currency', '*', Role::APP_ADMIN); diff --git a/app/ServiceProvider/AvatarProvider.php b/app/ServiceProvider/AvatarProvider.php index 73d37d5c..aac4fcab 100644 --- a/app/ServiceProvider/AvatarProvider.php +++ b/app/ServiceProvider/AvatarProvider.php @@ -6,6 +6,7 @@ use Pimple\Container; use Pimple\ServiceProviderInterface; use Kanboard\Core\User\Avatar\AvatarManager; use Kanboard\User\Avatar\GravatarProvider; +use Kanboard\User\Avatar\AvatarFileProvider; use Kanboard\User\Avatar\LetterAvatarProvider; /** @@ -28,6 +29,7 @@ class AvatarProvider implements ServiceProviderInterface $container['avatarManager'] = new AvatarManager; $container['avatarManager']->register(new LetterAvatarProvider($container)); $container['avatarManager']->register(new GravatarProvider($container)); + $container['avatarManager']->register(new AvatarFileProvider($container)); return $container; } } diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index b883c905..18c1d578 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -24,6 +24,7 @@ class ClassProvider implements ServiceProviderInterface 'Model' => array( 'Action', 'ActionParameter', + 'AvatarFile', 'Board', 'Category', 'Color', @@ -48,9 +49,7 @@ class ClassProvider implements ServiceProviderInterface 'ProjectNotification', 'ProjectMetadata', 'ProjectGroupRole', - 'ProjectGroupRoleFilter', 'ProjectUserRole', - 'ProjectUserRoleFilter', 'RememberMeSession', 'Subtask', 'SubtaskTimeTracking', @@ -62,7 +61,6 @@ class ClassProvider implements ServiceProviderInterface 'TaskExternalLink', 'TaskFinder', 'TaskFile', - 'TaskFilter', 'TaskLink', 'TaskModification', 'TaskPermission', @@ -78,15 +76,6 @@ class ClassProvider implements ServiceProviderInterface 'UserUnreadNotification', 'UserMetadata', ), - 'Formatter' => array( - 'TaskFilterGanttFormatter', - 'TaskFilterAutoCompleteFormatter', - 'TaskFilterCalendarFormatter', - 'TaskFilterICalendarFormatter', - 'ProjectGanttFormatter', - 'UserFilterAutoCompleteFormatter', - 'GroupAutoCompleteFormatter', - ), 'Validator' => array( 'ActionValidator', 'AuthValidator', diff --git a/app/ServiceProvider/DatabaseProvider.php b/app/ServiceProvider/DatabaseProvider.php index 8cede8af..d323807d 100644 --- a/app/ServiceProvider/DatabaseProvider.php +++ b/app/ServiceProvider/DatabaseProvider.php @@ -44,8 +44,8 @@ class DatabaseProvider implements ServiceProviderInterface if ($db->schema()->check(\Schema\VERSION)) { return $db; } else { - $errors = $db->getLogMessages(); - throw new RuntimeException('Unable to migrate database schema: '.(isset($errors[0]) ? $errors[0] : 'Unknown error')); + $messages = $db->getLogMessages(); + throw new RuntimeException('Unable to run SQL migrations: '.implode(', ', $messages).' (You may have to fix it manually)'); } } diff --git a/app/ServiceProvider/FilterProvider.php b/app/ServiceProvider/FilterProvider.php new file mode 100644 index 00000000..f3918d77 --- /dev/null +++ b/app/ServiceProvider/FilterProvider.php @@ -0,0 +1,174 @@ +<?php + +namespace Kanboard\ServiceProvider; + +use Kanboard\Core\Filter\LexerBuilder; +use Kanboard\Core\Filter\QueryBuilder; +use Kanboard\Filter\ProjectActivityCreationDateFilter; +use Kanboard\Filter\ProjectActivityCreatorFilter; +use Kanboard\Filter\ProjectActivityProjectNameFilter; +use Kanboard\Filter\ProjectActivityTaskStatusFilter; +use Kanboard\Filter\ProjectActivityTaskTitleFilter; +use Kanboard\Filter\TaskAssigneeFilter; +use Kanboard\Filter\TaskCategoryFilter; +use Kanboard\Filter\TaskColorFilter; +use Kanboard\Filter\TaskColumnFilter; +use Kanboard\Filter\TaskCommentFilter; +use Kanboard\Filter\TaskCreationDateFilter; +use Kanboard\Filter\TaskCreatorFilter; +use Kanboard\Filter\TaskDescriptionFilter; +use Kanboard\Filter\TaskDueDateFilter; +use Kanboard\Filter\TaskIdFilter; +use Kanboard\Filter\TaskLinkFilter; +use Kanboard\Filter\TaskModificationDateFilter; +use Kanboard\Filter\TaskProjectFilter; +use Kanboard\Filter\TaskReferenceFilter; +use Kanboard\Filter\TaskStatusFilter; +use Kanboard\Filter\TaskSubtaskAssigneeFilter; +use Kanboard\Filter\TaskSwimlaneFilter; +use Kanboard\Filter\TaskTitleFilter; +use Kanboard\Model\Project; +use Kanboard\Model\ProjectGroupRole; +use Kanboard\Model\ProjectUserRole; +use Kanboard\Model\User; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Filter Provider + * + * @package serviceProvider + * @author Frederic Guillot + */ +class FilterProvider implements ServiceProviderInterface +{ + /** + * Register providers + * + * @access public + * @param \Pimple\Container $container + * @return \Pimple\Container + */ + public function register(Container $container) + { + $this->createUserFilter($container); + $this->createProjectFilter($container); + $this->createTaskFilter($container); + return $container; + } + + public function createUserFilter(Container $container) + { + $container['userQuery'] = $container->factory(function ($c) { + $builder = new QueryBuilder(); + $builder->withQuery($c['db']->table(User::TABLE)); + return $builder; + }); + + return $container; + } + + public function createProjectFilter(Container $container) + { + $container['projectGroupRoleQuery'] = $container->factory(function ($c) { + $builder = new QueryBuilder(); + $builder->withQuery($c['db']->table(ProjectGroupRole::TABLE)); + return $builder; + }); + + $container['projectUserRoleQuery'] = $container->factory(function ($c) { + $builder = new QueryBuilder(); + $builder->withQuery($c['db']->table(ProjectUserRole::TABLE)); + return $builder; + }); + + $container['projectQuery'] = $container->factory(function ($c) { + $builder = new QueryBuilder(); + $builder->withQuery($c['db']->table(Project::TABLE)); + return $builder; + }); + + $container['projectActivityLexer'] = $container->factory(function ($c) { + $builder = new LexerBuilder(); + $builder + ->withQuery($c['projectActivity']->getQuery()) + ->withFilter(new ProjectActivityTaskTitleFilter(), true) + ->withFilter(new ProjectActivityTaskStatusFilter()) + ->withFilter(new ProjectActivityProjectNameFilter()) + ->withFilter(ProjectActivityCreationDateFilter::getInstance() + ->setDateParser($c['dateParser']) + ) + ->withFilter(ProjectActivityCreatorFilter::getInstance() + ->setCurrentUserId($c['userSession']->getId()) + ) + ; + + return $builder; + }); + + $container['projectActivityQuery'] = $container->factory(function ($c) { + $builder = new QueryBuilder(); + $builder->withQuery($c['projectActivity']->getQuery()); + + return $builder; + }); + + return $container; + } + + public function createTaskFilter(Container $container) + { + $container['taskQuery'] = $container->factory(function ($c) { + $builder = new QueryBuilder(); + $builder->withQuery($c['taskFinder']->getExtendedQuery()); + return $builder; + }); + + $container['taskLexer'] = $container->factory(function ($c) { + $builder = new LexerBuilder(); + + $builder + ->withQuery($c['taskFinder']->getExtendedQuery()) + ->withFilter(TaskAssigneeFilter::getInstance() + ->setCurrentUserId($c['userSession']->getId()) + ) + ->withFilter(new TaskCategoryFilter()) + ->withFilter(TaskColorFilter::getInstance() + ->setColorModel($c['color']) + ) + ->withFilter(new TaskColumnFilter()) + ->withFilter(new TaskCommentFilter()) + ->withFilter(TaskCreationDateFilter::getInstance() + ->setDateParser($c['dateParser']) + ) + ->withFilter(TaskCreatorFilter::getInstance() + ->setCurrentUserId($c['userSession']->getId()) + ) + ->withFilter(new TaskDescriptionFilter()) + ->withFilter(TaskDueDateFilter::getInstance() + ->setDateParser($c['dateParser']) + ) + ->withFilter(new TaskIdFilter()) + ->withFilter(TaskLinkFilter::getInstance() + ->setDatabase($c['db']) + ) + ->withFilter(TaskModificationDateFilter::getInstance() + ->setDateParser($c['dateParser']) + ) + ->withFilter(new TaskProjectFilter()) + ->withFilter(new TaskReferenceFilter()) + ->withFilter(new TaskStatusFilter()) + ->withFilter(TaskSubtaskAssigneeFilter::getInstance() + ->setCurrentUserId($c['userSession']->getId()) + ->setDatabase($c['db']) + ) + ->withFilter(new TaskSwimlaneFilter()) + ->withFilter(new TaskTitleFilter(), true) + ; + + return $builder; + }); + + return $container; + } +} diff --git a/app/ServiceProvider/HelperProvider.php b/app/ServiceProvider/HelperProvider.php index 37be5a05..bf3956a2 100644 --- a/app/ServiceProvider/HelperProvider.php +++ b/app/ServiceProvider/HelperProvider.php @@ -13,12 +13,14 @@ class HelperProvider implements ServiceProviderInterface { $container['helper'] = new Helper($container); $container['helper']->register('app', '\Kanboard\Helper\AppHelper'); + $container['helper']->register('calendar', '\Kanboard\Helper\CalendarHelper'); $container['helper']->register('asset', '\Kanboard\Helper\AssetHelper'); $container['helper']->register('board', '\Kanboard\Helper\BoardHelper'); $container['helper']->register('dt', '\Kanboard\Helper\DateHelper'); $container['helper']->register('file', '\Kanboard\Helper\FileHelper'); $container['helper']->register('form', '\Kanboard\Helper\FormHelper'); $container['helper']->register('hook', '\Kanboard\Helper\HookHelper'); + $container['helper']->register('ical', '\Kanboard\Helper\ICalHelper'); $container['helper']->register('layout', '\Kanboard\Helper\LayoutHelper'); $container['helper']->register('model', '\Kanboard\Helper\ModelHelper'); $container['helper']->register('subtask', '\Kanboard\Helper\SubtaskHelper'); @@ -27,6 +29,8 @@ class HelperProvider implements ServiceProviderInterface $container['helper']->register('url', '\Kanboard\Helper\UrlHelper'); $container['helper']->register('user', '\Kanboard\Helper\UserHelper'); $container['helper']->register('avatar', '\Kanboard\Helper\AvatarHelper'); + $container['helper']->register('projectHeader', '\Kanboard\Helper\ProjectHeaderHelper'); + $container['helper']->register('projectActivity', '\Kanboard\Helper\ProjectActivityHelper'); $container['template'] = new Template($container['helper']); diff --git a/app/ServiceProvider/RouteProvider.php b/app/ServiceProvider/RouteProvider.php index 0e7548d4..30d23a51 100644 --- a/app/ServiceProvider/RouteProvider.php +++ b/app/ServiceProvider/RouteProvider.php @@ -42,7 +42,7 @@ class RouteProvider implements ServiceProviderInterface // Search routes $container['route']->addRoute('search', 'search', 'index'); - $container['route']->addRoute('search/:search', 'search', 'index'); + $container['route']->addRoute('search/activity', 'search', 'activity'); // ProjectCreation routes $container['route']->addRoute('project/create', 'ProjectCreation', 'create'); @@ -62,6 +62,7 @@ class RouteProvider implements ServiceProviderInterface $container['route']->addRoute('project/:project_id/enable', 'project', 'enable'); $container['route']->addRoute('project/:project_id/permissions', 'ProjectPermission', 'index'); $container['route']->addRoute('project/:project_id/import', 'taskImport', 'step1'); + $container['route']->addRoute('project/:project_id/activity', 'activity', 'project'); // Project Overview $container['route']->addRoute('project/:project_id/overview', 'ProjectOverview', 'show'); diff --git a/app/Template/activity/filter_dropdown.php b/app/Template/activity/filter_dropdown.php new file mode 100644 index 00000000..8d7a7de3 --- /dev/null +++ b/app/Template/activity/filter_dropdown.php @@ -0,0 +1,14 @@ +<div class="dropdown"> + <a href="#" class="dropdown-menu dropdown-menu-link-icon" title="<?= t('Default filters') ?>"><i class="fa fa-filter fa-fw"></i><i class="fa fa-caret-down"></i></a> + <ul> + <li><a href="#" class="filter-helper filter-reset" data-filter="" title="<?= t('Keyboard shortcut: "%s"', 'r') ?>"><?= t('Reset filters') ?></a></li> + <li><a href="#" class="filter-helper" data-filter="creator:me"><?= t('My activities') ?></a></li> + <li><a href="#" class="filter-helper" data-filter="created:<=<?= date('Y-m-d', strtotime('yesterday')) ?>"><?= t('Activity until yesterday') ?></a></li> + <li><a href="#" class="filter-helper" data-filter="created:<=<?= date('Y-m-d')?>"><?= t('Activity until today') ?></a></li> + <li><a href="#" class="filter-helper" data-filter="status:closed"><?= t('Closed tasks') ?></a></li> + <li><a href="#" class="filter-helper" data-filter="status:open"><?= t('Open tasks') ?></a></li> + <li> + <?= $this->url->doc(t('View advanced search syntax'), 'search') ?> + </li> + </ul> +</div>
\ No newline at end of file diff --git a/app/Template/activity/project.php b/app/Template/activity/project.php index ba6d6629..176d9b99 100644 --- a/app/Template/activity/project.php +++ b/app/Template/activity/project.php @@ -1,40 +1,14 @@ <section id="main"> - <div class="page-header"> + <?= $this->projectHeader->render($project, 'Analytic', $this->app->getRouterAction()) ?> + + <?php if ($project['is_public']): ?> + <div class="menu-inline pull-right"> <ul> - <li> - <span class="dropdown"> - <span> - <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> - <ul> - <?= $this->render('project/dropdown', array('project' => $project)) ?> - </ul> - </span> - </span> - </li> - <li> - <i class="fa fa-th fa-fw"></i> - <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?> - </li> - <li> - <i class="fa fa-calendar fa-fw"></i> - <?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?> - </li> - <?php if ($this->user->hasProjectAccess('ProjectEdit', 'edit', $project['id'])): ?> - <li> - <i class="fa fa-cog fa-fw"></i> - <?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $project['id'])) ?> - </li> - <?php endif ?> - <li> - <i class="fa fa-folder fa-fw"></i> - <?= $this->url->link(t('All projects'), 'project', 'index') ?> - </li> - <?php if ($project['is_public']): ?> - <li><i class="fa fa-rss-square fa-fw"></i><?= $this->url->link(t('RSS feed'), 'feed', 'project', array('token' => $project['token']), false, '', '', true) ?></li> - <li><i class="fa fa-calendar fa-fw"></i><?= $this->url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token'])) ?></li> - <?php endif ?> + <li><i class="fa fa-rss-square fa-fw"></i><?= $this->url->link(t('RSS feed'), 'feed', 'project', array('token' => $project['token']), false, '', '', true) ?></li> + <li><i class="fa fa-calendar fa-fw"></i><?= $this->url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token'])) ?></li> </ul> </div> + <?php endif ?> <?= $this->render('event/events', array('events' => $events)) ?> </section>
\ No newline at end of file diff --git a/app/Template/analytic/layout.php b/app/Template/analytic/layout.php index f1dba552..35793cbb 100644 --- a/app/Template/analytic/layout.php +++ b/app/Template/analytic/layout.php @@ -1,36 +1,5 @@ <section id="main"> - <div class="page-header"> - <ul> - <li> - <span class="dropdown"> - <span> - <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> - <ul> - <?= $this->render('project/dropdown', array('project' => $project)) ?> - </ul> - </span> - </span> - </li> - <li> - <i class="fa fa-th fa-fw"></i> - <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?> - </li> - <li> - <i class="fa fa-calendar fa-fw"></i> - <?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?> - </li> - <?php if ($this->user->hasProjectAccess('ProjectEdit', 'edit', $project['id'])): ?> - <li> - <i class="fa fa-cog fa-fw"></i> - <?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $project['id'])) ?> - </li> - <?php endif ?> - <li> - <i class="fa fa-folder fa-fw"></i> - <?= $this->url->link(t('All projects'), 'project', 'index') ?> - </li> - </ul> - </div> + <?= $this->projectHeader->render($project, 'Listing', 'show') ?> <section class="sidebar-container"> <?= $this->render($sidebar_template, array('project' => $project)) ?> diff --git a/app/Template/app/layout.php b/app/Template/app/layout.php index 200cb0d7..2a32ac02 100644 --- a/app/Template/app/layout.php +++ b/app/Template/app/layout.php @@ -1,5 +1,5 @@ <section id="main"> - <div class="page-header page-header-mobile"> + <div class="page-header"> <ul> <?php if ($this->user->hasAccess('ProjectCreation', 'create')): ?> <li> diff --git a/app/Template/avatar_file/show.php b/app/Template/avatar_file/show.php new file mode 100644 index 00000000..266a2ccb --- /dev/null +++ b/app/Template/avatar_file/show.php @@ -0,0 +1,20 @@ +<div class="page-header"> + <h2><?= t('Avatar') ?></h2> +</div> + +<?= $this->avatar->render($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], '') ?> + +<form method="post" enctype="multipart/form-data" action="<?= $this->url->href('AvatarFile', 'upload', array('user_id' => $user['id'])) ?>"> + <?= $this->form->csrf() ?> + <?= $this->form->label(t('Upload my avatar image'), 'avatar') ?> + <?= $this->form->file('avatar') ?> + + <div class="form-actions"> + <?php if (! empty($user['avatar_path'])): ?> + <?= $this->url->link(t('Remove my image'), 'AvatarFile', 'remove', array('user_id' => $user['id']), true, 'btn btn-red') ?> + <?php endif ?> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> + </div> +</form> diff --git a/app/Template/board/task_avatar.php b/app/Template/board/task_avatar.php index 5630c190..39f6b54d 100644 --- a/app/Template/board/task_avatar.php +++ b/app/Template/board/task_avatar.php @@ -12,6 +12,7 @@ $task['assignee_username'], $task['assignee_name'], $task['assignee_email'], + $task['assignee_avatar_path'], 'avatar-inline' ) ?> </span> diff --git a/app/Template/board/task_footer.php b/app/Template/board/task_footer.php index a8203739..a9d381a3 100644 --- a/app/Template/board/task_footer.php +++ b/app/Template/board/task_footer.php @@ -76,6 +76,8 @@ <i class="fa fa-flag flag-milestone"></i> </span> <?php endif ?> + + <?= $this->hook->render('template:board:task:icons', array('task' => $task)) ?> <?= $this->task->formatPriority($project, $task) ?> diff --git a/app/Template/board/view_private.php b/app/Template/board/view_private.php index b5e38c66..13702273 100644 --- a/app/Template/board/view_private.php +++ b/app/Template/board/view_private.php @@ -1,19 +1,12 @@ <section id="main"> - <?= $this->render('project_header/header', array( - 'project' => $project, - 'filters' => $filters, - 'categories_list' => $categories_list, - 'users_list' => $users_list, - 'custom_filters_list' => $custom_filters_list, - 'is_board' => true, - )) ?> + <?= $this->projectHeader->render($project, 'Board', 'show', true) ?> <?= $this->render('board/table_container', array( - 'project' => $project, - 'swimlanes' => $swimlanes, - 'board_private_refresh_interval' => $board_private_refresh_interval, - 'board_highlight_period' => $board_highlight_period, + 'project' => $project, + 'swimlanes' => $swimlanes, + 'board_private_refresh_interval' => $board_private_refresh_interval, + 'board_highlight_period' => $board_highlight_period, )) ?> </section> diff --git a/app/Template/calendar/show.php b/app/Template/calendar/show.php index 7085b51e..f00e810b 100644 --- a/app/Template/calendar/show.php +++ b/app/Template/calendar/show.php @@ -1,9 +1,5 @@ <section id="main"> - <?= $this->render('project_header/header', array( - 'project' => $project, - 'filters' => $filters, - )) ?> - + <?= $this->projectHeader->render($project, 'Calendar', 'show') ?> <div id="calendar" data-save-url="<?= $this->url->href('calendar', 'save', array('project_id' => $project['id'])) ?>" data-check-url="<?= $this->url->href('calendar', 'project', array('project_id' => $project['id'])) ?>" diff --git a/app/Template/comment/show.php b/app/Template/comment/show.php index ce456c5d..3f45e2e7 100644 --- a/app/Template/comment/show.php +++ b/app/Template/comment/show.php @@ -1,6 +1,6 @@ <div class="comment <?= isset($preview) ? 'comment-preview' : '' ?>" id="comment-<?= $comment['id'] ?>"> - <?= $this->avatar->render($comment['user_id'], $comment['username'], $comment['name'], $comment['email']) ?> + <?= $this->avatar->render($comment['user_id'], $comment['username'], $comment['name'], $comment['email'], $comment['avatar_path']) ?> <div class="comment-title"> <?php if (! empty($comment['username'])): ?> diff --git a/app/Template/event/events.php b/app/Template/event/events.php index ef651321..c58376c4 100644 --- a/app/Template/event/events.php +++ b/app/Template/event/events.php @@ -7,7 +7,8 @@ $event['creator_id'], $event['author_username'], $event['author_name'], - $event['email'] + $event['email'], + $event['avatar_path'] ) ?> <div class="activity-content"> diff --git a/app/Template/gantt/project.php b/app/Template/gantt/project.php index fe193c2b..e6c8592f 100644 --- a/app/Template/gantt/project.php +++ b/app/Template/gantt/project.php @@ -1,10 +1,5 @@ <section id="main"> - <?= $this->render('project_header/header', array( - 'project' => $project, - 'filters' => $filters, - 'users_list' => $users_list, - )) ?> - + <?= $this->projectHeader->render($project, 'Gantt', 'project') ?> <div class="menu-inline"> <ul> <li <?= $sorting === 'board' ? 'class="active"' : '' ?>> diff --git a/app/Template/listing/show.php b/app/Template/listing/show.php index 10dcedcc..98b9528a 100644 --- a/app/Template/listing/show.php +++ b/app/Template/listing/show.php @@ -1,11 +1,5 @@ <section id="main"> - <?= $this->render('project_header/header', array( - 'project' => $project, - 'filters' => $filters, - 'custom_filters_list' => $custom_filters_list, - 'users_list' => $users_list, - 'categories_list' => $categories_list, - )) ?> + <?= $this->projectHeader->render($project, 'Listing', 'show') ?> <?php if ($paginator->isEmpty()): ?> <p class="alert"><?= t('No tasks found.') ?></p> @@ -24,7 +18,11 @@ <?php foreach ($paginator->getCollection() as $task): ?> <tr> <td class="task-table color-<?= $task['color_id'] ?>"> - <?= $this->render('task/dropdown', array('task' => $task)) ?> + <?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?> + <?= $this->render('task/dropdown', array('task' => $task)) ?> + <?php else: ?> + #<?= $task['id'] ?> + <?php endif ?> </td> <td> <?= $this->text->e($task['swimlane_name'] ?: $task['default_swimlane']) ?> diff --git a/app/Template/project/layout.php b/app/Template/project/layout.php index eb391ae5..fcb3e5f3 100644 --- a/app/Template/project/layout.php +++ b/app/Template/project/layout.php @@ -1,30 +1,5 @@ <section id="main"> - <div class="page-header"> - <ul> - <li> - <span class="dropdown"> - <span> - <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> - <ul> - <?= $this->render('project/dropdown', array('project' => $project)) ?> - </ul> - </span> - </span> - </li> - <li> - <i class="fa fa-th fa-fw"></i> - <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?> - </li> - <li> - <i class="fa fa-calendar fa-fw"></i> - <?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?> - </li> - <li> - <i class="fa fa-folder fa-fw"></i> - <?= $this->url->link(t('All projects'), 'project', 'index') ?> - </li> - </ul> - </div> + <?= $this->projectHeader->render($project, 'Listing', 'show') ?> <section class="sidebar-container"> <?= $this->render($sidebar_template, array('project' => $project)) ?> diff --git a/app/Template/project_header/dropdown.php b/app/Template/project_header/dropdown.php index bbc033bf..759a5135 100644 --- a/app/Template/project_header/dropdown.php +++ b/app/Template/project_header/dropdown.php @@ -1,7 +1,7 @@ <div class="dropdown"> - <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> + <a href="#" class="dropdown-menu action-menu"><?= t('Menu') ?> <i class="fa fa-caret-down"></i></a> <ul> - <?php if ($is_board): ?> + <?php if ($board_view): ?> <li> <span class="filter-display-mode" <?= $this->board->isCollapsed($project['id']) ? '' : 'style="display: none;"' ?>> <i class="fa fa-expand fa-fw"></i> diff --git a/app/Template/project_header/header.php b/app/Template/project_header/header.php index f6e5af9e..aaa8137b 100644 --- a/app/Template/project_header/header.php +++ b/app/Template/project_header/header.php @@ -1,7 +1,7 @@ <div class="project-header"> <?= $this->hook->render('template:project:header:before', array('project' => $project)) ?> - <?= $this->render('project_header/dropdown', array('project' => $project, 'is_board' => isset($is_board))) ?> + <?= $this->render('project_header/dropdown', array('project' => $project, 'board_view' => $board_view)) ?> <?= $this->render('project_header/views', array('project' => $project, 'filters' => $filters)) ?> <?= $this->render('project_header/search', array( 'project' => $project, diff --git a/app/Template/project_header/views.php b/app/Template/project_header/views.php index f8fdbb02..353e4b62 100644 --- a/app/Template/project_header/views.php +++ b/app/Template/project_header/views.php @@ -1,7 +1,7 @@ <ul class="views"> <li <?= $this->app->getRouterController() === 'ProjectOverview' ? 'class="active"' : '' ?>> <i class="fa fa-eye fa-fw"></i> - <?= $this->url->link(t('Overview'), 'ProjectOverview', 'show', array('project_id' => $project['id']), false, 'view-overview', t('Keyboard shortcut: "%s"', 'v o')) ?> + <?= $this->url->link(t('Overview'), 'ProjectOverview', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-overview', t('Keyboard shortcut: "%s"', 'v o')) ?> </li> <li <?= $this->app->getRouterController() === 'Board' ? 'class="active"' : '' ?>> <i class="fa fa-th fa-fw"></i> diff --git a/app/Template/project_overview/description.php b/app/Template/project_overview/description.php index 9895aede..72ccf06a 100644 --- a/app/Template/project_overview/description.php +++ b/app/Template/project_overview/description.php @@ -5,7 +5,7 @@ <div class="accordion-content"> <?php if ($this->user->hasProjectAccess('ProjectEdit', 'description', $project['id'])): ?> <div class="buttons-header"> - <?= $this->url->button('fa-edit', t('Edit Description'), 'ProjectEdit', 'description', array('project_id' => $project['id']), 'popover') ?> + <?= $this->url->button('fa-edit', t('Edit description'), 'ProjectEdit', 'description', array('project_id' => $project['id']), 'popover') ?> </div> <?php endif ?> <article class="markdown"> diff --git a/app/Template/project_overview/show.php b/app/Template/project_overview/show.php index 9a9786e8..6fe815b3 100644 --- a/app/Template/project_overview/show.php +++ b/app/Template/project_overview/show.php @@ -1,9 +1,5 @@ <section id="main"> - <?= $this->render('project_header/header', array( - 'project' => $project, - 'filters' => $filters, - )) ?> - + <?= $this->projectHeader->render($project, 'ProjectOverview', 'show') ?> <?= $this->render('project_overview/columns', array('project' => $project)) ?> <?= $this->render('project_overview/description', array('project' => $project)) ?> <?= $this->render('project_overview/attachments', array('project' => $project, 'images' => $images, 'files' => $files)) ?> diff --git a/app/Template/search/activity.php b/app/Template/search/activity.php new file mode 100644 index 00000000..60362215 --- /dev/null +++ b/app/Template/search/activity.php @@ -0,0 +1,39 @@ +<section id="main"> + <div class="page-header"> + <ul> + <li> + <i class="fa fa-search fa-fw"></i> + <?= $this->url->link(t('Search tasks'), 'search', 'index') ?> + </li> + </ul> + </div> + + <div class="filter-box"> + <form method="get" action="<?= $this->url->dir() ?>" class="search"> + <?= $this->form->hidden('controller', $values) ?> + <?= $this->form->hidden('action', $values) ?> + <?= $this->form->text('search', $values, array(), array(empty($values['search']) ? 'autofocus' : '', 'placeholder="'.t('Search').'"'), 'form-input-large') ?> + <?= $this->render('activity/filter_dropdown') ?> + </form> + </div> + + <?php if (empty($values['search'])): ?> + <div class="listing"> + <h3><?= t('Advanced search') ?></h3> + <p><?= t('Example of query: ') ?><strong>project:"My project" creator:me</strong></p> + <ul> + <li><?= t('Search by project: ') ?><strong>project:"My project"</strong></li> + <li><?= t('Search by creator: ') ?><strong>creator:admin</strong></li> + <li><?= t('Search by creation date: ') ?><strong>created:today</strong></li> + <li><?= t('Search by task status: ') ?><strong>status:open</strong></li> + <li><?= t('Search by task title: ') ?><strong>title:"My task"</strong></li> + </ul> + <p><i class="fa fa-external-link fa-fw"></i><?= $this->url->doc(t('View advanced search syntax'), 'search') ?></p> + </div> + <?php elseif (! empty($values['search']) && $nb_events === 0): ?> + <p class="alert"><?= t('Nothing found.') ?></p> + <?php else: ?> + <?= $this->render('event/events', array('events' => $events)) ?> + <?php endif ?> + +</section>
\ No newline at end of file diff --git a/app/Template/search/index.php b/app/Template/search/index.php index 9231a6f3..d5d07ed6 100644 --- a/app/Template/search/index.php +++ b/app/Template/search/index.php @@ -2,8 +2,8 @@ <div class="page-header"> <ul> <li> - <i class="fa fa-folder fa-fw"></i> - <?= $this->url->link(t('All projects'), 'project', 'index') ?> + <i class="fa fa-search fa-fw"></i> + <?= $this->url->link(t('Activity stream search'), 'search', 'activity') ?> </li> </ul> </div> diff --git a/app/Template/task/details.php b/app/Template/task/details.php index a7c4ad01..6093c157 100644 --- a/app/Template/task/details.php +++ b/app/Template/task/details.php @@ -1,5 +1,6 @@ <section id="task-summary"> <h2><?= $this->text->e($task['title']) ?></h2> + <div class="task-summary-container color-<?= $task['color_id'] ?>"> <div class="task-summary-column"> <ul class="no-bullet"> @@ -134,4 +135,10 @@ </ul> </div> </div> + + <?php if ($editable && empty($task['date_started'])): ?> + <div class="task-summary-buttons"> + <?= $this->url->button('fa-play', t('Set start date'), 'taskmodification', 'start', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> + <?php endif ?> </section> diff --git a/app/Template/task/layout.php b/app/Template/task/layout.php index ca27fd4f..52db5d1b 100644 --- a/app/Template/task/layout.php +++ b/app/Template/task/layout.php @@ -1,25 +1,5 @@ <section id="main"> - <div class="page-header"> - <ul> - <li> - <?= $this->render('task/menu', array('task' => $task)) ?> - </li> - <li> - <i class="fa fa-th fa-fw"></i> - <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $task['project_id']), false, '', '', false, $task['swimlane_id'] != 0 ? 'swimlane-'.$task['swimlane_id'] : '') ?> - </li> - <li> - <i class="fa fa-calendar fa-fw"></i> - <?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $task['project_id'])) ?> - </li> - <?php if ($this->user->hasProjectAccess('ProjectEdit', 'edit', $task['project_id'])): ?> - <li> - <i class="fa fa-cog fa-fw"></i> - <?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $task['project_id'])) ?> - </li> - <?php endif ?> - </ul> - </div> + <?= $this->projectHeader->render($project, 'Listing', 'show') ?> <section class="sidebar-container" id="task-view" data-edit-url="<?= $this->url->href('taskmodification', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" diff --git a/app/Template/task/menu.php b/app/Template/task/menu.php deleted file mode 100644 index fe30d06e..00000000 --- a/app/Template/task/menu.php +++ /dev/null @@ -1,78 +0,0 @@ -<?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?> -<div class="dropdown"> - <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> - <ul> - <?php if (empty($task['date_started'])): ?> - <li> - <i class="fa fa-play fa-fw"></i> - <?= $this->url->link(t('Set automatically the start date'), 'taskmodification', 'start', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - </li> - <?php endif ?> - <li> - <i class="fa fa-pencil-square-o fa-fw"></i> - <?= $this->url->link(t('Edit the task'), 'taskmodification', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> - </li> - <li> - <i class="fa fa-align-left fa-fw"></i> - <?= $this->url->link(t('Edit the description'), 'taskmodification', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> - </li> - <li> - <i class="fa fa-refresh fa-rotate-90 fa-fw"></i> - <?= $this->url->link(t('Edit recurrence'), 'TaskRecurrence', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> - </li> - <li> - <i class="fa fa-plus fa-fw"></i> - <?= $this->url->link(t('Add a sub-task'), 'subtask', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> - </li> - <li> - <i class="fa fa-code-fork fa-fw"></i> - <?= $this->url->link(t('Add internal link'), 'TaskInternalLink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> - </li> - <li> - <i class="fa fa-external-link fa-fw"></i> - <?= $this->url->link(t('Add external link'), 'TaskExternalLink', 'find', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> - </li> - <li> - <i class="fa fa-comment-o fa-fw"></i> - <?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> - </li> - <li> - <i class="fa fa-file fa-fw"></i> - <?= $this->url->link(t('Attach a document'), 'TaskFile', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> - </li> - <li> - <i class="fa fa-camera fa-fw"></i> - <?= $this->url->link(t('Add a screenshot'), 'TaskFile', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> - </li> - <li> - <i class="fa fa-files-o fa-fw"></i> - <?= $this->url->link(t('Duplicate'), 'taskduplication', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> - </li> - <li> - <i class="fa fa-clipboard fa-fw"></i> - <?= $this->url->link(t('Duplicate to another project'), 'taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> - </li> - <li> - <i class="fa fa-clone fa-fw"></i> - <?= $this->url->link(t('Move to another project'), 'taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> - </li> - <li> - <?php if ($task['is_active'] == 1): ?> - <i class="fa fa-times fa-fw"></i> - <?= $this->url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> - <?php else: ?> - <i class="fa fa-check-square-o fa-fw"></i> - <?= $this->url->link(t('Open this task'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> - <?php endif ?> - </li> - <?php if ($this->task->canRemove($task)): ?> - <li> - <i class="fa fa-trash-o fa-fw"></i> - <?= $this->url->link(t('Remove'), 'task', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?> - </li> - <?php endif ?> - - <?= $this->hook->render('template:task:menu') ?> - </ul> -</div> -<?php endif ?> diff --git a/app/Template/task/show.php b/app/Template/task/show.php index d68f6c48..86422941 100644 --- a/app/Template/task/show.php +++ b/app/Template/task/show.php @@ -14,7 +14,6 @@ 'task' => $task, 'subtasks' => $subtasks, 'project' => $project, - 'users_list' => isset($users_list) ? $users_list : array(), 'editable' => true, )) ?> diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php index ee3b1594..773b28dc 100644 --- a/app/Template/task/sidebar.php +++ b/app/Template/task/sidebar.php @@ -24,6 +24,8 @@ </li> <?php endif ?> </ul> + + <?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?> <h2><?= t('Actions') ?></h2> <ul> <li> @@ -90,6 +92,7 @@ </li> <?php endif ?> </ul> + <?php endif ?> <?= $this->hook->render('template:task:sidebar', array('task' => $task)) ?> </div> diff --git a/app/Template/task_creation/form.php b/app/Template/task_creation/form.php index cecba9ef..9bfd839f 100644 --- a/app/Template/task_creation/form.php +++ b/app/Template/task_creation/form.php @@ -28,6 +28,8 @@ <?php if (! isset($duplicate)): ?> <?= $this->form->checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?> <?php endif ?> + + <?= $this->hook->render('template:task:form:left-column', array('values'=>$values, 'errors'=>$errors)) ?> </div> <div class="form-column"> @@ -48,4 +50,4 @@ <button type="submit" class="btn btn-blue" tabindex="15"><?= t('Save') ?></button> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $values['project_id']), false, 'close-popover') ?> </div> -</form>
\ No newline at end of file +</form> diff --git a/app/Template/user/index.php b/app/Template/user/index.php index 364fd965..0b5da17c 100644 --- a/app/Template/user/index.php +++ b/app/Template/user/index.php @@ -14,6 +14,7 @@ <?php else: ?> <table class="table-stripped"> <tr> + <th class="column-5"><?= $paginator->order(t('Id'), 'id') ?></th> <th class="column-18"><?= $paginator->order(t('Username'), 'username') ?></th> <th class="column-18"><?= $paginator->order(t('Name'), 'name') ?></th> <th class="column-15"><?= $paginator->order(t('Email'), 'email') ?></th> @@ -26,7 +27,9 @@ <?php foreach ($paginator->getCollection() as $user): ?> <tr> <td> - <?= '#'.$user['id'] ?> + <?= '#'.$user['id'] ?> + </td> + <td> <?= $this->url->link($this->text->e($user['username']), 'user', 'show', array('user_id' => $user['id'])) ?> </td> <td> diff --git a/app/Template/user/notifications.php b/app/Template/user/notifications.php index 2a5c8152..6e1a0004 100644 --- a/app/Template/user/notifications.php +++ b/app/Template/user/notifications.php @@ -3,11 +3,8 @@ </div> <form method="post" action="<?= $this->url->href('user', 'notifications', array('user_id' => $user['id'])) ?>" autocomplete="off"> - <?= $this->form->csrf() ?> - <?= $this->form->checkbox('notifications_enabled', t('Enable notifications'), '1', $notifications['notifications_enabled'] == 1) ?><br> - <hr> <h4><?= t('Notification methods:') ?></h4> <?= $this->form->checkboxes('notification_types', $types, $notifications) ?> diff --git a/app/Template/user/profile.php b/app/Template/user/profile.php index 80a633e3..9c9d3282 100644 --- a/app/Template/user/profile.php +++ b/app/Template/user/profile.php @@ -1,5 +1,6 @@ <section id="main"> <br> + <?= $this->avatar->render($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path']) ?> <ul class="listing"> <li><?= t('Username:') ?> <strong><?= $this->text->e($user['username']) ?></strong></li> <li><?= t('Name:') ?> <strong><?= $this->text->e($user['name']) ?: t('None') ?></strong></li> diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php index 20fd2ad2..5ea2e355 100644 --- a/app/Template/user/sidebar.php +++ b/app/Template/user/sidebar.php @@ -37,6 +37,9 @@ <li <?= $this->app->checkMenuSelection('user', 'edit') ?>> <?= $this->url->link(t('Edit profile'), 'user', 'edit', array('user_id' => $user['id'])) ?> </li> + <li <?= $this->app->checkMenuSelection('AvatarFile') ?>> + <?= $this->url->link(t('Avatar'), 'AvatarFile', 'show', array('user_id' => $user['id'])) ?> + </li> <?php endif ?> <?php if ($user['is_ldap_user'] == 0): ?> diff --git a/app/User/Avatar/AvatarFileProvider.php b/app/User/Avatar/AvatarFileProvider.php new file mode 100644 index 00000000..eea565f0 --- /dev/null +++ b/app/User/Avatar/AvatarFileProvider.php @@ -0,0 +1,42 @@ +<?php + +namespace Kanboard\User\Avatar; + +use Kanboard\Core\Base; +use Kanboard\Core\User\Avatar\AvatarProviderInterface; + +/** + * Avatar Local Image File Provider + * + * @package avatar + * @author Frederic Guillot + */ +class AvatarFileProvider extends Base implements AvatarProviderInterface +{ + /** + * Render avatar html + * + * @access public + * @param array $user + * @param int $size + * @return string + */ + public function render(array $user, $size) + { + $url = $this->helper->url->href('AvatarFile', 'image', array('user_id' => $user['id'], 'size' => $size)); + $title = $this->helper->text->e($user['name'] ?: $user['username']); + return '<img src="' . $url . '" alt="' . $title . '" title="' . $title . '">'; + } + + /** + * Determine if the provider is active + * + * @access public + * @param array $user + * @return boolean + */ + public function isActive(array $user) + { + return !empty($user['avatar_path']); + } +} diff --git a/app/User/Avatar/GravatarProvider.php b/app/User/Avatar/GravatarProvider.php index 7a719734..87ca51b1 100644 --- a/app/User/Avatar/GravatarProvider.php +++ b/app/User/Avatar/GravatarProvider.php @@ -17,8 +17,9 @@ class GravatarProvider extends Base implements AvatarProviderInterface * Render avatar html * * @access public - * @param array $user - * @param int $size + * @param array $user + * @param int $size + * @return string */ public function render(array $user, $size) { diff --git a/app/User/Avatar/LetterAvatarProvider.php b/app/User/Avatar/LetterAvatarProvider.php index 81c4586d..f9659e61 100644 --- a/app/User/Avatar/LetterAvatarProvider.php +++ b/app/User/Avatar/LetterAvatarProvider.php @@ -24,13 +24,14 @@ class LetterAvatarProvider extends Base implements AvatarProviderInterface * Render avatar html * * @access public - * @param array $user - * @param int $size + * @param array $user + * @param int $size + * @return string */ public function render(array $user, $size) { $initials = $this->helper->user->getInitials($user['name'] ?: $user['username']); - $rgb = $this->getBackgroundColor($initials); + $rgb = $this->getBackgroundColor($user['name'] ?: $user['username']); return sprintf( '<div class="avatar-letter" style="background-color: rgb(%d, %d, %d)" title="%s">%s</div>', diff --git a/app/common.php b/app/common.php index 7dbd7587..da624844 100644 --- a/app/common.php +++ b/app/common.php @@ -39,4 +39,5 @@ $container->register(new Kanboard\ServiceProvider\RouteProvider); $container->register(new Kanboard\ServiceProvider\ActionProvider); $container->register(new Kanboard\ServiceProvider\ExternalLinkProvider); $container->register(new Kanboard\ServiceProvider\AvatarProvider); +$container->register(new Kanboard\ServiceProvider\FilterProvider); $container->register(new Kanboard\ServiceProvider\PluginProvider); |