summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml1
-rw-r--r--ChangeLog10
-rw-r--r--app/Action/TaskAssignColorSwimlane.php99
-rw-r--r--app/Action/TaskAssignPrioritySwimlane.php99
-rw-r--r--app/Controller/ActionController.php1
-rw-r--r--app/Controller/ActionCreationController.php1
-rw-r--r--app/Controller/BoardAjaxController.php9
-rw-r--r--app/Controller/BoardTooltipController.php5
-rw-r--r--app/Controller/CommentController.php20
-rw-r--r--app/Controller/TaskViewController.php4
-rw-r--r--app/Controller/UserCredentialController.php17
-rw-r--r--app/Core/Base.php2
-rw-r--r--app/Core/Cache/BaseCache.php (renamed from app/Core/Cache/Base.php)6
-rw-r--r--app/Core/Cache/CacheInterface.php14
-rw-r--r--app/Core/Cache/FileCache.php98
-rw-r--r--app/Core/Cache/MemoryCache.php10
-rw-r--r--app/Core/Event/EventManager.php1
-rw-r--r--app/Core/Plugin/Installer.php27
-rw-r--r--app/Core/Tool.php28
-rw-r--r--app/Core/User/UserSession.php46
-rw-r--r--app/Decorator/MetadataCacheDecorator.php96
-rw-r--r--app/Helper/BoardHelper.php3
-rw-r--r--app/Locale/bs_BA/translations.php2
-rw-r--r--app/Locale/cs_CZ/translations.php2
-rw-r--r--app/Locale/da_DK/translations.php2
-rw-r--r--app/Locale/de_DE/translations.php2
-rw-r--r--app/Locale/el_GR/translations.php2
-rw-r--r--app/Locale/es_ES/translations.php2
-rw-r--r--app/Locale/fi_FI/translations.php2
-rw-r--r--app/Locale/fr_FR/translations.php2
-rw-r--r--app/Locale/hu_HU/translations.php2
-rw-r--r--app/Locale/id_ID/translations.php2
-rw-r--r--app/Locale/it_IT/translations.php2
-rw-r--r--app/Locale/ja_JP/translations.php2
-rw-r--r--app/Locale/ko_KR/translations.php194
-rw-r--r--app/Locale/my_MY/translations.php2
-rw-r--r--app/Locale/nb_NO/translations.php2
-rw-r--r--app/Locale/nl_NL/translations.php2
-rw-r--r--app/Locale/pl_PL/translations.php2
-rw-r--r--app/Locale/pt_BR/translations.php2
-rw-r--r--app/Locale/pt_PT/translations.php2
-rw-r--r--app/Locale/ru_RU/translations.php2
-rw-r--r--app/Locale/sr_Latn_RS/translations.php2
-rw-r--r--app/Locale/sv_SE/translations.php2
-rw-r--r--app/Locale/th_TH/translations.php126
-rw-r--r--app/Locale/tr_TR/translations.php2
-rw-r--r--app/Locale/zh_CN/translations.php2
-rw-r--r--app/Model/ActionParameterModel.php3
-rw-r--r--app/Model/UserMetadataModel.php3
-rw-r--r--app/ServiceProvider/ActionProvider.php4
-rw-r--r--app/ServiceProvider/AuthenticationProvider.php2
-rw-r--r--app/ServiceProvider/CacheProvider.php51
-rw-r--r--app/ServiceProvider/ClassProvider.php3
-rw-r--r--app/Template/action/index.php2
-rw-r--r--app/Template/action_creation/params.php3
-rw-r--r--app/Template/board/task_footer.php6
-rw-r--r--app/Template/dashboard/notifications.php16
-rw-r--r--app/Template/project_list/show.php2
-rw-r--r--app/Template/user_view/show.php5
-rw-r--r--app/common.php1
-rw-r--r--app/constants.php6
-rw-r--r--assets/css/app.min.css2
-rw-r--r--assets/sass/_project.sass4
-rw-r--r--assets/sass/_project_header.sass4
-rw-r--r--assets/sass/_project_views_switcher.sass2
-rw-r--r--assets/sass/_task_tags.sass12
-rw-r--r--config.default.php13
-rw-r--r--doc/api-authentication.markdown36
-rw-r--r--doc/bruteforce-protection.markdown9
-rw-r--r--doc/calendar-configuration.markdown49
-rw-r--r--doc/config.markdown11
-rw-r--r--doc/debian-installation.markdown14
-rw-r--r--doc/email-configuration.markdown81
-rw-r--r--doc/es_ES/analytics.markdown10
-rw-r--r--doc/es_ES/debian-installation.markdown65
-rw-r--r--doc/faq.markdown6
-rw-r--r--doc/kanban-vs-todo-and-scrum.markdown54
-rw-r--r--doc/plugin-hooks.markdown2
-rw-r--r--tests/configtest/DefaultConfigFileTest.php9
-rw-r--r--tests/units/Action/TaskAssignColorSwimlaneTest.php70
-rw-r--r--tests/units/Action/TaskAssignPrioritySwimlaneTest.php46
-rw-r--r--tests/units/Base.php1
-rw-r--r--tests/units/Core/Cache/FileCacheTest.php186
-rw-r--r--tests/units/Core/ObjectStorage/FileStorageTest.php (renamed from tests/units/Core/FileStorageTest.php)4
-rw-r--r--tests/units/Core/User/UserSessionTest.php21
-rw-r--r--tests/units/Decorator/MetadataCacheDecoratorTest.php127
86 files changed, 1428 insertions, 480 deletions
diff --git a/.travis.yml b/.travis.yml
index 40af3ca8..32403be4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -26,3 +26,4 @@ before_script:
script:
- phpunit -c tests/units.$DB.xml
+ - phpunit tests/configtest
diff --git a/ChangeLog b/ChangeLog
index a559b4ff..8ea71453 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -3,10 +3,19 @@ Version 1.0.33 (unreleased)
New features:
+* Add the possibility to unlock users from the user interface
* New API calls for task metadata
+* New automatic actions:
+ - Define colour by Swimlane
+ - Define priority by Swimlane
Improvements:
+* Show both time spent and estimated on the board
+* Store board collapsed mode user preference in the database
+* Store comment sorting direction in the database
+* Avoid tags overlapping on the board
+* Show project name in notifications
* Allow priority changes for inverted priority scales
* Add the possibility to attach template hooks with local variables and callback
* Add "reference" hooks
@@ -15,6 +24,7 @@ Improvements:
Bug fixes:
+* Fix undefined constant in config example file
* Fix PHP notice when sending overdue notifications
Version 1.0.32
diff --git a/app/Action/TaskAssignColorSwimlane.php b/app/Action/TaskAssignColorSwimlane.php
new file mode 100644
index 00000000..31f2d25a
--- /dev/null
+++ b/app/Action/TaskAssignColorSwimlane.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Kanboard\Action;
+
+use Kanboard\Model\TaskModel;
+
+/**
+ * Assign a color to a task based on Swimlane
+ *
+ * @package Kanboard\Action
+ * @author Dave Almond
+ */
+class TaskAssignColorSwimlane extends Base
+{
+ /**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Assign a color when the task is moved to a specific swimlane');
+ }
+
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ TaskModel::EVENT_CREATE,
+ TaskModel::EVENT_MOVE_SWIMLANE,
+ );
+ }
+
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'swimlane_id' => t('Swimlane'),
+ 'color_id' => t('Color'),
+ );
+ }
+
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array(
+ 'task_id',
+ 'task' => array(
+ 'project_id',
+ 'swimlane_id',
+ ),
+ );
+ }
+
+ /**
+ * Execute the action (set the task color)
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool True if the action was executed or false when not executed
+ */
+ public function doAction(array $data)
+ {
+ $values = array(
+ 'id' => $data['task_id'],
+ 'color_id' => $this->getParam('color_id'),
+ );
+
+ return $this->taskModificationModel->update($values, false);
+ }
+
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ return $data['task']['swimlane_id'] == $this->getParam('swimlane_id');
+ }
+}
diff --git a/app/Action/TaskAssignPrioritySwimlane.php b/app/Action/TaskAssignPrioritySwimlane.php
new file mode 100644
index 00000000..7eaca7c8
--- /dev/null
+++ b/app/Action/TaskAssignPrioritySwimlane.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Kanboard\Action;
+
+use Kanboard\Model\TaskModel;
+
+/**
+ * Set a priority automatically according to the Swimlane
+ *
+ * @package Kanboard\Action
+ * @author Dave Almond
+ */
+class TaskAssignPrioritySwimlane extends Base
+{
+ /**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Assign a priority when the task is moved to a specific swimlane');
+ }
+
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ TaskModel::EVENT_CREATE,
+ TaskModel::EVENT_MOVE_SWIMLANE,
+ );
+ }
+
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'swimlane_id' => t('Swimlane'),
+ 'priority' => t('Priority'),
+ );
+ }
+
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array(
+ 'task_id',
+ 'task' => array(
+ 'project_id',
+ 'swimlane_id',
+ ),
+ );
+ }
+
+ /**
+ * Execute the action (set the priority)
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool True if the action was executed or false when not executed
+ */
+ public function doAction(array $data)
+ {
+ $values = array(
+ 'id' => $data['task_id'],
+ 'priority' => $this->getParam('priority'),
+ );
+
+ return $this->taskModificationModel->update($values);
+ }
+
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ return $data['task']['swimlane_id'] == $this->getParam('swimlane_id');
+ }
+}
diff --git a/app/Controller/ActionController.php b/app/Controller/ActionController.php
index 097640f6..c935125a 100644
--- a/app/Controller/ActionController.php
+++ b/app/Controller/ActionController.php
@@ -33,6 +33,7 @@ class ActionController extends BaseController
'colors_list' => $this->colorModel->getList(),
'categories_list' => $this->categoryModel->getList($project['id']),
'links_list' => $this->linkModel->getList(0, false),
+ 'swimlane_list' => $this->swimlaneModel->getList($project['id']),
'title' => t('Automatic actions')
)));
}
diff --git a/app/Controller/ActionCreationController.php b/app/Controller/ActionCreationController.php
index 9b228f28..1629e68f 100644
--- a/app/Controller/ActionCreationController.php
+++ b/app/Controller/ActionCreationController.php
@@ -84,6 +84,7 @@ class ActionCreationController extends BaseController
'priorities_list' => $this->projectTaskPriorityModel->getPriorities($project),
'project' => $project,
'available_actions' => $this->actionManager->getAvailableActions(),
+ 'swimlane_list' => $this->swimlaneModel->getList($project['id']),
'events' => $this->actionManager->getCompatibleEvents($values['action_name']),
)));
}
diff --git a/app/Controller/BoardAjaxController.php b/app/Controller/BoardAjaxController.php
index 9b721f06..ccd47667 100644
--- a/app/Controller/BoardAjaxController.php
+++ b/app/Controller/BoardAjaxController.php
@@ -4,6 +4,7 @@ namespace Kanboard\Controller;
use Kanboard\Core\Controller\AccessForbiddenException;
use Kanboard\Formatter\BoardFormatter;
+use Kanboard\Model\UserMetadataModel;
/**
* Class BoardAjaxController
@@ -88,7 +89,7 @@ class BoardAjaxController extends BaseController
*/
public function collapse()
{
- $this->changeDisplayMode(true);
+ $this->changeDisplayMode(1);
}
/**
@@ -98,19 +99,19 @@ class BoardAjaxController extends BaseController
*/
public function expand()
{
- $this->changeDisplayMode(false);
+ $this->changeDisplayMode(0);
}
/**
* Change display mode
*
* @access private
- * @param boolean $mode
+ * @param int $mode
*/
private function changeDisplayMode($mode)
{
$project_id = $this->request->getIntegerParam('project_id');
- $this->userSession->setBoardDisplayMode($project_id, $mode);
+ $this->userMetadataCacheDecorator->set(UserMetadataModel::KEY_BOARD_COLLAPSED.$project_id, $mode);
if ($this->request->isAjax()) {
$this->response->html($this->renderBoard($project_id));
diff --git a/app/Controller/BoardTooltipController.php b/app/Controller/BoardTooltipController.php
index 134d728e..79b9b509 100644
--- a/app/Controller/BoardTooltipController.php
+++ b/app/Controller/BoardTooltipController.php
@@ -2,6 +2,8 @@
namespace Kanboard\Controller;
+use Kanboard\Model\UserMetadataModel;
+
/**
* Board Tooltip
*
@@ -75,10 +77,11 @@ class BoardTooltipController extends BaseController
public function comments()
{
$task = $this->getTask();
+ $commentSortingDirection = $this->userMetadataCacheDecorator->get(UserMetadataModel::KEY_COMMENT_SORTING_DIRECTION, 'ASC');
$this->response->html($this->template->render('board/tooltip_comments', array(
'task' => $task,
- 'comments' => $this->commentModel->getAll($task['id'], $this->userSession->getCommentSorting())
+ 'comments' => $this->commentModel->getAll($task['id'], $commentSortingDirection)
)));
}
diff --git a/app/Controller/CommentController.php b/app/Controller/CommentController.php
index 2a8c258a..c61a0602 100644
--- a/app/Controller/CommentController.php
+++ b/app/Controller/CommentController.php
@@ -4,6 +4,7 @@ namespace Kanboard\Controller;
use Kanboard\Core\Controller\AccessForbiddenException;
use Kanboard\Core\Controller\PageNotFoundException;
+use Kanboard\Model\UserMetadataModel;
/**
* Comment Controller
@@ -82,10 +83,10 @@ class CommentController extends BaseController
$this->flash->failure(t('Unable to create your comment.'));
}
- return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments'), true);
+ $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments'), true);
+ } else {
+ $this->create($values, $errors);
}
-
- return $this->create($values, $errors);
}
/**
@@ -183,9 +184,16 @@ class CommentController extends BaseController
{
$task = $this->getTask();
- $order = $this->userSession->getCommentSorting() === 'ASC' ? 'DESC' : 'ASC';
- $this->userSession->setCommentSorting($order);
+ $oldDirection = $this->userMetadataCacheDecorator->get(UserMetadataModel::KEY_COMMENT_SORTING_DIRECTION, 'ASC');
+ $newDirection = $oldDirection === 'ASC' ? 'DESC' : 'ASC';
- $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments'));
+ $this->userMetadataCacheDecorator->set(UserMetadataModel::KEY_COMMENT_SORTING_DIRECTION, $newDirection);
+
+ $this->response->redirect($this->helper->url->to(
+ 'TaskViewController',
+ 'show',
+ array('task_id' => $task['id'], 'project_id' => $task['project_id']),
+ 'comments'
+ ));
}
}
diff --git a/app/Controller/TaskViewController.php b/app/Controller/TaskViewController.php
index 36597457..31b9de11 100644
--- a/app/Controller/TaskViewController.php
+++ b/app/Controller/TaskViewController.php
@@ -4,6 +4,7 @@ namespace Kanboard\Controller;
use Kanboard\Core\Controller\AccessForbiddenException;
use Kanboard\Core\Controller\PageNotFoundException;
+use Kanboard\Model\UserMetadataModel;
/**
* Task Controller
@@ -61,13 +62,14 @@ class TaskViewController extends BaseController
{
$task = $this->getTask();
$subtasks = $this->subtaskModel->getAll($task['id']);
+ $commentSortingDirection = $this->userMetadataCacheDecorator->get(UserMetadataModel::KEY_COMMENT_SORTING_DIRECTION, 'ASC');
$this->response->html($this->helper->layout->task('task/show', array(
'task' => $task,
'project' => $this->projectModel->getById($task['project_id']),
'files' => $this->taskFileModel->getAllDocuments($task['id']),
'images' => $this->taskFileModel->getAllImages($task['id']),
- 'comments' => $this->commentModel->getAll($task['id'], $this->userSession->getCommentSorting()),
+ 'comments' => $this->commentModel->getAll($task['id'], $commentSortingDirection),
'subtasks' => $subtasks,
'internal_links' => $this->taskLinkModel->getAllGroupedByLabel($task['id']),
'external_links' => $this->taskExternalLinkModel->getAll($task['id']),
diff --git a/app/Controller/UserCredentialController.php b/app/Controller/UserCredentialController.php
index 4021dc37..98fe967d 100644
--- a/app/Controller/UserCredentialController.php
+++ b/app/Controller/UserCredentialController.php
@@ -106,4 +106,21 @@ class UserCredentialController extends BaseController
return $this->changeAuthentication($values, $errors);
}
+
+ /**
+ * Unlock user
+ */
+ public function unlock()
+ {
+ $user = $this->getUser();
+ $this->checkCSRFParam();
+
+ if ($this->userLockingModel->resetFailedLogin($user['username'])) {
+ $this->flash->success(t('User unlocked successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to unlock the user.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('UserViewController', 'show', array('user_id' => $user['id'])));
+ }
}
diff --git a/app/Core/Base.php b/app/Core/Base.php
index 68604785..3b7c5e66 100644
--- a/app/Core/Base.php
+++ b/app/Core/Base.php
@@ -18,6 +18,7 @@ use Pimple\Container;
* @property \Kanboard\Core\Action\ActionManager $actionManager
* @property \Kanboard\Core\ExternalLink\ExternalLinkManager $externalLinkManager
* @property \Kanboard\Core\Cache\MemoryCache $memoryCache
+ * @property \Kanboard\Core\Cache\BaseCache $cacheDriver
* @property \Kanboard\Core\Event\EventManager $eventManager
* @property \Kanboard\Core\Group\GroupManager $groupManager
* @property \Kanboard\Core\Http\Client $httpClient
@@ -55,6 +56,7 @@ use Pimple\Container;
* @property \Kanboard\Core\Helper $helper
* @property \Kanboard\Core\Paginator $paginator
* @property \Kanboard\Core\Template $template
+ * @property \Kanboard\Decorator\MetadataCacheDecorator $userMetadataCacheDecorator
* @property \Kanboard\Model\ActionModel $actionModel
* @property \Kanboard\Model\ActionParameterModel $actionParameterModel
* @property \Kanboard\Model\AvatarFileModel $avatarFileModel
diff --git a/app/Core/Cache/Base.php b/app/Core/Cache/BaseCache.php
index d62b8507..b51c4c0c 100644
--- a/app/Core/Cache/Base.php
+++ b/app/Core/Cache/BaseCache.php
@@ -3,12 +3,12 @@
namespace Kanboard\Core\Cache;
/**
- * Base class for cache drivers
+ * Base Class for Cache Drivers
*
- * @package cache
+ * @package Kanboard\Core\Cache
* @author Frederic Guillot
*/
-abstract class Base
+abstract class BaseCache implements CacheInterface
{
/**
* Proxy cache
diff --git a/app/Core/Cache/CacheInterface.php b/app/Core/Cache/CacheInterface.php
index d9e9747a..19bd6ef7 100644
--- a/app/Core/Cache/CacheInterface.php
+++ b/app/Core/Cache/CacheInterface.php
@@ -3,15 +3,15 @@
namespace Kanboard\Core\Cache;
/**
- * Cache Interface
+ * Interface CacheInterface
*
- * @package cache
- * @author Frederic Guillot
+ * @package Kanboard\Core\Cache
+ * @author Frederic Guillot
*/
interface CacheInterface
{
/**
- * Save a new value in the cache
+ * Store an item in the cache
*
* @access public
* @param string $key
@@ -20,7 +20,7 @@ interface CacheInterface
public function set($key, $value);
/**
- * Fetch value from cache
+ * Retrieve an item from the cache by key
*
* @access public
* @param string $key
@@ -29,14 +29,14 @@ interface CacheInterface
public function get($key);
/**
- * Clear all cache
+ * Remove all items from the cache
*
* @access public
*/
public function flush();
/**
- * Remove cached value
+ * Remove an item from the cache
*
* @access public
* @param string $key
diff --git a/app/Core/Cache/FileCache.php b/app/Core/Cache/FileCache.php
new file mode 100644
index 00000000..d477a1f3
--- /dev/null
+++ b/app/Core/Cache/FileCache.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Kanboard\Core\Cache;
+
+use Kanboard\Core\Tool;
+use LogicException;
+
+/**
+ * Class FileCache
+ *
+ * @package Kanboard\Core\Cache
+ */
+class FileCache extends BaseCache
+{
+ /**
+ * Store an item in the cache
+ *
+ * @access public
+ * @param string $key
+ * @param string $value
+ */
+ public function set($key, $value)
+ {
+ $this->createCacheFolder();
+ file_put_contents($this->getFilenameFromKey($key), serialize($value));
+ }
+
+ /**
+ * Retrieve an item from the cache by key
+ *
+ * @access public
+ * @param string $key
+ * @return mixed Null when not found, cached value otherwise
+ */
+ public function get($key)
+ {
+ $filename = $this->getFilenameFromKey($key);
+
+ if (file_exists($filename)) {
+ return unserialize(file_get_contents($filename));
+ }
+
+ return null;
+ }
+
+ /**
+ * Remove all items from the cache
+ *
+ * @access public
+ */
+ public function flush()
+ {
+ $this->createCacheFolder();
+ Tool::removeAllFiles(CACHE_DIR, false);
+ }
+
+ /**
+ * Remove an item from the cache
+ *
+ * @access public
+ * @param string $key
+ */
+ public function remove($key)
+ {
+ $filename = $this->getFilenameFromKey($key);
+
+ if (file_exists($filename)) {
+ unlink($filename);
+ }
+ }
+
+ /**
+ * Get absolute filename from the key
+ *
+ * @access protected
+ * @param string $key
+ * @return string
+ */
+ protected function getFilenameFromKey($key)
+ {
+ return CACHE_DIR.DIRECTORY_SEPARATOR.$key;
+ }
+
+ /**
+ * Create cache folder if missing
+ *
+ * @access protected
+ * @throws LogicException
+ */
+ protected function createCacheFolder()
+ {
+ if (! is_dir(CACHE_DIR)) {
+ if (! mkdir(CACHE_DIR, 0755)) {
+ throw new LogicException('Unable to create cache directory: '.CACHE_DIR);
+ }
+ }
+ }
+}
diff --git a/app/Core/Cache/MemoryCache.php b/app/Core/Cache/MemoryCache.php
index 39e3947b..4fb94728 100644
--- a/app/Core/Cache/MemoryCache.php
+++ b/app/Core/Cache/MemoryCache.php
@@ -3,12 +3,12 @@
namespace Kanboard\Core\Cache;
/**
- * Memory Cache
+ * Memory Cache Driver
*
- * @package cache
+ * @package Kanboard\Core\Cache
* @author Frederic Guillot
*/
-class MemoryCache extends Base implements CacheInterface
+class MemoryCache extends BaseCache
{
/**
* Container
@@ -19,7 +19,7 @@ class MemoryCache extends Base implements CacheInterface
private $storage = array();
/**
- * Save a new value in the cache
+ * Store an item in the cache
*
* @access public
* @param string $key
@@ -31,7 +31,7 @@ class MemoryCache extends Base implements CacheInterface
}
/**
- * Fetch value from cache
+ * Retrieve an item from the cache by key
*
* @access public
* @param string $key
diff --git a/app/Core/Event/EventManager.php b/app/Core/Event/EventManager.php
index 9ae43170..48a9d299 100644
--- a/app/Core/Event/EventManager.php
+++ b/app/Core/Event/EventManager.php
@@ -53,6 +53,7 @@ class EventManager
TaskModel::EVENT_CREATE_UPDATE => t('Task creation or modification'),
TaskModel::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'),
TaskModel::EVENT_DAILY_CRONJOB => t('Daily background job for tasks'),
+ TaskModel::EVENT_MOVE_SWIMLANE => t('Move a task to another swimlane'),
);
$events = array_merge($events, $this->events);
diff --git a/app/Core/Plugin/Installer.php b/app/Core/Plugin/Installer.php
index 48c4d978..b3618aeb 100644
--- a/app/Core/Plugin/Installer.php
+++ b/app/Core/Plugin/Installer.php
@@ -2,9 +2,8 @@
namespace Kanboard\Core\Plugin;
-use RecursiveDirectoryIterator;
-use RecursiveIteratorIterator;
use ZipArchive;
+use Kanboard\Core\Tool;
/**
* Class Installer
@@ -64,7 +63,7 @@ class Installer extends \Kanboard\Core\Base
throw new PluginInstallerException(e('You don\'t have the permission to remove this plugin.'));
}
- $this->removeAllDirectories($pluginFolder);
+ Tool::removeAllFiles($pluginFolder);
}
/**
@@ -137,26 +136,4 @@ class Installer extends \Kanboard\Core\Base
unlink($zip->filename);
$zip->close();
}
-
- /**
- * Remove recursively a directory
- *
- * @access protected
- * @param string $directory
- */
- protected function removeAllDirectories($directory)
- {
- $it = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS);
- $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
-
- foreach ($files as $file) {
- if ($file->isDir()) {
- rmdir($file->getRealPath());
- } else {
- unlink($file->getRealPath());
- }
- }
-
- rmdir($directory);
- }
}
diff --git a/app/Core/Tool.php b/app/Core/Tool.php
index bfa6c955..9b8820eb 100644
--- a/app/Core/Tool.php
+++ b/app/Core/Tool.php
@@ -3,6 +3,8 @@
namespace Kanboard\Core;
use Pimple\Container;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
/**
* Tool class
@@ -13,6 +15,32 @@ use Pimple\Container;
class Tool
{
/**
+ * Remove recursively a directory
+ *
+ * @static
+ * @access public
+ * @param string $directory
+ * @param bool $removeDirectory
+ */
+ public static function removeAllFiles($directory, $removeDirectory = true)
+ {
+ $it = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS);
+ $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
+
+ foreach ($files as $file) {
+ if ($file->isDir()) {
+ rmdir($file->getRealPath());
+ } else {
+ unlink($file->getRealPath());
+ }
+ }
+
+ if ($removeDirectory) {
+ rmdir($directory);
+ }
+ }
+
+ /**
* Build dependency injection container from an array
*
* @static
diff --git a/app/Core/User/UserSession.php b/app/Core/User/UserSession.php
index 9c63f07a..7917b223 100644
--- a/app/Core/User/UserSession.php
+++ b/app/Core/User/UserSession.php
@@ -179,50 +179,4 @@ class UserSession extends Base
{
$this->sessionStorage->filters[$project_id] = $filters;
}
-
- /**
- * Is board collapsed or expanded
- *
- * @access public
- * @param integer $project_id
- * @return boolean
- */
- public function isBoardCollapsed($project_id)
- {
- return ! empty($this->sessionStorage->boardCollapsed[$project_id]) ? $this->sessionStorage->boardCollapsed[$project_id] : false;
- }
-
- /**
- * Set board display mode
- *
- * @access public
- * @param integer $project_id
- * @param boolean $is_collapsed
- */
- public function setBoardDisplayMode($project_id, $is_collapsed)
- {
- $this->sessionStorage->boardCollapsed[$project_id] = $is_collapsed;
- }
-
- /**
- * Set comments sorting
- *
- * @access public
- * @param string $order
- */
- public function setCommentSorting($order)
- {
- $this->sessionStorage->commentSorting = $order;
- }
-
- /**
- * Get comments sorting direction
- *
- * @access public
- * @return string
- */
- public function getCommentSorting()
- {
- return empty($this->sessionStorage->commentSorting) ? 'ASC' : $this->sessionStorage->commentSorting;
- }
}
diff --git a/app/Decorator/MetadataCacheDecorator.php b/app/Decorator/MetadataCacheDecorator.php
new file mode 100644
index 00000000..0897b51c
--- /dev/null
+++ b/app/Decorator/MetadataCacheDecorator.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Kanboard\Decorator;
+
+use Kanboard\Core\Cache\CacheInterface;
+use Kanboard\Model\MetadataModel;
+
+/**
+ * Class MetadataCacheDecorator
+ *
+ * @package Kanboard\Decorator
+ * @author Frederic Guillot
+ */
+class MetadataCacheDecorator
+{
+ /**
+ * @var CacheInterface
+ */
+ protected $cache;
+
+ /**
+ * @var MetadataModel
+ */
+ protected $metadataModel;
+
+ /**
+ * @var string
+ */
+ protected $cachePrefix;
+
+ /**
+ * @var int
+ */
+ protected $entityId;
+
+ /**
+ * Constructor
+ *
+ * @param CacheInterface $cache
+ * @param MetadataModel $metadataModel
+ * @param string $cachePrefix
+ * @param integer $entityId
+ */
+ public function __construct(CacheInterface $cache, MetadataModel $metadataModel, $cachePrefix, $entityId)
+ {
+ $this->cache = $cache;
+ $this->metadataModel = $metadataModel;
+ $this->cachePrefix = $cachePrefix;
+ $this->entityId = $entityId;
+ }
+
+ /**
+ * Get metadata value by key
+ *
+ * @param string $key
+ * @param mixed $default
+ * @return mixed
+ */
+ public function get($key, $default)
+ {
+ $metadata = $this->cache->get($this->getCacheKey());
+
+ if ($metadata === null) {
+ $metadata = $this->metadataModel->getAll($this->entityId);
+ $this->cache->set($this->getCacheKey(), $metadata);
+ }
+
+ return isset($metadata[$key]) ? $metadata[$key] : $default;
+ }
+
+ /**
+ * Set new metadata value
+ *
+ * @param $key
+ * @param $value
+ */
+ public function set($key, $value)
+ {
+ $this->metadataModel->save($this->entityId, array(
+ $key => $value,
+ ));
+
+ $metadata = $this->metadataModel->getAll($this->entityId);
+ $this->cache->set($this->getCacheKey(), $metadata);
+ }
+
+ /**
+ * Get cache key
+ *
+ * @return string
+ */
+ protected function getCacheKey()
+ {
+ return $this->cachePrefix.$this->entityId;
+ }
+}
diff --git a/app/Helper/BoardHelper.php b/app/Helper/BoardHelper.php
index a86a6c18..f5df3db2 100644
--- a/app/Helper/BoardHelper.php
+++ b/app/Helper/BoardHelper.php
@@ -3,6 +3,7 @@
namespace Kanboard\Helper;
use Kanboard\Core\Base;
+use Kanboard\Model\UserMetadataModel;
/**
* Board Helper
@@ -21,6 +22,6 @@ class BoardHelper extends Base
*/
public function isCollapsed($project_id)
{
- return $this->userSession->isBoardCollapsed($project_id);
+ return $this->userMetadataCacheDecorator->get(UserMetadataModel::KEY_BOARD_COLLAPSED.$project_id, 0) == 1;
}
}
diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php
index 898e4d66..e17a3cdf 100644
--- a/app/Locale/bs_BA/translations.php
+++ b/app/Locale/bs_BA/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Da li želiš da ukloniš projekat: "%s"?',
'Remove project' => 'Ukloni projekat',
'Edit the board for "%s"' => 'Uredi ploču za "%s"',
- 'All projects' => 'Svi projekti',
'Add a new column' => 'Dodaj novu kolonu',
'Title' => 'Naslov',
'Assigned to %s' => 'Dodijeljen korisniku %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(Naredba VACUUM)',
'(Gzip compressed Sqlite file)' => '(Sqlite baza spakovana Gzip-om)',
'Close a task' => 'Zatvori zadatak',
- 'Edit a task' => 'Uredi zadatak',
'Column' => 'Kolona',
'Color' => 'Boja',
'Assignee' => 'Izvršilac',
diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php
index 565f92f5..277ddecc 100644
--- a/app/Locale/cs_CZ/translations.php
+++ b/app/Locale/cs_CZ/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Opravdu chcete vyjmout projekt: "%s"?',
'Remove project' => 'Vyjmout projekt',
'Edit the board for "%s"' => 'Editace nástěnky pro "%s" ',
- 'All projects' => 'Všechny projekty',
'Add a new column' => 'Přidat nový sloupec',
'Title' => 'Název',
'Assigned to %s' => 'Přiřazeno uživateli: %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(Vyčištění)',
'(Gzip compressed Sqlite file)' => '(Gzip )',
'Close a task' => 'Uzavřít úkol',
- 'Edit a task' => 'Editovat úkol',
'Column' => 'Sloupec',
'Color' => 'Barva',
'Assignee' => 'Přiřazeno uživateli',
diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php
index 0ff4253e..c4bf55ff 100644
--- a/app/Locale/da_DK/translations.php
+++ b/app/Locale/da_DK/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Vil du virkelig fjerne dette projekt: "%s"?',
'Remove project' => 'Fjern projekt',
'Edit the board for "%s"' => 'Rediger boardet for "%s"',
- 'All projects' => 'Alle Projekter',
'Add a new column' => 'Tilføj en ny kolonne',
'Title' => 'Titel',
'Assigned to %s' => 'Ansvarlig: %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(VACUUM kommando)',
'(Gzip compressed Sqlite file)' => '(Gzip-komprimeret Sqlite fil)',
'Close a task' => 'Luk en opgave',
- 'Edit a task' => 'Rediger en opgave',
'Column' => 'Kolonne',
'Color' => 'Farve',
'Assignee' => 'Ansvarlig',
diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php
index 59de2956..a2a1c17a 100644
--- a/app/Locale/de_DE/translations.php
+++ b/app/Locale/de_DE/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Soll dieses Projekt wirklich gelöscht werden: "%s"?',
'Remove project' => 'Projekt löschen',
'Edit the board for "%s"' => 'Pinnwand für "%s" bearbeiten',
- 'All projects' => 'Alle Projekte',
'Add a new column' => 'Neue Spalte hinzufügen',
'Title' => 'Titel',
'Assigned to %s' => 'Zuständig: %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(VACUUM Befehl)',
'(Gzip compressed Sqlite file)' => '(Gzip-komprimierte SQLite-Datei)',
'Close a task' => 'Aufgabe abschließen',
- 'Edit a task' => 'Aufgabe bearbeiten',
'Column' => 'Spalte',
'Color' => 'Farbe',
'Assignee' => 'Zuständiger',
diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php
index cf3bb588..30143ac5 100644
--- a/app/Locale/el_GR/translations.php
+++ b/app/Locale/el_GR/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Αφαίρεση του έργου: « %s » ?',
'Remove project' => 'Αφαίρεση του έργου',
'Edit the board for "%s"' => 'Διόρθωση πίνακα από « %s »',
- 'All projects' => 'Όλα τα έργα',
'Add a new column' => 'Πρόσθήκη στήλης',
'Title' => 'Τίτλος',
'Assigned to %s' => 'Ανατιθεμένο στον %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(VACUUM command)',
'(Gzip compressed Sqlite file)' => '(Gzip compressed Sqlite file)',
'Close a task' => 'Κλείσιμο εργασίας',
- 'Edit a task' => 'Διόρθωση εργασίας',
'Column' => 'Στήλη',
'Color' => 'Χρώμα',
'Assignee' => 'Ανατεθιμένο στον χρήστη',
diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php
index 5a2c16c0..be603eeb 100644
--- a/app/Locale/es_ES/translations.php
+++ b/app/Locale/es_ES/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => '¿Realmente desea eliminar este proyecto: «%s»?',
'Remove project' => 'Eliminar el proyecto',
'Edit the board for "%s"' => 'Modificar el tablero para «%s»',
- 'All projects' => 'Todos los proyectos',
'Add a new column' => 'Añadir una nueva columna',
'Title' => 'Título',
'Assigned to %s' => 'Asignada a %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(comando VACUUM)',
'(Gzip compressed Sqlite file)' => '(archivo Sqlite comprimido en Gzip)',
'Close a task' => 'Cerrar una tarea',
- 'Edit a task' => 'Modificar una tarea',
'Column' => 'Columna',
'Color' => 'Color',
'Assignee' => 'Responsable',
diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php
index ed38fb56..7ce7fc5f 100644
--- a/app/Locale/fi_FI/translations.php
+++ b/app/Locale/fi_FI/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Haluatko varmasti poistaa projektin: "%s"?',
'Remove project' => 'Poista projekti',
'Edit the board for "%s"' => 'Muokkaa taulua projektille "%s"',
- 'All projects' => 'Kaikki projektit',
'Add a new column' => 'Lisää uusi sarake',
'Title' => 'Nimi',
'Assigned to %s' => 'Tekijä: %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(VACUUM-komento)',
'(Gzip compressed Sqlite file)' => '(Gzip-pakattu Sqlite-tiedosto)',
'Close a task' => 'Sulje tehtävä',
- 'Edit a task' => 'Muokkaa tehtävää',
'Column' => 'Sarake',
'Color' => 'Väri',
'Assignee' => 'Suorittaja',
diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php
index e7184949..71b8ace5 100644
--- a/app/Locale/fr_FR/translations.php
+++ b/app/Locale/fr_FR/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Voulez-vous vraiment supprimer ce projet : « %s » ?',
'Remove project' => 'Supprimer le projet',
'Edit the board for "%s"' => 'Modifier le tableau pour « %s »',
- 'All projects' => 'Tous les projets',
'Add a new column' => 'Ajouter une nouvelle colonne',
'Title' => 'Titre',
'Assigned to %s' => 'Assigné à %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(Commande VACUUM)',
'(Gzip compressed Sqlite file)' => '(Fichier Sqlite compressé en Gzip)',
'Close a task' => 'Fermer une tâche',
- 'Edit a task' => 'Modifier une tâche',
'Column' => 'Colonne',
'Color' => 'Couleur',
'Assignee' => 'Personne assignée',
diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php
index 32e34857..c5900fb5 100644
--- a/app/Locale/hu_HU/translations.php
+++ b/app/Locale/hu_HU/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Valóban törölni akarja ezt a projektet: "%s"?',
'Remove project' => 'Projekt törlése',
'Edit the board for "%s"' => 'Tábla szerkesztése: "%s"',
- 'All projects' => 'Minden projekt',
'Add a new column' => 'Új oszlop',
'Title' => 'Cím',
'Assigned to %s' => 'Felelős: %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(VACUUM parancs)',
'(Gzip compressed Sqlite file)' => '(Gzip tömörített SQLite fájl)',
'Close a task' => 'Feladat lezárása',
- 'Edit a task' => 'Feladat módosítása',
'Column' => 'Oszlop',
'Color' => 'Szín',
'Assignee' => 'Felelős',
diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php
index 9ecb0121..22b6a75e 100644
--- a/app/Locale/id_ID/translations.php
+++ b/app/Locale/id_ID/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Apakah anda yakin akan menghapus proyek ini : « %s » ?',
'Remove project' => 'Hapus proyek',
'Edit the board for "%s"' => 'Rubah papan untuk « %s »',
- 'All projects' => 'Semua proyek',
'Add a new column' => 'Tambah kolom baru',
'Title' => 'Judul',
'Assigned to %s' => 'Ditugaskan ke %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(perintah VACUUM)',
'(Gzip compressed Sqlite file)' => '(File Sqlite yang terkompress Gzip)',
'Close a task' => 'Tutup tugas',
- 'Edit a task' => 'Edit tugas',
'Column' => 'Kolom',
'Color' => 'Warna',
'Assignee' => 'Orang yang ditugaskan',
diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php
index b72ca181..1f6539b7 100644
--- a/app/Locale/it_IT/translations.php
+++ b/app/Locale/it_IT/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Vuoi davvero eliminare il seguente progetto: "%s" ?',
'Remove project' => 'Cancella il progetto',
'Edit the board for "%s"' => 'Modifica la bacheca per "%s"',
- 'All projects' => 'Tutti i progetti',
'Add a new column' => 'Aggiungi una nuova colonna',
'Title' => 'Titolo',
'Assigned to %s' => 'Assegnato a %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(Comando VACUUM)',
'(Gzip compressed Sqlite file)' => '(File Sqlite compresso in Gzip)',
'Close a task' => 'Chiudi un task',
- 'Edit a task' => 'Modifica un task',
'Column' => 'Colonna',
'Color' => 'Colore',
'Assignee' => 'Assegnatario',
diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php
index d3a11fc8..24dae922 100644
--- a/app/Locale/ja_JP/translations.php
+++ b/app/Locale/ja_JP/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'プロジェクト「%s」を本当に削除しますか?',
'Remove project' => 'プロジェクトの削除',
'Edit the board for "%s"' => 'ボード「%s」を変更する',
- 'All projects' => 'すべてのプロジェクト',
'Add a new column' => 'カラムの追加',
'Title' => 'タイトル',
'Assigned to %s' => '%sが担当',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(VACUUM コマンド)',
'(Gzip compressed Sqlite file)' => '(GZip コマンドで圧縮された Sqlite ファイル)',
'Close a task' => 'タスクをクロースする',
- 'Edit a task' => 'タスクを変更する',
'Column' => 'カラム',
'Color' => '色',
'Assignee' => '担当',
diff --git a/app/Locale/ko_KR/translations.php b/app/Locale/ko_KR/translations.php
index b8ec0c81..19b1f5fd 100644
--- a/app/Locale/ko_KR/translations.php
+++ b/app/Locale/ko_KR/translations.php
@@ -68,14 +68,13 @@ return array(
'Do you really want to remove this project: "%s"?' => '프로젝트를 삭제하시겠습니까: "%s"?',
'Remove project' => '프로젝트의 삭제',
'Edit the board for "%s"' => '"%s"를 위한 보드 수정',
- 'All projects' => '모든 프로젝트',
- 'Add a new column' => '칼럼의 추가',
+ 'Add a new column' => '컬럼의 추가',
'Title' => '제목',
'Assigned to %s' => '담당자 %s',
- 'Remove a column' => '칼럼 삭제',
- 'Remove a column from a board' => '보드에서 칼럼 삭제',
+ 'Remove a column' => '컬럼 삭제',
+ 'Remove a column from a board' => '보드에서 컬럼 삭제',
'Unable to remove this column.' => '(※)컬럼을 삭제할 수 없었습니다.',
- 'Do you really want to remove this column: "%s"?' => '칼럼을 삭제하시겠습니까: "%s"?',
+ 'Do you really want to remove this column: "%s"?' => '컬럼을 삭제하시겠습니까: "%s"?',
'This action will REMOVE ALL TASKS associated to this column!' => '이 조작은 이 컬럼에 할당된 『 모든 할일을 삭제 』합니다!',
'Settings' => '설정',
'Application settings' => '애플리케이션의 설정',
@@ -88,8 +87,7 @@ return array(
'(VACUUM command)' => '(VACUUM명령)',
'(Gzip compressed Sqlite file)' => '(GZip명령으로 압축된 Sqlite파일)',
'Close a task' => '할일 마치기',
- 'Edit a task' => '할일 수정',
- 'Column' => '칼럼',
+ 'Column' => '컬럼',
'Color' => '색',
'Assignee' => '담당자',
'Create another task' => '다른 할일 추가',
@@ -98,7 +96,7 @@ return array(
'Do you really want to open this task: "%s"?' => '할일은 시작 하시겠습니까: "%s"?',
'Back to the board' => '보드로 돌아가기',
'There is nobody assigned' => '담당자가 없습니다',
- 'Column on the board:' => '칼럼:',
+ 'Column on the board:' => '컬럼:',
'Close this task' => '할일 마치기',
'Open this task' => '할일을 열다',
'There is no description.' => '설명이 없다',
@@ -190,12 +188,12 @@ return array(
'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' => '할일을 다른 칼럼에 이동하는 ',
+ 'Move a task to another column' => '할일을 다른 컬럼에 이동하는 ',
'Task modification' => '할일 변경',
'Task creation' => '할일을 만들',
'Closing a task' => '할일을 닫혔다',
'Assign a color to a specific user' => '색을 사용자에 할당',
- 'Column title' => '칼럼의 제목',
+ 'Column title' => '컬럼의 제목',
'Position' => '위치',
'Duplicate to another project' => '다른 프로젝트에 복사',
'Duplicate' => '복사',
@@ -362,7 +360,7 @@ return array(
'%s updated the task %s' => '%s이 할일 %s을 갱신 하였습니다',
'%s opened the task %s' => '%s이 할일 %s을 시작시켰습니다',
'%s moved the task %s to the position #%d in the column "%s"' => '%s이 할일%s을 위치#%d컬럼%s로 옮겼습니다',
- '%s moved the task %s to the column "%s"' => '%s이 할일 %s을 칼럼 "%s" 로 옮겼습니다',
+ '%s 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의 서브 할일을 추가했습니다',
@@ -383,7 +381,7 @@ return array(
'%s opened the task #%d' => '%s이 할일#%d를 오픈했습니다',
'Activity' => '활동',
'Default values are "%s"' => '기본 값은 "%s" 입니다',
- 'Default columns for new projects (Comma-separated)' => '새로운 프로젝트의 기본 칼럼 (콤마(,)로 분리됨)',
+ 'Default columns for new projects (Comma-separated)' => '새로운 프로젝트의 기본 컬럼 (콤마(,)로 분리됨)',
'Task assignee change' => '담당자의 변경',
'%s changed the assignee of the task #%d to %s' => '%s이 할일 #%d의 담당을 %s로 변경합니다',
'%s changed the assignee of the task %s to %s' => '%s이 할일 %s의 담당을 %s로 변경했습니다',
@@ -428,7 +426,7 @@ return array(
'Create a comment from an external provider' => '외부 서비스로부터 의견을 작성한다',
'Project management' => '프로젝트 관리',
'My projects' => '내 프로젝트',
- 'Columns' => '칼럼',
+ 'Columns' => '컬럼',
'Task' => '할일',
'Your are not member of any project.' => '어떤 프로젝트에도 속하지 않습니다.',
'Percentage' => '비중',
@@ -486,16 +484,16 @@ return array(
'Application default' => '애플리케이션 기본',
'Language:' => '언어:',
'Timezone:' => '시간대:',
- 'All columns' => '모든 칼럼',
+ 'All columns' => '모든 컬럼',
'Calendar' => '달력',
'Next' => '다음에 ',
'#%d' => '#%d',
'All swimlanes' => '모든 스윔레인',
'All colors' => '모든 색',
- 'Moved to column %s' => '"%s" 칼럼으로 이동',
+ 'Moved to column %s' => '"%s" 컬럼으로 이동',
'User dashboard' => '대시보드',
'Allow only one subtask in progress at the same time for a user' => '한 사용자에 대한 하나의 할일만 진행 중에 가능합니다',
- 'Edit column "%s"' => '"%s" 칼럼 수정',
+ 'Edit column "%s"' => '"%s" 컬럼 수정',
'Select the new status of the subtask: "%s"' => '서브 할일의 새로운 상태 선택: "%s"',
'Subtask timesheet' => '서브 할일 타임시트',
'There is nothing to show.' => '기록이 없습니다',
@@ -506,7 +504,7 @@ return array(
'Start' => '시작',
'End' => '종료',
'Task age in days' => '할일이 생긴 시간',
- 'Days in this column' => '이 칼럼에 있는 시간',
+ 'Days in this column' => '이 컬럼에 있는 시간',
'%dd' => '%d일',
'Add a new link' => ' 새로운 링크 추가',
'Do you really want to remove this link: "%s"?' => '링크를 삭제하시겠습니까: "%s"?',
@@ -569,16 +567,16 @@ return array(
// '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' => '이동 전 칼럼',
+ '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' => '칼럼에 있던 시간',
+ '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.' => '이 리포트는 할일의 칼럼 간 이동을 시간, 유저, 경과 시간과 함께 기록한 것입니다.',
+ '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' => '현재의 기축 통화',
@@ -599,7 +597,7 @@ return array(
'Check my code' => '코드 체크',
'Secret key: ' => '비밀키: ',
'Test your device' => '디바이스 테스트',
- 'Assign a color when the task is moved to a specific column' => '상세 칼럼으로 이동할 할일의 색깔을 지정하세요',
+ 'Assign a color when the task is moved to a specific column' => '상세 컬럼으로 이동할 할일의 색깔을 지정하세요',
'%s via Kanboard' => '%s via E-board',
'Burndown chart' => '번다운 차트',
// 'This chart show the task complexity over the time (Work Remaining).' => '',
@@ -639,8 +637,8 @@ return array(
'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' => '할일이 마지막 칼럼으로 옮겨졌을때',
+ 'When task is moved from first column' => '할일이 첫번째 컬럼으로 옮겨졌을때',
+ 'When task is moved to last column' => '할일이 마지막 컬럼으로 옮겨졌을때',
'Year(s)' => '년',
'Calendar settings' => '달력 설정',
'Project calendar view' => '프로젝트 달력 보기',
@@ -661,8 +659,8 @@ return array(
'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' => '카테고리 변경시 할일을 다른 칼럼으로 이동',
+ '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' => '할일 다시 시작',
'Notification' => '알림',
@@ -698,7 +696,7 @@ return array(
'Only for tasks created by me' => '내가 만든 일',
'Only for tasks created by me and assigned to me' => '내가 만들었거나 내가 담당자인 일',
// '%%Y-%%m-%%d' => '',
- 'Total for all columns' => '모든 칼럼',
+ 'Total for all columns' => '모든 컬럼',
'You need at least 2 days of data to show the chart.' => '차트를 보기 위하여 최소 2일의 데이터가 필요합니다',
'<15m' => '<15분',
'<30m' => '<30분',
@@ -731,7 +729,7 @@ return array(
'Advanced search' => '검색 문법',
'Example of query: ' => '문법 예제 ',
'Search by project: ' => '프로젝트로 찾기 ',
- 'Search by column: ' => '칼럼으로 찾기 ',
+ 'Search by column: ' => '컬럼으로 찾기 ',
'Search by assignee: ' => '담당자로 찾기 ',
'Search by color: ' => '색깔로 찾기 ',
'Search by category: ' => '카테고리로 찾기 ',
@@ -739,18 +737,18 @@ return array(
'Search by due date: ' => '마감날짜로 찾기 ',
'Average time spent into each column' => '각 칼럼의 평균 소요시간',
'Average time spent' => '평균 소요시간',
- 'This chart show the average time spent into each column for the last %d tasks.' => '마지막 %d 할일의 칼럼 평균 소요시간을 차트에 표시합니다',
+ 'This chart show the average time spent into each column for the last %d tasks.' => '마지막 %d 할일의 컬럼 평균 소요시간을 차트에 표시합니다',
'Average Lead and Cycle time' => '평균 Lead and Cycle 시간',
'Average lead time: ' => '평균 lead 시간',
'Average cycle time: ' => '평균 cycle 시간',
'Cycle Time' => '사이클 시간',
'Lead Time' => '리드 시간',
'This chart show the average lead and cycle time for the last %d tasks over the time.' => '마지막 %d 할일의 평균 리드와 사이클 시간을 차트에 표시합니다',
- 'Average time into each column' => '각 칼럼의 평균 시간',
+ 'Average time into each column' => '각 컬럼의 평균 시간',
'Lead and cycle time' => '리드와 사이클 시간',
'Lead time: ' => '리드 시간: ',
'Cycle time: ' => '사이클 시간: ',
- 'Time spent into each column' => '각 칼럼에서 걸린 시간',
+ '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.' => '할일이 종료되지 않았다면, 완료 시간 대신 현재 시간이 사용됩니다',
@@ -767,7 +765,7 @@ return array(
'Trigger automatically subtask time tracking' => '자동 서브 할일 시간 트래킹 트리거',
'Include closed tasks in the cumulative flow diagram' => '누적 플로우 다이어그램에 종료된 할일을 포함합니다',
'Current swimlane: %s' => '현재 스웜라인: %s',
- 'Current column: %s' => '현재 칼럼: %s',
+ 'Current column: %s' => '현재 컬럼: %s',
'Current category: %s' => '현재 카테고리: %s',
'no category' => '카테고리 아님',
'Current assignee: %s' => '현재 할당자: %s',
@@ -789,8 +787,8 @@ return array(
'People who are project managers' => '프로젝트 매니저',
'People who are project members' => '프로젝트 멤버',
// 'NOK - Norwegian Krone' => '',
- 'Show this column' => '칼럼 보이기',
- 'Hide this column' => '칼럼 숨기기',
+ 'Show this column' => '컬럼 보이기',
+ 'Hide this column' => '컬럼 숨기기',
'open file' => '열기',
'End date' => '종료 날짜',
'Users overview' => '유저 전체보기',
@@ -836,7 +834,7 @@ return array(
'Task updated #%d' => '할일 #%d이 갱신 되었습니다',
'Task #%d closed' => '할일 #%d를 마쳤습니다',
'Task #%d opened' => '할일 #%d가 시작되었습니다',
- 'Column changed for task #%d' => '할일 #%d의 칼럼이 변경되었습니다',
+ 'Column changed for task #%d' => '할일 #%d의 컬럼이 변경되었습니다',
'New position for task #%d' => '할일 #%d이 새로운 위치에 등록되었습니다',
'Swimlane changed for task #%d' => '#%d 할일의 스웜라인이 변경됩니다',
'Assignee changed on task #%d' => '#%d 할일의 담당자가 변경됩니다',
@@ -867,7 +865,7 @@ return array(
'Double Quote' => '이중 따옴표',
'Single Quote' => '따옴표',
'%s attached a file to the task #%d' => '%s가 할일 #%d에 파일을 추가하였습니다',
- 'There is no column or swimlane activated in your project!' => '프로젝트에 활성화된 칼럼이나 스웜라인이 없습니다',
+ 'There is no column or swimlane activated in your project!' => '프로젝트에 활성화된 컬럼이나 스웜라인이 없습니다',
// 'Append filter (instead of replacement)' => '',
'Append/Replace' => '추가/변경',
'Append' => '추가',
@@ -953,8 +951,8 @@ return array(
'Actual Time' => '실제 시간',
'Estimated vs actual time' => '예상 vs 실제 시간',
// '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' => '상세 칼럼의 할일을 종료합니다',
+ '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: ' => '이중 인증: ',
'Disable two-factor authentication' => '이중 인증 비활성화',
@@ -972,10 +970,10 @@ return array(
'Creation' => '생성',
'Expiration' => '만료',
'Password reset history' => '비밀번호 초기화 기록',
- 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '칼럼 "%s"와 스웜라인 "%s"의 모든 할일이 성공적으로 종료되었습니다',
- '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.' => '칼럼 "%s"와 스웜라인 "%s"의 할일 %d가 종료될 것입니다',
- 'Close all tasks of this column' => '칼럼의 모든 할일 마치기',
+ 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '컬럼 "%s"와 스웜라인 "%s"의 모든 할일이 성공적으로 종료되었습니다',
+ '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.' => '컬럼 "%s"와 스웜라인 "%s"의 할일 %d가 종료될 것입니다',
+ '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' => '프로필',
@@ -1027,7 +1025,7 @@ return array(
'Reference:' => '참고:',
'Complexity:' => '복합:',
'Swimlane:' => '스웜라인:',
- 'Column:' => '칼럼:',
+ 'Column:' => '컬럼:',
'Position:' => '위치:',
'Creator:' => '생성자:',
'Time estimated:' => '예상 시간:',
@@ -1070,11 +1068,11 @@ return array(
'Uploaded by %s' => '%s로 올리기',
'Filename' => '파일 이름',
'Size' => '크기',
- 'Column created successfully.' => '칼럼이 성공적으로 생성되었습니다',
- 'Another column with the same name exists in the project' => '프로젝트에 동일한 이름의 칼럼이 있습니다',
+ 'Column created successfully.' => '컬럼이 성공적으로 생성되었습니다',
+ 'Another column with the same name exists in the project' => '프로젝트에 동일한 이름의 컬럼이 있습니다',
'Default filters' => '기본 필터',
- 'Your board doesn\'t have any columns!' => '보드에 칼럼이 존재하지 않습니다',
- 'Change column position' => '칼럼 위치 변경',
+ 'Your board doesn\'t have any columns!' => '보드에 컬럼이 존재하지 않습니다',
+ 'Change column position' => '컬럼 위치 변경',
'Switch to the project overview' => '프로젝트 개요로 변경',
'User filters' => '사용자 필터',
'Category filters' => '카테고리 필터',
@@ -1164,54 +1162,54 @@ return array(
'Email settings' => '이메일 설정',
'Email sender address' => '이메일 보낸이 주소',
'Email transport' => '이메일 전송',
- // 'Webhook token' => '',
+ 'Webhook token' => 'Webhook토큰',
'Imports' => '가져오기',
- // 'Project tags management' => '',
- // 'Tag created successfully.' => '',
- // 'Unable to create this tag.' => '',
- // 'Tag updated successfully.' => '',
- // 'Unable to update this tag.' => '',
- // 'Tag removed successfully.' => '',
- // 'Unable to remove this tag.' => '',
- // 'Global tags management' => '',
- // 'Tags' => '',
- // 'Tags management' => '',
- // 'Add new tag' => '',
- // 'Edit a tag' => '',
- // 'Project tags' => '',
- // 'There is no specific tag for this project at the moment.' => '',
- // 'Tag' => '',
- // 'Remove a tag' => '',
- // 'Do you really want to remove this tag: "%s"?' => '',
- // 'Global tags' => '',
- // 'There is no global tag at the moment.' => '',
- // 'This field cannot be empty' => '',
- // 'Close a task when there is no activity in an specific column' => '',
- // '%s removed a subtask for the task #%d' => '',
- // '%s removed a comment on the task #%d' => '',
- // 'Comment removed on task #%d' => '',
- // 'Subtask removed on task #%d' => '',
- // 'Hide tasks in this column in the dashboard' => '',
- // '%s removed a comment on the task %s' => '',
- // '%s removed a subtask for the task %s' => '',
- // 'Comment removed' => '',
- // 'Subtask removed' => '',
- // '%s set a new internal link for the task #%d' => '',
- // '%s removed an internal link for the task #%d' => '',
- // 'A new internal link for the task #%d have been defined' => '',
- // 'Internal link removed for the task #%d' => '',
- // '%s set a new internal link for the task %s' => '',
- // '%s removed an internal link for the task %s' => '',
- // 'Automatically set the due date on task creation' => '',
- // 'Move the task to another column when closed' => '',
- // 'Move the task to another column when not moved during a given period' => '',
- // 'Dashboard for %s' => '',
- // 'Tasks overview for %s' => '',
- // 'Subtasks overview for %s' => '',
- // 'Projects overview for %s' => '',
- // 'Activity stream for %s' => '',
- // 'Calendar for %s' => '',
- // 'Notifications for %s' => '',
- // 'Subtasks export' => '',
- // 'Tasks exportation' => '',
+ 'Project tags management' => '프로젝트 태그 관리',
+ 'Tag created successfully.' => '태그가 성공적으로 생성되었습니다.',
+ 'Unable to create this tag.' => '태그를 생성할 수 없습니다.',
+ 'Tag updated successfully.' => '태그가 성공적으로 수정되었습니다.',
+ 'Unable to update this tag.' => '태그를 수정할 수 없습니다.',
+ 'Tag removed successfully.' => '태그가 성공적으로 삭제되었습니다.',
+ 'Unable to remove this tag.' => '태그를 삭제할 수 없습니다.',
+ 'Global tags management' => '전역 태그 관리',
+ 'Tags' => '태그',
+ 'Tags management' => '태그 관리',
+ 'Add new tag' => '태그 추가',
+ 'Edit a tag' => '태그 수정',
+ 'Project tags' => '프로젝트 태그',
+ 'There is no specific tag for this project at the moment.' => '현재 이 프로젝트에는 태그가 없습니다.',
+ 'Tag' => '태그',
+ 'Remove a tag' => '태그 삭제',
+ 'Do you really want to remove this tag: "%s"?' => '태그를 삭제하시겠습니까: "%s"?',
+ 'Global tags' => '전역 태그',
+ 'There is no global tag at the moment.' => '현재 전역 태그가 없습니다.',
+ 'This field cannot be empty' => '이 필드는 비워둘 수 없습니다',
+ 'Close a task when there is no activity in an specific column' => '활동이 없는 컬럼의 할일 마치기',
+ '%s removed a subtask for the task #%d' => '%s가 할일 #%d의 서브 할일을 삭제하였습니다',
+ '%s removed a comment on the task #%d' => '%s가 할일 #%d의 댓글을 삭제하였습니다',
+ 'Comment removed on task #%d' => '할일 #%d의 댓글이 삭제되었습니다',
+ 'Subtask removed on task #%d' => '할일 #%d의 서브 할일이 삭제되었습니다',
+ 'Hide tasks in this column in the dashboard' => '대시보드 컬럼의 할일 숨기기',
+ '%s removed a comment on the task %s' => '%s가 할일 %s의 댓글을 삭제하였습니다',
+ '%s removed a subtask for the task %s' => '%s가 할일 %s의 서브 할일을 삭제하였습니다',
+ 'Comment removed' => '댓글이 삭제되었습니다',
+ 'Subtask removed' => '서브 할일이 삭제되었습니다',
+ '%s set a new internal link for the task #%d' => '%s가 할일 #%d의 새로운 내부 링크를 설정하였습니다',
+ '%s removed an internal link for the task #%d' => '%s가 할일 #%d의 새로운 내부 링크를 삭제하였습니다',
+ 'A new internal link for the task #%d have been defined' => '할일 #%d의 새로운 내부 링크가 정의되었습니다',
+ 'Internal link removed for the task #%d' => '할일 #%d의 새로운 내부 링크가 삭제되었습니다',
+ '%s set a new internal link for the task %s' => '%s가 할일 %s의 새로운 내부 링크를 설정하였습니다',
+ '%s removed an internal link for the task %s' => '%s가 할일 %s의 새로운 내부 링크를 삭제하였습니다',
+ 'Automatically set the due date on task creation' => '할일 생성시 마감일이 자동으로 설정되었습니다',
+ 'Move the task to another column when closed' => '할일을 마치면 다른 컬럼으로 이동시키기',
+ 'Move the task to another column when not moved during a given period' => '주어진 기간동안 이동하지 않으면 할일을 다른 컬럼으로 이동시키기',
+ 'Dashboard for %s' => '%s의 대시보드',
+ 'Tasks overview for %s' => '%s의 할일 개요',
+ 'Subtasks overview for %s' => '%s의 서브 할일 개요',
+ 'Projects overview for %s' => '%s의 프로젝트 개요',
+ 'Activity stream for %s' => '%s의 활동기록',
+ 'Calendar for %s' => '%s의 달력',
+ 'Notifications for %s' => '%s의 알림',
+ 'Subtasks export' => '서브할일 내보내기',
+ 'Tasks exportation' => '할일 내보내기',
);
diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php
index 79baadf7..661fe5ac 100644
--- a/app/Locale/my_MY/translations.php
+++ b/app/Locale/my_MY/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Anda yakin mahu menghapus projek ini : « %s » ?',
'Remove project' => 'Hapus projek',
'Edit the board for "%s"' => 'Ubah papan untuk « %s »',
- 'All projects' => 'Semua projek',
'Add a new column' => 'Tambah kolom baru',
'Title' => 'Judul',
'Assigned to %s' => 'Ditugaskan ke %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(perintah VACUUM)',
'(Gzip compressed Sqlite file)' => '(File Sqlite yang termampat Gzip)',
'Close a task' => 'Tutup tugas',
- 'Edit a task' => 'Sunting tugas',
'Column' => 'Kolom',
'Color' => 'Warna',
'Assignee' => 'Orang yang ditugaskan',
diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php
index 3a0c3353..86fb33a9 100644
--- a/app/Locale/nb_NO/translations.php
+++ b/app/Locale/nb_NO/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Vil du fjerne dette prosjektet: "%s"?',
'Remove project' => 'Fjern prosjekt',
'Edit the board for "%s"' => 'Endre prosjektsiden for "%s"',
- 'All projects' => 'Alle prosjekter',
'Add a new column' => 'Legg til en ny kolonne',
'Title' => 'Tittel',
'Assigned to %s' => 'Tildelt: %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(VACUUM kommando)',
'(Gzip compressed Sqlite file)' => '(Gzip-komprimert Sqlite fil)',
'Close a task' => 'Lukk en oppgave',
- 'Edit a task' => 'Endre en oppgave',
'Column' => 'Kolonne',
'Color' => 'Farge',
'Assignee' => 'Tildelt',
diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php
index 5a026092..43bd32d3 100644
--- a/app/Locale/nl_NL/translations.php
+++ b/app/Locale/nl_NL/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Weet u zeker dat u dit project wil verwijderen : « %s » ?',
'Remove project' => 'Project verwijderen',
'Edit the board for "%s"' => 'Bord bewerken voor « %s »',
- 'All projects' => 'Alle projecten',
'Add a new column' => 'Kolom toevoegen',
'Title' => 'Titel',
'Assigned to %s' => 'Toegewezen aan %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(VACUUM commando)',
'(Gzip compressed Sqlite file)' => '(Gzip ingepakt Sqlite bestand)',
'Close a task' => 'Taak sluiten',
- 'Edit a task' => 'Taak bewerken',
'Column' => 'Kolom',
'Color' => 'Kleur',
'Assignee' => 'Toegewezene',
diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php
index af3bcd4e..c19ebbbe 100644
--- a/app/Locale/pl_PL/translations.php
+++ b/app/Locale/pl_PL/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Na pewno chcesz usunąć projekt: "%s"?',
'Remove project' => 'Usuń projekt',
'Edit the board for "%s"' => 'Edytuj tablicę dla "%s"',
- 'All projects' => 'Wszystkie projekty',
'Add a new column' => 'Dodaj nową kolumnę',
'Title' => 'Nazwa',
'Assigned to %s' => 'Przypisane do %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(komenda VACUUM)',
'(Gzip compressed Sqlite file)' => '(baza danych spakowana Gzip)',
'Close a task' => 'Zakończ zadanie',
- 'Edit a task' => 'Edytuj zadanie',
'Column' => 'Kolumna',
'Color' => 'Kolor',
'Assignee' => 'Odpowiedzialny',
diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php
index 2772d8bb..f4ed4f62 100644
--- a/app/Locale/pt_BR/translations.php
+++ b/app/Locale/pt_BR/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Você realmente deseja remover este projeto: "%s"?',
'Remove project' => 'Remover projeto',
'Edit the board for "%s"' => 'Editar o board para "%s"',
- 'All projects' => 'Todos os projetos',
'Add a new column' => 'Adicionar uma nova coluna',
'Title' => 'Título',
'Assigned to %s' => 'Designado para %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(Comando VACUUM)',
'(Gzip compressed Sqlite file)' => '(Arquivo Sqlite comprimido com Gzip)',
'Close a task' => 'Finalizar uma tarefa',
- 'Edit a task' => 'Editar uma tarefa',
'Column' => 'Coluna',
'Color' => 'Cor',
'Assignee' => 'Designação',
diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php
index ebc26cd7..1925eef2 100644
--- a/app/Locale/pt_PT/translations.php
+++ b/app/Locale/pt_PT/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Tem a certeza que quer remover este projecto: "%s" ?',
'Remove project' => 'Remover projecto',
'Edit the board for "%s"' => 'Editar o quadro para "%s"',
- 'All projects' => 'Todos os projectos',
'Add a new column' => 'Adicionar uma nova coluna',
'Title' => 'Título',
'Assigned to %s' => 'Designado para %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(Comando VACUUM)',
'(Gzip compressed Sqlite file)' => '(Arquivo Sqlite comprimido com Gzip)',
'Close a task' => 'Finalizar uma tarefa',
- 'Edit a task' => 'Editar uma tarefa',
'Column' => 'Coluna',
'Color' => 'Cor',
'Assignee' => 'Assignado',
diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php
index f3ec5af7..74563ad4 100644
--- a/app/Locale/ru_RU/translations.php
+++ b/app/Locale/ru_RU/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Вы точно хотите удалить проект: "%s"?',
'Remove project' => 'Удалить проект',
'Edit the board for "%s"' => 'Изменить доску для "%s"',
- 'All projects' => 'Все проекты',
'Add a new column' => 'Добавить новую колонку',
'Title' => 'Название',
'Assigned to %s' => 'Назначено %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(Команда VACUUM)',
'(Gzip compressed Sqlite file)' => '(Сжать GZip файл SQLite)',
'Close a task' => 'Закрыть задачу',
- 'Edit a task' => 'Изменить задачу',
'Column' => 'Колонка',
'Color' => 'Цвет',
'Assignee' => 'Назначена',
diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php
index 7e28e9a9..18dbbc65 100644
--- a/app/Locale/sr_Latn_RS/translations.php
+++ b/app/Locale/sr_Latn_RS/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Da li želiš da ukloniš projekat: "%s"?',
'Remove project' => 'Ukloni projekat',
'Edit the board for "%s"' => 'Izmeni tablu za "%s"',
- 'All projects' => 'Svi projekti',
'Add a new column' => 'Dodaj novu kolonu',
'Title' => 'Naslov',
'Assigned to %s' => 'Dodeljen korisniku %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(komanda VACUUM)',
'(Gzip compressed Sqlite file)' => '(Sqlite baza spakovana Gzip-om)',
'Close a task' => 'Zatvori zadatak',
- 'Edit a task' => 'Izmeni zadatak',
'Column' => 'Kolona',
'Color' => 'Boja',
'Assignee' => 'Dodeli',
diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php
index 5ad2938c..f7d1a4e4 100644
--- a/app/Locale/sv_SE/translations.php
+++ b/app/Locale/sv_SE/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Vill du verkligen ta bort projektet: "%s" ?',
'Remove project' => 'Ta bort projekt',
'Edit the board for "%s"' => 'Ändra tavlan för "%s"',
- 'All projects' => 'Alla projekt',
'Add a new column' => 'Lägg till ny kolumn',
'Title' => 'Titel',
'Assigned to %s' => 'Tilldelad %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(Vacuum kommando)',
'(Gzip compressed Sqlite file)' => '(Gzip komprimera Sqlite filen)',
'Close a task' => 'Stäng en uppgift',
- 'Edit a task' => 'Ändra en uppgift',
'Column' => 'Kolumn',
'Color' => 'Färg',
'Assignee' => 'Uppdragsinnehavare',
diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php
index 2aee696b..825dab93 100644
--- a/app/Locale/th_TH/translations.php
+++ b/app/Locale/th_TH/translations.php
@@ -42,11 +42,11 @@ return array(
'Sign in' => 'เข้าสู่ระบบ',
'Users' => 'ผู้ใช้',
'No user' => 'ไม่มีผู้ใช้',
- 'Forbidden' => 'ไม่อนุญาติ',
- 'Access Forbidden' => 'ไม่อนุญาติให้เข้า',
+ 'Forbidden' => 'ไม่อนุญาต',
+ 'Access Forbidden' => 'ไม่อนุญาตให้เข้า',
'Edit user' => 'แก้ไขผู้ใช้',
'Logout' => 'ออกจากระบบ',
- 'Bad username or password' => 'ชื่อผู้ใช่หรือรหัสผ่านผิด',
+ 'Bad username or password' => 'ชื่อผู้ใช้หรือรหัสผ่านผิด',
'Edit project' => 'แก้ไขโปรเจค',
'Name' => 'ชื่อ',
'Projects' => 'โปรเจค',
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'คุณต้องการเอาโปรเจค « %s » ออกใช่หรือไม่?',
'Remove project' => 'ลบโปรเจค',
'Edit the board for "%s"' => 'แก้ไขบอร์ดสำหรับ « %s »',
- 'All projects' => 'โปรเจคทั้งหมด',
'Add a new column' => 'เพิ่มคอลัมน์ใหม่',
'Title' => 'หัวเรื่อง',
'Assigned to %s' => 'กำหนดให้ %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(VACUUM command)',
'(Gzip compressed Sqlite file)' => '(Gzip compressed Sqlite file)',
'Close a task' => 'ปิดงาน',
- 'Edit a task' => 'แก้ไขงาน',
'Column' => 'คอลัมน์',
'Color' => 'สี',
'Assignee' => 'กำหนดให้',
@@ -213,7 +211,7 @@ return array(
'Last logins' => 'เข้าใช้ล่าสุด',
'Login date' => 'วันที่เข้าใข้',
'Authentication method' => 'วิธีการยืนยันตัวตน',
- 'IP address' => 'ไอพี แอดเดรส',
+ 'IP address' => 'ที่อยู่ไอพี',
'User agent' => 'User agent',
'Persistent connections' => 'Persistent connections',
'No session.' => 'No session.',
@@ -230,35 +228,35 @@ return array(
'Description' => 'คำอธิบาย',
'%d comments' => '%d ความคิดเห็น',
'%d comment' => '%d ความคิดเห็น',
- 'Email address invalid' => 'อีเมลผิด',
- 'Your external account is not linked anymore to your profile.' => 'บัญชีภายนอกของคุณไม่ได้เชื่อมโยงอีกต่อไปในโปรไฟล์ของคุณ',
+ '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.' => 'บัญชีภายนอกของคุณลิงค์กับโปรไฟล์ของคุณเรียบร้อย',
+ 'Your external account is linked to your profile successfully.' => 'ทำการเชื่อมโยงบัญชีภายนอกของคุณกับโปรไฟล์ของคุณเรียบร้อย',
'Email' => 'อีเมล',
'Task removed successfully.' => 'ลบงานเรียบร้อยแล้ว',
'Unable to remove this task.' => 'ไม่สามารถลบงานนี้',
- 'Remove a task' => 'ลบงาาน',
+ 'Remove a task' => 'ลบงาน',
'Do you really want to remove this task: "%s"?' => 'คุณต้องการลบงาน "%s" ออกใช่หรือไม่?',
'Assign automatically a color based on a category' => 'กำหนดสีอัตโนมัติขึ้นอยู่กับหมวด',
'Assign automatically a category based on a color' => 'กำหนดหมวดอัตโนมัติขึ้นอยู่กับสี',
'Task creation or modification' => 'สร้างหรือแก้ไขงาน',
- 'Category' => 'หมวด',
- 'Category:' => 'หมวด:',
- 'Categories' => 'หมวด',
- 'Your category have been created successfully.' => 'สร้างหมวดเรียบร้อยแล้ว',
- 'Unable to create your category.' => 'ไม่สามารถสร้างหมวดได้',
+ 'Category' => 'หมวดหมู่',
+ 'Category:' => 'หมวดหมู่:',
+ 'Categories' => 'หมวดหมู่',
+ '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' => 'ลบหมวด',
+ 'Unable to update your category.' => 'ไม่สามารถปรับปรุงหมวดหมู่ได้',
+ 'Remove a category' => 'ลบหมวดหมู่',
'Category removed successfully.' => 'ลบหมวดเรียบร้อยแล้ว',
- 'Unable to remove this category.' => 'ไม่สามารถลบหมวดได้',
- 'Category modification for the project "%s"' => 'แก้ไขหมวดสำหรับโปรเจค "%s"',
- 'Category Name' => 'ชื่อหมวด',
- 'Add a new category' => 'เพิ่มหมวดใหม่',
- 'Do you really want to remove this category: "%s"?' => 'คุณต้องการลบหมวด "%s" ใช่หรือไม่?',
- 'All categories' => 'หมวดทั้งหมด',
- 'No category' => 'ไม่มีหมวด',
+ 'Unable to remove this category.' => 'ไม่สามารถลบหมวดหมู่ได้',
+ 'Category modification for the project "%s"' => 'แก้ไขหมวดหมู่สำหรับโปรเจค "%s"',
+ 'Category Name' => 'ชื่อหมวดหมู่',
+ 'Add a new category' => 'เพิ่มหมวดหมู่ใหม่',
+ 'Do you really want to remove this category: "%s"?' => 'คุณต้องการลบหมวดหมู่ "%s" ใช่หรือไม่?',
+ 'All categories' => 'หมวดหมู่ทั้งหมด',
+ 'No category' => 'ไม่มีหมวดหมู่',
'The name is required' => 'ต้องการชื่อ',
'Remove a file' => 'ลบไฟล์',
'Unable to remove this file.' => 'ไม่สามารถลบไฟล์ได้',
@@ -294,7 +292,7 @@ return array(
'Unable to update your sub-task.' => 'ไม่สามารถปรับปรุงานย่อยได้',
'Unable to create your sub-task.' => 'ไม่สามารถสร้างงานย่อยได้',
'Sub-task added successfully.' => 'เพิ่มงานย่อยเรียบร้อยแล้ว',
- 'Maximum size: ' => 'ขนาดสูงสุด:',
+ 'Maximum size: ' => 'ขนาดไฟล์สูงสุด:',
'Unable to upload the file.' => 'ไม่สามารถอัพโหลดไฟล์ได้',
'Display another project' => 'แสดงโปรเจคอื่น',
'Created by %s' => 'สร้างโดย %s',
@@ -355,38 +353,38 @@ return array(
'Password modification' => 'แก้ไขรหัสผ่าน',
'External authentications' => 'การยืนยันภายนอก',
'Never connected.' => 'ไม่เชื่อมต่อ',
- 'No external authentication enabled.' => 'ไม่เปิดการใช้งานการยืนยันภายนอก',
- 'Password modified successfully.' => 'แก้ไขรหัสผ่านเรียบร้อยแล้ว',
+ 'No external authentication enabled.' => 'ปิดการใช้งานการยืนยันภายนอก',
+ 'Password modified successfully.' => 'แก้ไขรหัสผ่านเรียบร้อย',
'Unable to change the password.' => 'ไม่สามารถเปลี่ยนรหัสผ่านได้',
- 'Change category' => 'เปลี่ยนหมวด',
- '%s updated the task %s' => '%s ปรับปรุงงานแล้ว %s',
- '%s opened the task %s' => '%s เปิดงานแล้ว %s',
- '%s moved the task %s to the position #%d in the column "%s"' => '%s ย้ายงานแล้ว %s ไปตำแหน่ง #%d ในคอลัมน์ "%s"',
- '%s moved the task %s to the column "%s"' => '%s ย้ายงานแล้ว %s ไปคอลัมน์ "%s"',
- '%s created the task %s' => '%s สร้างงานแล้ว %s',
- '%s closed the task %s' => '%s ปิดงานแล้ว %s',
- '%s created a subtask for the task %s' => '%s สร้างงานย่อยสำหรับงานแล้ว %s',
- '%s updated a subtask for the task %s' => '%s ปรับปรุงงานย่อยสำหรับงานแล้ว %s',
- 'Assigned to %s with an estimate of %s/%sh' => 'กำหนดให้ %s โดยประมาณแล้ว %s/%sh',
- 'Not assigned, estimate of %sh' => 'ไม่กำหนดแล้ว, ประมาณเวลาที่ใช้ %s ชั่วโมง',
- '%s updated a comment on the task %s' => '%s ปรับปรุงความคิดเห็นในงานแล้ว %s',
- '%s commented the task %s' => '%s แสดงความคิดเห็นของงานแล้ว %s',
+ 'Change category' => 'เปลี่ยนหมวดหมู่',
+ '%s updated the task %s' => '%s ได้ปรับปรุงงาน %s',
+ '%s opened the task %s' => '%s ได้สร้างงาน %s',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s ได้ย้ายงาน %s ไปยังตำแหน่ง #%d ในคอลัมน์ "%s"',
+ '%s moved the task %s to the column "%s"' => '%s ได้ย้ายงาน %s ไปยังคอลัมน์ "%s"',
+ '%s created the task %s' => '%s ได้สร้างงาน %s',
+ '%s closed the task %s' => '%s ได้ปิดงาน %s',
+ '%s created a subtask for the task %s' => '%s ได้สร้างงานย่อยสำหรับงาน %s',
+ '%s updated a subtask for the task %s' => '%s ได้ปรับปรุงงานย่อยสำหรับงาน %s',
+ 'Assigned to %s with an estimate of %s/%sh' => 'มอบหมายให้ %s โดยประมาณเวลาที่ใช้ %s/%sh',
+ 'Not assigned, estimate of %sh' => 'ไม่ระบุผู้รับผิดชอบ, ประมาณเวลาที่ใช้ %s ชั่วโมง',
+ '%s updated a comment on the task %s' => '%s ได้ปรับปรุงความคิดเห็นในงาน %s',
+ '%s commented the task %s' => '%s ได้แสดงความคิดเห็นในงาน %s',
'%s\'s activity' => 'กิจกรรม %s',
'RSS feed' => 'RSS feed',
- '%s updated a comment on the task #%d' => '%s ปรับปรุงความคิดเห็นบนงานแล้ว #%d',
- '%s commented on the task #%d' => '%s แสดงความคิดเห็นบนงานแล้ว #%d',
- '%s updated a subtask for the task #%d' => '%s ปรับปรุงงานย่อยสำหรับงานแล้ว #%d',
- '%s created a subtask for the task #%d' => '%s สร้างงานย่อยสำหรับงานแล้ว #%d',
- '%s updated the task #%d' => '%s ปรับปรุงงานแล้ว #%d',
- '%s created the task #%d' => '%s สร้างงานแล้ว #%d',
- '%s closed the task #%d' => '%s ปิดงานแล้ว #%d',
- '%s opened the task #%d' => '%s เปิดงานแล้ว #%d',
+ '%s updated a comment on the task #%d' => '%s ได้ปรับปรุงความคิดเห็นในงาน #%d',
+ '%s commented on the task #%d' => '%s ได้แสดงความคิดเห็นบนงาน #%d',
+ '%s updated a subtask for the task #%d' => '%s ได้ปรับปรุงงานย่อยสำหรับงาน #%d',
+ '%s created a subtask for the task #%d' => '%s ได้สร้างงานย่อยสำหรับงาน #%d',
+ '%s updated the task #%d' => '%s ได้ปรับปรุงงาน #%d',
+ '%s created the task #%d' => '%s ได้สร้างงาน #%d',
+ '%s closed the task #%d' => '%s ได้ปิดงาน #%d',
+ '%s opened the task #%d' => '%s ได้เปิดงาน #%d',
'Activity' => 'กิจกรรม',
'Default values are "%s"' => 'ค่าเริ่มต้น "%s"',
'Default columns for new projects (Comma-separated)' => 'คอลัมน์เริ่มต้นสำหรับโปรเจคใหม่ (Comma-separated)',
'Task assignee change' => 'เปลี่ยนการกำหนดบุคคลของงาน',
- '%s changed the assignee of the task #%d to %s' => '%s เปลี่ยนผู้รับผิดชอบของงาน #%d เป็น %s',
- '%s changed the assignee of the task %s to %s' => '%s เปลี่ยนผู้รับผิดชอบของงาน %s เป็น %s',
+ '%s changed the assignee of the task #%d to %s' => '%s ได้เปลี่ยนผู้รับผิดชอบงาน #%d เป็น %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' => 'สร้างงานจากบริการภายนอก',
@@ -401,8 +399,8 @@ return array(
// 'Webhook settings' => '',
// 'Reset token' => '',
// 'API endpoint:' => '',
- 'Refresh interval for private board' => 'ระยะรีเฟรชบอร์ดส่วนตัว',
- 'Refresh interval for public board' => 'ระยะรีเฟรชบอร์ดสาธารณะ',
+ '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)' => 'ช่วงเวลา (เป็นวินาที) ใช้ในการตัดสินใจว่าเป็นการแก้ไขเร็วๆ นี้ (0 ไม่ใช้งาน, ค่าเริ่มต้น 2 วัน)',
'Frequency in second (60 seconds by default)' => 'ความถี่ (ค่าเริ่มต้นทุก 60 วินาที) ',
@@ -430,7 +428,7 @@ return array(
'My projects' => 'โปรเจคของฉัน',
'Columns' => 'คอลัมน์',
'Task' => 'งาน',
- 'Your are not member of any project.' => 'คุณไม่ได้เป็นสมาชิกของโปรเจค',
+ 'Your are not member of any project.' => 'คุณไม่ได้เป็นสมาชิกของโปรเจคใดๆ',
'Percentage' => 'เปอร์เซ็นต์',
'Number of tasks' => 'จำนวนงาน',
'Task distribution' => 'การกระจายงาน',
@@ -438,18 +436,18 @@ return array(
'Subtask' => 'งานย่อย',
'My subtasks' => 'งานย่อยของฉัน',
'User repartition' => 'การแบ่งงานของผู้ใช้',
- 'Clone this project' => 'เลียนแบบโปรเจคนี้',
+ 'Clone this project' => 'สำเนาโปรเจคนี้',
'Column removed successfully.' => 'ลบคอลัมน์สำเร็จ',
- 'Not enough data to show the graph.' => 'ไม่มีข้อมูลแสดงเป็นกราฟ',
+ 'Not enough data to show the graph.' => 'ไม่มีข้อมูลเพียงพอสำหรับการแสดงกราฟ',
'Previous' => 'ก่อนหน้า',
'The id must be an integer' => 'ไอดีต้องเป็นตัวเลขจำนวนเต็ม',
- 'The project id must be an integer' => 'ไอดีโปรเจคต้องเป็นตัวเลข',
- 'The status must be an integer' => 'สถานะต้องเป็นตัวเลข',
+ 'The project id must be an integer' => 'ไอดีโปรเจคต้องเป็นตัวเลขเท่านั้น',
+ 'The status must be an integer' => 'สถานะต้องเป็นตัวเลขเท่านั้น',
'The subtask id is required' => 'ต้องการงานย่อย',
- 'The subtask id must be an integer' => 'ไอดีงานย่อยต้องเป็นตัวเลข',
+ 'The subtask id must be an integer' => 'ไอดีงานย่อยต้องเป็นตัวเลขเท่านั้น',
'The task id is required' => 'ต้องการไอดีงาน',
- 'The task id must be an integer' => 'ไอดีงานต้องเป็นตัวเลข',
- 'The user id must be an integer' => 'ไอดีผู้ใช้ต้องเป็นตัวเลข',
+ 'The task id must be an integer' => 'ไอดีงานต้องเป็นตัวเลขเท่านั้น',
+ 'The user id must be an integer' => 'ไอดีผู้ใช้ต้องเป็นตัวเลขเท่านั้น',
'This value is required' => 'ต้องการค่านี้',
'This value must be numeric' => 'ค่านี้ต้องเป็นตัวเลข',
'Unable to create this task.' => 'ไม่สามารถสร้างงานนี้',
@@ -478,7 +476,7 @@ return array(
'Default categories for new projects (Comma-separated)' => 'ค่าเริ่มต้นหมวดสำหรับโปรเจคใหม่ (Comma-separated)',
'Integrations' => 'การใช้ร่วมกัน',
'Integration with third-party services' => 'การใช้งานร่วมกับบริการ third-party',
- 'Subtask Id' => 'รหัสงานย่อย',
+ 'Subtask Id' => 'ไอดีของงานย่อย',
'Subtasks' => 'งานย่อย',
'Subtasks Export' => 'ส่งออก งานย่อย',
'Task Title' => 'ชื่องาน',
@@ -511,7 +509,7 @@ return array(
'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?',
- 'Field required' => 'ต้องใส่',
+ 'Field required' => 'จำเป็นต้องใส่',
'Link added successfully.' => 'เพิ่มลิงค์เรียบร้อยแล้ว',
'Link updated successfully.' => 'ปรับปรุงลิงค์เรียบร้อยแล้ว',
'Link removed successfully.' => 'ลบลิงค์เรียบร้อยแล้ว',
@@ -931,11 +929,11 @@ return array(
'Remove this user' => 'เอาผู้ใช้คนนี้ออก',
'Permissions' => 'การอนุญาตใช้งาน',
'Allowed Users' => 'การอนุญาตผู้ใช้',
- 'No user have been allowed specifically.' => 'ไม่มีผู้ใช้ได้รับอนุญาติเป็นพิเศษ',
+ 'No user have been allowed specifically.' => 'ไม่มีผู้ใช้ได้รับอนุญาตเป็นพิเศษ',
'Role' => 'บทบาท',
'Enter user name...' => 'พิมพ์ชื่อผู้ใช้...',
'Allowed Groups' => 'อนุญาตกลุ่ม',
- 'No group have been allowed specifically.' => 'ไม่มีกลุ่มได้รับอนุญาติเป็นพิเศษ',
+ 'No group have been allowed specifically.' => 'ไม่มีกลุ่มได้รับอนุญาตเป็นพิเศษ',
'Group' => 'กลุ่ม',
'Group Name' => 'ชื่อกลุ่ม',
'Enter group name...' => 'พิมพ์ชื่อกลุ่ม...',
diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php
index 69642b58..7eb5c2aa 100644
--- a/app/Locale/tr_TR/translations.php
+++ b/app/Locale/tr_TR/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => 'Bu projeyi gerçekten silmek istiyor musunuz: "%s"?',
'Remove project' => 'Projeyi sil',
'Edit the board for "%s"' => 'Tabloyu "%s" için güncelle',
- 'All projects' => 'Tüm projeler',
'Add a new column' => 'Yeni sütun ekle',
'Title' => 'Başlık',
'Assigned to %s' => '%s kullanıcısına atanmış',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(VACUUM komutu)',
'(Gzip compressed Sqlite file)' => '(Gzip ile sıkıştırılmış Sqlite dosyası)',
'Close a task' => 'Bir görevi kapat',
- 'Edit a task' => 'Bir görevi düzenle',
'Column' => 'Sütun',
'Color' => 'Renk',
'Assignee' => 'Atanan',
diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php
index b4e9c063..f28434d2 100644
--- a/app/Locale/zh_CN/translations.php
+++ b/app/Locale/zh_CN/translations.php
@@ -68,7 +68,6 @@ return array(
'Do you really want to remove this project: "%s"?' => '确定要移除项目"%s"吗?',
'Remove project' => '移除项目',
'Edit the board for "%s"' => '为"%s"修改看板',
- 'All projects' => '所有项目',
'Add a new column' => '添加新栏目',
'Title' => '标题',
'Assigned to %s' => '指派给 %s',
@@ -88,7 +87,6 @@ return array(
'(VACUUM command)' => '(VACUUM 指令)',
'(Gzip compressed Sqlite file)' => '(用Gzip压缩的Sqlite文件)',
'Close a task' => '关闭一个任务',
- 'Edit a task' => '修改一个任务',
'Column' => '栏目',
'Color' => '颜色',
'Assignee' => '负责人',
diff --git a/app/Model/ActionParameterModel.php b/app/Model/ActionParameterModel.php
index 9895da0f..cdac396e 100644
--- a/app/Model/ActionParameterModel.php
+++ b/app/Model/ActionParameterModel.php
@@ -157,6 +157,9 @@ class ActionParameterModel extends Base
case 'user_id':
case 'owner_id':
return $this->projectPermissionModel->isAssignable($project_id, $value) ? $value : false;
+ case 'swimlane_id':
+ $column = $this->swimlaneModel->getById($value);
+ return empty($column) ? false : $this->swimlaneModel->getIdByName($project_id, $column['name']) ?: false;
default:
return $value;
}
diff --git a/app/Model/UserMetadataModel.php b/app/Model/UserMetadataModel.php
index e931d3ba..42fe4c6d 100644
--- a/app/Model/UserMetadataModel.php
+++ b/app/Model/UserMetadataModel.php
@@ -10,6 +10,9 @@ namespace Kanboard\Model;
*/
class UserMetadataModel extends MetadataModel
{
+ const KEY_COMMENT_SORTING_DIRECTION = 'comment.sorting.direction';
+ const KEY_BOARD_COLLAPSED = 'board.collapsed.';
+
/**
* Get the table
*
diff --git a/app/ServiceProvider/ActionProvider.php b/app/ServiceProvider/ActionProvider.php
index 946fbf41..d46562b7 100644
--- a/app/ServiceProvider/ActionProvider.php
+++ b/app/ServiceProvider/ActionProvider.php
@@ -36,6 +36,8 @@ use Kanboard\Action\TaskOpen;
use Kanboard\Action\TaskUpdateStartDate;
use Kanboard\Action\TaskCloseNoActivity;
use Kanboard\Action\TaskCloseNoActivityColumn;
+use Kanboard\Action\TaskAssignColorSwimlane;
+use Kanboard\Action\TaskAssignPrioritySwimlane;
/**
* Action Provider
@@ -86,6 +88,8 @@ class ActionProvider implements ServiceProviderInterface
$container['actionManager']->register(new TaskOpen($container));
$container['actionManager']->register(new TaskUpdateStartDate($container));
$container['actionManager']->register(new TaskAssignDueDateOnCreation($container));
+ $container['actionManager']->register(new TaskAssignColorSwimlane($container));
+ $container['actionManager']->register(new TaskAssignPrioritySwimlane($container));
return $container;
}
diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php
index 978bc05b..adff1e63 100644
--- a/app/ServiceProvider/AuthenticationProvider.php
+++ b/app/ServiceProvider/AuthenticationProvider.php
@@ -151,7 +151,7 @@ class AuthenticationProvider implements ServiceProviderInterface
$acl->add('UserCreationController', '*', Role::APP_ADMIN);
$acl->add('UserListController', '*', Role::APP_ADMIN);
$acl->add('UserStatusController', '*', Role::APP_ADMIN);
- $acl->add('UserCredentialController', array('changeAuthentication', 'saveAuthentication'), Role::APP_ADMIN);
+ $acl->add('UserCredentialController', array('changeAuthentication', 'saveAuthentication', 'unlock'), Role::APP_ADMIN);
return $acl;
}
diff --git a/app/ServiceProvider/CacheProvider.php b/app/ServiceProvider/CacheProvider.php
new file mode 100644
index 00000000..fac44d53
--- /dev/null
+++ b/app/ServiceProvider/CacheProvider.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Kanboard\Core\Cache\FileCache;
+use Kanboard\Core\Cache\MemoryCache;
+use Kanboard\Decorator\MetadataCacheDecorator;
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+
+/**
+ * Cache Provider
+ *
+ * @package Kanboard\ServiceProvider
+ * @author Frederic Guillot
+ */
+class CacheProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['memoryCache'] = function() {
+ return new MemoryCache();
+ };
+
+ if (CACHE_DRIVER === 'file') {
+ $container['cacheDriver'] = function() {
+ return new FileCache();
+ };
+ } else {
+ $container['cacheDriver'] = $container['memoryCache'];
+ }
+
+ $container['userMetadataCacheDecorator'] = function($c) {
+ return new MetadataCacheDecorator(
+ $c['cacheDriver'],
+ $c['userMetadataModel'],
+ 'user.metadata.',
+ $c['userSession']->getId()
+ );
+ };
+
+ return $container;
+ }
+}
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index aab41c74..d837500a 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -140,9 +140,6 @@ class ClassProvider implements ServiceProviderInterface
'Response',
'RememberMeCookie',
),
- 'Core\Cache' => array(
- 'MemoryCache',
- ),
'Core\Plugin' => array(
'Hook',
),
diff --git a/app/Template/action/index.php b/app/Template/action/index.php
index 085ea3ad..7768a0b6 100644
--- a/app/Template/action/index.php
+++ b/app/Template/action/index.php
@@ -54,6 +54,8 @@
<?= $this->text->in($param_value, $categories_list) ?>
<?php elseif ($this->text->contains($param_name, 'link_id')): ?>
<?= $this->text->in($param_value, $links_list) ?>
+ <?php elseif ($this->text->contains($param_name, 'swimlane_id')): ?>
+ <?= $this->text->in($param_value, $swimlane_list) ?>
<?php else: ?>
<?= $this->text->e($param_value) ?>
<?php endif ?>
diff --git a/app/Template/action_creation/params.php b/app/Template/action_creation/params.php
index fa892177..c9608f21 100644
--- a/app/Template/action_creation/params.php
+++ b/app/Template/action_creation/params.php
@@ -41,6 +41,9 @@
<?php elseif ($this->text->contains($param_name, 'duration')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
<?= $this->form->number('params['.$param_name.']', $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'swimlane_id')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $swimlane_list, $values) ?>
<?php else: ?>
<?= $this->form->label($param_desc, $param_name) ?>
<?= $this->form->text('params['.$param_name.']', $values) ?>
diff --git a/app/Template/board/task_footer.php b/app/Template/board/task_footer.php
index bc34363c..650fe559 100644
--- a/app/Template/board/task_footer.php
+++ b/app/Template/board/task_footer.php
@@ -81,8 +81,10 @@
</span>
<?php endif ?>
- <?php if (! empty($task['time_estimated'])): ?>
- <span class="task-time-estimated" title="<?= t('Time estimated') ?>"><?= $this->text->e($task['time_estimated']).'h' ?></span>
+ <?php if (! empty($task['time_estimated']) || ! empty($task['time_spent'])): ?>
+ <span class="task-time-estimated" title="<?= t('Time spent and estimated') ?>">
+ <?= $this->text->e($task['time_spent']) ?>/<?= $this->text->e($task['time_estimated']) ?>h
+ </span>
<?php endif ?>
<?php if ($task['is_milestone'] == 1): ?>
diff --git a/app/Template/dashboard/notifications.php b/app/Template/dashboard/notifications.php
index a189d74f..4fb59e24 100644
--- a/app/Template/dashboard/notifications.php
+++ b/app/Template/dashboard/notifications.php
@@ -15,13 +15,27 @@
<table class="table-striped table-scrolling table-small">
<tr>
+ <th class="column-20"><?= t('Project') ?></th>
<th><?= t('Notification') ?></th>
- <th class="column-20"><?= t('Date') ?></th>
+ <th class="column-15"><?= t('Date') ?></th>
<th class="column-15"><?= t('Action') ?></th>
</tr>
<?php foreach ($notifications as $notification): ?>
<tr>
<td>
+ <?php if (isset($notification['event_data']['task']['project_name'])): ?>
+ <?= $this->url->link(
+ $this->text->e($notification['event_data']['task']['project_name']),
+ 'BoardViewController',
+ 'show',
+ array('project_id' => $notification['event_data']['task']['project_id'])
+ )
+ ?>
+ <?php elseif (isset($notification['event_data']['project_name'])): ?>
+ <?= $this->text->e($notification['event_data']['project_name']) ?>
+ <?php endif ?>
+ </td>
+ <td>
<?php if ($this->text->contains($notification['event_name'], 'subtask')): ?>
<i class="fa fa-tasks fa-fw"></i>
<?php elseif ($this->text->contains($notification['event_name'], 'task.move')): ?>
diff --git a/app/Template/project_list/show.php b/app/Template/project_list/show.php
index 8e4c3e6a..4f7c2e83 100644
--- a/app/Template/project_list/show.php
+++ b/app/Template/project_list/show.php
@@ -1,12 +1,14 @@
<section id="main">
<div class="page-header">
<ul>
+ <?= $this->hook->render('template:project-list:menu:before', array('project' => $project)) ?>
<?php if ($this->user->hasAccess('ProjectUserOverviewController', 'managers')): ?>
<li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('Users overview'), 'ProjectUserOverviewController', 'managers') ?></li>
<?php endif ?>
<?php if ($this->user->hasAccess('ProjectGanttController', 'show')): ?>
<li><i class="fa fa-sliders fa-fw"></i><?= $this->url->link(t('Projects Gantt chart'), 'ProjectGanttController', 'show') ?></li>
<?php endif ?>
+ <?= $this->hook->render('template:project-list:menu:after', array('project' => $project)) ?>
</ul>
</div>
<?php if ($paginator->isEmpty()): ?>
diff --git a/app/Template/user_view/show.php b/app/Template/user_view/show.php
index fc11f8a1..2f5a73af 100644
--- a/app/Template/user_view/show.php
+++ b/app/Template/user_view/show.php
@@ -18,6 +18,11 @@
<li><?= t('Number of failed login:') ?> <strong><?= $user['nb_failed_login'] ?></strong></li>
<?php if ($user['lock_expiration_date'] != 0): ?>
<li><?= t('Account locked until:') ?> <strong><?= $this->dt->datetime($user['lock_expiration_date']) ?></strong></li>
+ <?php if ($this->user->isAdmin()): ?>
+ <li>
+ <?= $this->url->link(t('Unlock this user'), 'UserCredentialController', 'unlock', array('user_id' => $user['id']), true) ?>
+ </li>
+ <?php endif ?>
<?php endif ?>
</ul>
diff --git a/app/common.php b/app/common.php
index 15fd7a75..e5490c11 100644
--- a/app/common.php
+++ b/app/common.php
@@ -35,6 +35,7 @@ $container->register(new Kanboard\ServiceProvider\MailProvider());
$container->register(new Kanboard\ServiceProvider\HelperProvider());
$container->register(new Kanboard\ServiceProvider\SessionProvider());
$container->register(new Kanboard\ServiceProvider\LoggingProvider());
+$container->register(new Kanboard\ServiceProvider\CacheProvider());
$container->register(new Kanboard\ServiceProvider\DatabaseProvider());
$container->register(new Kanboard\ServiceProvider\AuthenticationProvider());
$container->register(new Kanboard\ServiceProvider\NotificationProvider());
diff --git a/app/constants.php b/app/constants.php
index 40b88fe9..3adb0835 100644
--- a/app/constants.php
+++ b/app/constants.php
@@ -12,6 +12,12 @@ defined('DATA_DIR') or define('DATA_DIR', ROOT_DIR.DIRECTORY_SEPARATOR.'data');
// Files directory (attachments)
defined('FILES_DIR') or define('FILES_DIR', DATA_DIR.DIRECTORY_SEPARATOR.'files');
+// Available cache drivers are "file" and "memory"
+defined('CACHE_DRIVER') or define('CACHE_DRIVER', 'memory');
+
+// Cache folder (file driver)
+defined('CACHE_DIR') or define('CACHE_DIR', DATA_DIR.DIRECTORY_SEPARATOR.'cache');
+
// Plugins settings
defined('PLUGINS_DIR') or define('PLUGINS_DIR', ROOT_DIR.DIRECTORY_SEPARATOR.'plugins');
defined('PLUGIN_API_URL') or define('PLUGIN_API_URL', 'https://kanboard.net/plugins.json');
diff --git a/assets/css/app.min.css b/assets/css/app.min.css
index c3045e6c..a8a103df 100644
--- a/assets/css/app.min.css
+++ b/assets/css/app.min.css
@@ -1 +1 @@
-h1,li,ul,ol,table,tr,td,th,p,blockquote,body{margin:0;padding:0;font-size:100%}body{margin-left:10px;margin-right:10px;padding-bottom:10px;color:#333;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}small{font-size:0.8em}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,0.1);border-bottom:1px solid rgba(255,255,255,0.3)}.pull-right{text-align:right}ul.no-bullet li{list-style-type:none;margin-left:0}.chosen-select{min-height:27px}#app-loading-icon{position:fixed;right:3px;bottom:3px}.assign-me{vertical-align:bottom}a{color:#36c;border:none}a:focus{outline:0;color:#DF5353;text-decoration:none}a:hover{color:#333;text-decoration:none}h1,h2,h3{font-weight:normal;color:#333}h1{font-size:1.5em}h2{font-size:1.4em;margin-bottom:10px}h3{margin-top:10px;font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px}table.table-fixed{table-layout:fixed;white-space:nowrap}table.table-fixed th{overflow:hidden}table.table-fixed td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.table-small{font-size:0.8em}table.table-striped tr:nth-child(odd){background:#fefefe}@media (max-width: 768px){table.table-scrolling{overflow-x:auto;display:inline-block;vertical-align:top;max-width:100%;white-space:nowrap}}table th{text-align:left;padding:0.5em 3px;border:1px solid #eee;background:#fbfbfb}table th a{text-decoration:none;color:#333}table th a:focus,table th a:hover{text-decoration:underline}table td{border:1px solid #eee;padding:0.5em 3px;vertical-align:top}table td li{margin-left:20px}.column-1{width:1%}.column-2{width:2%}.column-3{width:3%}.column-4{width:4%}.column-5{width:5%}.column-6{width:6%}.column-7{width:7%}.column-8{width:8%}.column-9{width:9%}.column-10{width:10%}.column-11{width:11%}.column-12{width:12%}.column-13{width:13%}.column-14{width:14%}.column-15{width:15%}.column-16{width:16%}.column-17{width:17%}.column-18{width:18%}.column-19{width:19%}.column-20{width:20%}.column-21{width:21%}.column-22{width:22%}.column-23{width:23%}.column-24{width:24%}.column-25{width:25%}.column-26{width:26%}.column-27{width:27%}.column-28{width:28%}.column-29{width:29%}.column-30{width:30%}.column-31{width:31%}.column-32{width:32%}.column-33{width:33%}.column-34{width:34%}.column-35{width:35%}.column-36{width:36%}.column-37{width:37%}.column-38{width:38%}.column-39{width:39%}.column-40{width:40%}.column-41{width:41%}.column-42{width:42%}.column-43{width:43%}.column-44{width:44%}.column-45{width:45%}.column-46{width:46%}.column-47{width:47%}.column-48{width:48%}.column-49{width:49%}.column-50{width:50%}.column-51{width:51%}.column-52{width:52%}.column-53{width:53%}.column-54{width:54%}.column-55{width:55%}.column-56{width:56%}.column-57{width:57%}.column-58{width:58%}.column-59{width:59%}.column-60{width:60%}.column-61{width:61%}.column-62{width:62%}.column-63{width:63%}.column-64{width:64%}.column-65{width:65%}.column-66{width:66%}.column-67{width:67%}.column-68{width:68%}.column-69{width:69%}.column-70{width:70%}.column-71{width:71%}.column-72{width:72%}.column-73{width:73%}.column-74{width:74%}.column-75{width:75%}.column-76{width:76%}.column-77{width:77%}.column-78{width:78%}.column-79{width:79%}.column-80{width:80%}.column-81{width:81%}.column-82{width:82%}.column-83{width:83%}.column-84{width:84%}.column-85{width:85%}.column-86{width:86%}.column-87{width:87%}.column-88{width:88%}.column-89{width:89%}.column-90{width:90%}.column-91{width:91%}.column-92{width:92%}.column-93{width:93%}.column-94{width:94%}.column-95{width:95%}.column-96{width:96%}.column-97{width:97%}.column-98{width:98%}.column-99{width:99%}.column-100{width:100%}.draggable-row-handle{cursor:move;color:#dedede}.draggable-row-handle:hover{color:#333}tr.draggable-item-selected{background:#fff;border:2px solid #666;box-shadow:4px 2px 10px -4px rgba(0,0,0,0.55)}tr.draggable-item-selected td{border-top:none;border-bottom:none}tr.draggable-item-selected td:first-child{border-left:none}tr.draggable-item-selected td:last-child{border-right:none}.table-stripped tr.draggable-item-hover,.table-stripped tr.draggable-item-hover{background:#FEFFF2}form{margin-bottom:20px}label{cursor:pointer;display:block;margin-top:10px}input[type="number"],input[type="date"],input[type="email"],input[type="password"],input[type="text"]:not(.input-addon-field){color:#999;border:1px solid #ccc;width:300px;max-width:95%;font-size:1em;height:25px;padding-bottom:0;font-family:sans-serif;margin-top:10px;-webkit-appearance:none;-moz-appearance:none}input[type="number"]::-webkit-input-placeholder,input[type="date"]::-webkit-input-placeholder,input[type="email"]::-webkit-input-placeholder,input[type="password"]::-webkit-input-placeholder,input[type="text"]:not(.input-addon-field)::-webkit-input-placeholder{color:#dedede}input[type="number"]:-moz-placeholder,input[type="date"]:-moz-placeholder,input[type="email"]:-moz-placeholder,input[type="password"]:-moz-placeholder,input[type="text"]:not(.input-addon-field):-moz-placeholder{color:#dedede}input[type="number"]::-moz-placeholder,input[type="date"]::-moz-placeholder,input[type="email"]::-moz-placeholder,input[type="password"]::-moz-placeholder,input[type="text"]:not(.input-addon-field)::-moz-placeholder{color:#dedede}input[type="number"]:-ms-input-placeholder,input[type="date"]:-ms-input-placeholder,input[type="email"]:-ms-input-placeholder,input[type="password"]:-ms-input-placeholder,input[type="text"]:not(.input-addon-field):-ms-input-placeholder{color:#dedede}input[type="number"]:focus,input[type="date"]:focus,input[type="email"]:focus,input[type="password"]:focus,input[type="text"]:focus{color:#000;border-color:rgba(82,168,236,0.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,0.6)}input[type="number"]{width:70px}input[type="text"]:not(.input-addon-field).form-numeric{width:70px}input[type="text"]:not(.input-addon-field).form-datetime,input[type="text"]:not(.input-addon-field).form-date{width:150px}input[type="text"]:not(.input-addon-field).form-input-large{width:400px}input[type="text"]:not(.input-addon-field).form-input-small{width:150px}textarea:focus{color:#000;border-color:rgba(82,168,236,0.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,0.6)}textarea{border:1px solid #ccc;width:400px;max-width:99%;height:200px;font-family:sans-serif}select{max-width:95%}select:focus{outline:0}.tag-autocomplete{width:400px}span.select2-container{margin-top:2px}.form-actions{padding-top:20px;clear:both}.form-required{color:red;padding-left:5px;font-weight:bold}@media (max-width: 480px){.form-required{display:none}}input.form-error,textarea.form-error{border:2px solid #b94a48}input.form-error:focus,textarea.form-error:focus{box-shadow:none;border:2px solid #b94a48}.form-errors{color:#b94a48;list-style-type:none}ul.form-errors li{margin-left:0}.form-help{font-size:0.8em;color:brown;margin-bottom:15px}.form-inline{padding:0;margin:0;border:none}.form-inline label{display:inline}.form-inline input,.form-inline select{margin:0 15px 0 0}.form-inline .form-required{display:none}.form-inline-group{display:inline}.form-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:flex-start;justify-content:flex-start}.form-columns .form-column{margin-right:25px}.form-login{max-width:350px;margin:8% auto 0}.form-login li{margin-left:25px;line-height:25px}.form-login h2{margin-bottom:30px;font-weight:bold}.reset-password{margin-top:20px}.reset-password a{color:#999}.input-addon{display:flex}.input-addon-field{flex:1;font-size:1em;color:#999;margin:0;-webkit-appearance:none;-moz-appearance:none}.input-addon-item{background-color:rgba(147,128,108,0.1);color:#666;font:inherit;font-weight:normal}@media (max-width: 480px){.input-addon-item .dropdown .fa-caret-down{display:none}}.input-addon-field,.input-addon-item{border:1px solid rgba(147,128,108,0.25);padding:4px 0.75em}.input-addon-field:not(:first-child),.input-addon-item:not(:first-child){border-left:0}.input-addon-field:first-child,.input-addon-item:first-child{border-radius:5px 0 0 5px}.input-addon-field:last-child,.input-addon-item:last-child{border-radius:0 5px 5px 0}.alert{padding:8px 35px 8px 14px;margin-top:5px;margin-bottom:5px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-normal{color:#333;background-color:#f0f0f0;border-color:#ddd}.alert ul{margin-top:10px;margin-bottom:10px}.alert li{margin-left:25px}.alert-fade-out{text-align:center;position:fixed;bottom:0;left:20%;width:60%;padding-top:5px;padding-bottom:5px;margin-bottom:0;border-width:1px 0 0;border-radius:4px 4px 0 0;z-index:9999}a.btn{text-decoration:none}.btn{-webkit-appearance:none;-moz-appearance:none;font-size:1.2em;font-weight:normal;cursor:pointer;display:inline-block;border-radius:2px;padding:3px 10px;margin:0;border:1px solid #ddd;background:#f5f5f5;color:#333}.btn:hover,.btn:focus{border-color:#bbb;background:#fafafa;color:#000}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}.btn-red:hover,.btn-red:focus{border-color:#b0281a;background:#c53727;color:#fff}.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}.btn-blue:hover,.btn-blue:focus{border-color:#3079ed;background:#357ae8;color:#fff}.btn:disabled{color:#ccc;border-color:#ccc;background:#f7f7f7}.buttons-header{font-size:0.8em;margin-top:5px;margin-bottom:15px}.tooltip-arrow:after{background:#fff;border:1px solid #aaaaaa;box-shadow:0 0 5px #aaa}div.ui-tooltip{min-width:200px;max-width:600px}.tooltip-arrow{width:20px;height:10px;overflow:hidden;position:absolute}.tooltip-arrow.top{top:-10px}.tooltip-arrow.bottom{bottom:-10px}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.tooltip-arrow.bottom:after{top:-10px}.tooltip-arrow.top:after{bottom:-10px}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:600px}.ui-tooltip-content .markdown p{margin-bottom:0}.ui-tooltip li{list-style-type:none}.tooltip .fa-info-circle{color:#999}.dropdown{display:inline;position:relative}.dropdown ul{display:none}ul.dropdown-submenu-open{display:block;position:absolute;z-index:1000;min-width:285px;list-style:none;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,0.15)}.dropdown-submenu-open li{display:block;margin:0;padding:8px 10px;font-size:0.9em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.dropdown-submenu-open li:last-child{border:none}.dropdown-submenu-open li:not(.no-hover):hover{background:#4078C0;color:#fff}.dropdown-submenu-open li:hover a{color:#fff}.dropdown-submenu-open a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.dropdown-menu-link-text,.dropdown-menu-link-icon{color:#333;text-decoration:none}.dropdown-menu-link-text:hover{text-decoration:underline}.accordion-title{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NEQ5RDgxQzc2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NEQ5RDgxQzg2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0RDlEODFDNTZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0RDlEODFDNjZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvXFWFAAAAAYSURBVHjaYvj//z8D0/Pnz/8zgFgAAQYAS5UJscReGMIAAAAASUVORK5CYII=) repeat-x scroll 0 10px}.accordion-title h3{display:inline;padding-right:5px;background:#fff}.accordion-content{margin-top:15px;margin-bottom:25px}.accordion-toggle{color:#333;text-decoration:none}.accordion-toggle:focus{color:#333}.accordion-toggle:hover{color:#999}.accordion-toggle:before{content:"\f0d7"}.accordion-collapsed{margin-bottom:25px}.accordion-collapsed .accordion-toggle:before{content:"\f0da"}.accordion-collapsed .accordion-content{display:none}#main .confirm{max-width:700px}#popover-container{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);overflow:auto;z-index:100}#popover-content{position:absolute;width:75%;left:12%;top:1%;padding:15px;background:#fff;overflow:auto;max-height:95%}@media (max-width: 480px){#popover-content{left:0;width:100%}}@media (max-width: 768px){#popover-content{left:2.5%;width:85%}}.popover-form{margin-bottom:0}.pagination{text-align:center}.pagination-next{margin-left:5px}.pagination-previous{margin-right:5px}header{box-sizing:border-box;display:flex;flex-wrap:wrap;margin-top:5px;margin-bottom:5px;border-bottom:1px solid #dedede}header>*{box-sizing:border-box}header>*{width:1%}header .menus-container{width:10%}@media (min-width: 768px) and (max-width: 1150px){header .menus-container{width:15%}}@media (max-width: 768px){header .menus-container{width:65%;order:2}}header .board-selector-container{width:15%}@media (min-width: 768px) and (max-width: 1150px){header .board-selector-container{width:20%}}@media (max-width: 768px){header .board-selector-container{width:35%;order:1;margin-bottom:5px}}header .title-container{width:75%}@media (min-width: 768px) and (max-width: 1150px){header .title-container{width:65%}}@media (max-width: 768px){header .title-container{width:100%;order:3}}header h1{font-size:1.5em}header h1 .tooltip{opacity:0.3;font-size:0.7em}.web-notification-icon{color:#36c}.web-notification-icon:focus,.web-notification-icon:hover{color:#000}.logo a{opacity:0.5;color:#d40000;text-decoration:none}.logo span{color:#333}.logo a:hover{opacity:0.8;color:#333}.logo a:focus span,.logo a:hover span{color:#d40000}.page-header{margin-bottom:20px}.page-header .dropdown{padding-right:10px}.page-header h2{margin:0;padding:0;font-weight:bold;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333;text-decoration:none}.page-header h2 a:focus,.page-header h2 a:hover{color:#999}.page-header ul{text-align:left;margin-top:5px;display:inline-block}.page-header li{display:inline;padding-right:15px}@media (max-width: 480px){.page-header li{display:block;line-height:1.5em}}.page-header li.active a{color:#333;text-decoration:none;font-weight:bold}.page-header li.active a:hover,.page-header li.active a:focus{text-decoration:underline}.menu-inline{margin-bottom:5px}.menu-inline li{display:inline;padding-right:15px}.menu-inline li .active a{font-weight:bold;color:#000;text-decoration:none}.sidebar-container{box-sizing:border-box;display:flex;flex-wrap:wrap}.sidebar-container>*{box-sizing:border-box}.sidebar-container>*{width:1%}.sidebar-content{padding-left:10px;width:82%}@media (max-width: 480px){.sidebar-content{width:100%}}.sidebar{max-width:240px;min-width:190px;width:18%}@media (max-width: 480px){.sidebar{width:100%;max-width:99%;min-width:0}}.sidebar h2{margin-top:0}.sidebar>ul a{text-decoration:none;color:#999;font-weight:300}.sidebar>ul a:hover{color:#333}.sidebar>ul li{list-style-type:none;line-height:35px;border-bottom:1px dotted #efefef;padding-left:13px}.sidebar>ul li:hover{border-left:5px solid #555;padding-left:8px}.sidebar>ul li.active{border-left:5px solid #333;padding-left:8px}.sidebar>ul li.active a{color:#333;font-weight:bold}.sidebar-icons>ul li{padding-left:0}.sidebar-icons>ul li:hover,.sidebar-icons>ul li.active{padding-left:0;border-left:none}.sidebar>ul li.active a:focus,.sidebar>ul li.active a:hover{color:#555}.sidebar>ul li:last-child{margin-bottom:15px}.avatar img{vertical-align:bottom}.avatar-left{float:left;margin-right:10px}.avatar-inline{display:inline-block;margin-right:3px}.avatar-48 img,.avatar-48 div{border-radius:30px}.avatar-48 .avatar-letter{line-height:48px;width:48px;font-size:25px}.avatar-20 img,.avatar-20 div{border-radius:10px}.avatar-20 .avatar-letter{line-height:20px;width:20px;font-size:11px}.avatar-letter{color:#fff;text-align:center}#file-dropzone,#screenshot-zone{position:relative;border:2px dashed #ccc;width:99%;height:250px;overflow:auto}#file-dropzone-inner,#screenshot-inner{position:absolute;left:0;bottom:48%;width:100%;text-align:center;color:#aaa}#screenshot-zone.screenshot-pasted{border:2px solid #333}#file-list{margin:20px}#file-list li{list-style-type:none;padding-top:8px;padding-bottom:8px;border-bottom:1px dotted #ddd;width:95%}#file-list li.file-error{font-weight:bold;color:#b94a48}.file-thumbnails{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:flex-start;justify-content:flex-start}.file-thumbnail{width:250px;border:1px solid #efefef;border-radius:5px;margin-bottom:20px;box-shadow:4px 2px 10px -6px rgba(0,0,0,0.55);margin-right:15px}.file-thumbnail img{border-top-left-radius:5px;border-top-right-radius:5px}.file-thumbnail img:hover{opacity:0.5}.file-thumbnail-content{padding-left:8px;padding-right:8px}.file-thumbnail-title{font-weight:700;font-size:0.9em;color:#555}.file-thumbnail-description{font-size:0.8em;color:#999;margin-top:8px;margin-bottom:5px}.file-viewer{position:relative}.file-viewer img{max-width:95%;max-height:85%;margin-top:10px}.color-picker{width:180px}.color-picker-option{height:25px}.color-picker-square{display:inline-block;width:18px;height:18px;margin-right:5px;border:1px solid #000}.color-picker-label{display:inline-block;vertical-align:bottom;padding-bottom:3px}.filter-box{max-width:800px}.project-header{margin-top:8px;margin-bottom:20px}.action-menu{color:#333;text-decoration:none}.action-menu:hover,.action-menu:focus{text-decoration:underline}.project-creation-options{max-width:500px;border-left:3px dotted #efefef;margin-top:20px;padding-left:15px;padding-bottom:5px;padding-top:5px}.project-overview-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center;margin-bottom:20px;font-size:1.4em}@media (max-width: 480px){.project-overview-columns{display:block}}.project-overview-column{text-align:center;margin-right:3%;margin-top:5px;padding:3px 15px 3px 15px;border:1px dashed #ddd}@media (max-width: 480px){.project-overview-column{text-align:left}}.project-overview-column small{color:#999}.project-overview-column strong{color:#555;display:block}@media (max-width: 480px){.project-overview-column strong{display:inline}}.project-header{box-sizing:border-box;display:flex;flex-wrap:wrap}.project-header>*{box-sizing:border-box}.project-header>*{width:1%}.project-header .dropdown-component{width:5%}@media (min-width: 768px) and (max-width: 1150px){.project-header .dropdown-component{width:8%}}@media (max-width: 768px){.project-header .dropdown-component{width:100%}}.project-header .views-switcher-component{width:38%}@media (max-width: 1300px){.project-header .views-switcher-component{width:45%}}@media (min-width: 768px) and (max-width: 1150px){.project-header .views-switcher-component{width:92%}}@media (max-width: 768px){.project-header .views-switcher-component{width:100%}}.project-header .filter-box-component{margin:0;width:55%}@media (max-width: 1300px){.project-header .filter-box-component{width:50%}}@media (min-width: 768px) and (max-width: 1150px){.project-header .filter-box-component{width:100%;margin-top:10px}.project-header .filter-box-component .filter-box{max-width:100%}}@media (max-width: 768px){.project-header .filter-box-component{width:100%;margin-top:10px}.project-header .filter-box-component .filter-box{max-width:100%}}.project-header .filter-box-component form{margin:0}.views{display:inline-block;margin-right:10px;font-size:0.9em}@media (max-width: 560px){.views{width:100%}}@media (max-width: 768px){.views{margin-top:10px;font-size:1em}}.views li{white-space:nowrap;background:#fafafa;border:1px solid #ddd;border-right:none;padding:4px 8px;display:inline}@media (max-width: 560px){.views li{display:block;margin-top:5px;border-radius:5px;border:1px solid #ddd}}.views li.active a{font-weight:bold;color:#000;text-decoration:none}.views li:first-child{border-top-left-radius:5px;border-bottom-left-radius:5px}.views li:last-child{border-right:1px solid #ddd;border-top-right-radius:5px;border-bottom-right-radius:5px}.views a{color:#555;text-decoration:none}.views a:hover{color:#333;text-decoration:underline}.dashboard-project-stats small{margin-right:10px;color:#999}.dashboard-table-link{font-weight:bold;color:#000;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.public-board{margin-top:5px}.public-task{max-width:800px;margin:5px auto 0}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}#board td{vertical-align:top}.board-container-compact{overflow-x:initial}@media all and (-ms-high-contrast: active), (-ms-high-contrast: none){.board-container-compact #board{table-layout:auto}}#board th.board-column-header.board-column-compact{width:initial}.board-column-collapsed{display:none}td.board-column-task-collapsed{font-weight:bold;background-color:#fbfbfb}#board th.board-column-header-collapsed{width:28px;min-width:28px;text-align:center;overflow:hidden}.board-rotation-wrapper{position:relative;padding:8px 4px;min-height:150px;overflow:hidden}.board-rotation{white-space:nowrap;-webkit-backface-visibility:hidden;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.board-column-title .dropdown-menu{text-decoration:none}.board-add-icon{float:left;padding:0 5px}.board-add-icon a{text-decoration:none;color:#36c;font-size:1.6em;line-height:70%}.board-add-icon a:focus,.board-add-icon a:hover{text-decoration:none;color:red}.board-column-header-task-count{color:#999;font-weight:normal}a.board-swimlane-toggle{text-decoration:none}a.board-swimlane-toggle:hover,a.board-swimlane-toggle:focus{color:#000;text-decoration:none;border:none}.board-task-list{min-height:60px}.board-task-list-limit{background-color:#DF5353}.draggable-item{cursor:pointer;user-select:none;-webkit-user-select:none;-moz-user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-board{position:relative;margin-bottom:4px;border:1px solid #000;padding:2px;word-wrap:break-word;font-size:0.9em}div.task-board-recent{border-width:2px}div.task-board-status-closed{user-select:none;border:1px dotted #555}.task-board a{color:#000;text-decoration:none}.task-board .dropdown-menu{font-weight:bold}.task-board .task-score{font-weight:bold}.task-board-collapsed{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.task-board-title{margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.task-board-saving-state{opacity:0.3}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-category-container{text-align:right;margin-top:8px;margin-bottom:8px}.task-board-category{font-weight:500;color:#000;border:1px solid #555;padding:1px 2px 1px 2px;border-radius:4px}.task-board-category:hover{opacity:0.6}.task-board-avatars{text-align:right;float:right}.task-board-change-assignee{cursor:pointer}.task-board-change-assignee:hover{opacity:0.6}.task-board-icons{font-size:0.8em;text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons a{opacity:0.5}.task-board-icons span{opacity:0.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1.0}.flag-milestone{color:green}.task-board-age{display:inline-block}span.task-board-age-total{border:#666 1px solid;padding:1px 3px 1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:#666 1px solid;border-left:none;margin-left:-5px;padding:1px 3px 1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}.task-board-date{font-weight:bold;color:#000}span.task-board-date-today{opacity:1.0}span.task-board-date-overdue{opacity:1.0}.task-tags li{display:inline;margin:0 4px 0 0;padding:2px;color:#555;border:1px solid #666;border-radius:2px}.task-summary-container .task-tags{margin-top:10px}#task-summary{margin-bottom:15px}#task-summary h2{color:#555;font-size:1.6em;margin-top:0;padding-top:0}.task-summary-container{border:2px solid #000;border-radius:8px;padding:15px}.task-summary-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}@media (max-width: 480px){.task-summary-columns{display:block}}.task-summary-column{color:#333}.task-summary-column span{color:#555}.task-summary-column li{line-height:23px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:bold}.task-table .dropdown-menu:focus,.task-table .dropdown-menu:hover{text-decoration:underline}td.task-table a{color:#000;text-decoration:none}td.task-table a:hover{text-decoration:underline}.comment-sorting{text-align:right}.comment-sorting a{color:#555;font-weight:normal;text-decoration:none}.comment-sorting a:hover{color:#999}.comment{padding:5px;margin-bottom:15px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee;margin-left:55px;margin-bottom:10px}.comment-date{color:#999;font-weight:200}.comment-actions{font-size:0.8em;margin-left:55px;margin-top:8px}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.comment-content{margin-left:55px}.subtasks-table td{vertical-align:middle}.task-links-table td{vertical-align:middle}.task-links-task-count{color:#999}.task-link-closed{text-decoration:line-through}.markdown-editor-container{max-width:400px}div.CodeMirror,div.CodeMirror-scroll{max-height:250px;min-height:200px}.markdown-editor-small div.CodeMirror,.markdown-editor-small div.CodeMirror-scroll{min-height:100px;max-height:180px}.form-column div.CodeMirror{margin-bottom:10px}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;margin-bottom:10px;font-weight:bold}.markdown h2{font-weight:bold}.markdown p{margin-bottom:10px}.markdown ol,.markdown ul{margin-left:25px;margin-top:10px;margin-bottom:10px}.markdown pre{background:#fbfbfb;padding:10px;border-radius:5px;border:1px solid #ddd;overflow:auto;color:#555}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left:10px;margin-bottom:10px;margin-left:20px}.markdown img{display:block;max-width:80%;margin-top:10px}.documentation{margin:0 auto;padding:20px;max-width:850px;background:#fefefe;border:1px solid #ccc;border-radius:5px;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;margin-bottom:30px}.documentation h2{text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.listing{border-radius:4px;padding:8px 35px 8px 14px;margin-bottom:20px;border:1px solid #ddd;color:#333;background-color:#fcfcfc;overflow:auto}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.listing ul{margin-top:15px;margin-bottom:15px}.activity-event{margin-bottom:15px;padding:10px}.activity-event:hover{background:#fafafa}.activity-date{margin-left:10px;font-weight:normal;color:#999}.activity-content{margin-left:55px}.activity-title{font-weight:bold;color:#000;border-bottom:1px dotted #efefef}.activity-description{color:#555;margin-top:10px}@media (max-width: 480px){.activity-description{overflow:auto}}.activity-description li{list-style-type:circle}.activity-description ul{margin-top:10px;margin-left:20px}div.ganttview-hzheader-month,div.ganttview-hzheader-day,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series,div.ganttview-grid,div.ganttview-grid-row-cell{float:left}div.ganttview-hzheader-month,div.ganttview-hzheader-day{text-align:center}div.ganttview-grid-row-cell.last,div.ganttview-hzheader-day.last,div.ganttview-hzheader-month.last{border-right:none}div.ganttview{border:1px solid #999}div.ganttview-hzheader-month{width:60px;height:20px;border-right:1px solid #d0d0d0;line-height:20px;overflow:hidden}div.ganttview-hzheader-day{width:20px;height:20px;border-right:1px solid #f0f0f0;border-top:1px solid #d0d0d0;line-height:20px;color:#555}div.ganttview-vtheader{margin-top:41px;width:400px;overflow:hidden;background-color:#fff}div.ganttview-vtheader-item{color:#555}div.ganttview-vtheader-series-name{width:400px;height:31px;line-height:31px;padding-left:3px;border-top:1px solid #d0d0d0;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}div.ganttview-vtheader-series-name a{color:#555;text-decoration:none}div.ganttview-vtheader-series-name a:hover{color:#333;text-decoration:underline}div.ganttview-vtheader-series-name a i{color:#000}div.ganttview-vtheader-series-name a:hover i{color:#555}div.ganttview-slide-container{overflow:auto;border-left:1px solid #999}div.ganttview-grid-row-cell{width:20px;height:31px;border-right:1px solid #f0f0f0;border-top:1px solid #f0f0f0}div.ganttview-grid-row-cell.ganttview-weekend{background-color:#fafafa}div.ganttview-blocks{margin-top:40px}div.ganttview-block-container{height:28px;padding-top:4px}div.ganttview-block{position:relative;height:25px;background-color:#E5ECF9;border:1px solid #c0c0c0;border-radius:3px}.ganttview-block-movable{cursor:move}div.ganttview-block-not-defined{border-color:#000;background-color:#000}div.ganttview-block-text{position:absolute;height:12px;font-size:0.7em;color:#999;padding:2px 3px}div.ganttview-block div.ui-resizable-handle.ui-resizable-s{bottom:-0}.user-mention-link{font-weight:bold;color:#000;text-decoration:none}.user-mention-link:hover{color:#555}
+h1,li,ul,ol,table,tr,td,th,p,blockquote,body{margin:0;padding:0;font-size:100%}body{margin-left:10px;margin-right:10px;padding-bottom:10px;color:#333;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}small{font-size:0.8em}hr{border:0;height:0;border-top:1px solid rgba(0,0,0,0.1);border-bottom:1px solid rgba(255,255,255,0.3)}.pull-right{text-align:right}ul.no-bullet li{list-style-type:none;margin-left:0}.chosen-select{min-height:27px}#app-loading-icon{position:fixed;right:3px;bottom:3px}.assign-me{vertical-align:bottom}a{color:#36c;border:none}a:focus{outline:0;color:#DF5353;text-decoration:none}a:hover{color:#333;text-decoration:none}h1,h2,h3{font-weight:normal;color:#333}h1{font-size:1.5em}h2{font-size:1.4em;margin-bottom:10px}h3{margin-top:10px;font-size:1.2em}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px}table.table-fixed{table-layout:fixed;white-space:nowrap}table.table-fixed th{overflow:hidden}table.table-fixed td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.table-small{font-size:0.8em}table.table-striped tr:nth-child(odd){background:#fefefe}@media (max-width: 768px){table.table-scrolling{overflow-x:auto;display:inline-block;vertical-align:top;max-width:100%;white-space:nowrap}}table th{text-align:left;padding:0.5em 3px;border:1px solid #eee;background:#fbfbfb}table th a{text-decoration:none;color:#333}table th a:focus,table th a:hover{text-decoration:underline}table td{border:1px solid #eee;padding:0.5em 3px;vertical-align:top}table td li{margin-left:20px}.column-1{width:1%}.column-2{width:2%}.column-3{width:3%}.column-4{width:4%}.column-5{width:5%}.column-6{width:6%}.column-7{width:7%}.column-8{width:8%}.column-9{width:9%}.column-10{width:10%}.column-11{width:11%}.column-12{width:12%}.column-13{width:13%}.column-14{width:14%}.column-15{width:15%}.column-16{width:16%}.column-17{width:17%}.column-18{width:18%}.column-19{width:19%}.column-20{width:20%}.column-21{width:21%}.column-22{width:22%}.column-23{width:23%}.column-24{width:24%}.column-25{width:25%}.column-26{width:26%}.column-27{width:27%}.column-28{width:28%}.column-29{width:29%}.column-30{width:30%}.column-31{width:31%}.column-32{width:32%}.column-33{width:33%}.column-34{width:34%}.column-35{width:35%}.column-36{width:36%}.column-37{width:37%}.column-38{width:38%}.column-39{width:39%}.column-40{width:40%}.column-41{width:41%}.column-42{width:42%}.column-43{width:43%}.column-44{width:44%}.column-45{width:45%}.column-46{width:46%}.column-47{width:47%}.column-48{width:48%}.column-49{width:49%}.column-50{width:50%}.column-51{width:51%}.column-52{width:52%}.column-53{width:53%}.column-54{width:54%}.column-55{width:55%}.column-56{width:56%}.column-57{width:57%}.column-58{width:58%}.column-59{width:59%}.column-60{width:60%}.column-61{width:61%}.column-62{width:62%}.column-63{width:63%}.column-64{width:64%}.column-65{width:65%}.column-66{width:66%}.column-67{width:67%}.column-68{width:68%}.column-69{width:69%}.column-70{width:70%}.column-71{width:71%}.column-72{width:72%}.column-73{width:73%}.column-74{width:74%}.column-75{width:75%}.column-76{width:76%}.column-77{width:77%}.column-78{width:78%}.column-79{width:79%}.column-80{width:80%}.column-81{width:81%}.column-82{width:82%}.column-83{width:83%}.column-84{width:84%}.column-85{width:85%}.column-86{width:86%}.column-87{width:87%}.column-88{width:88%}.column-89{width:89%}.column-90{width:90%}.column-91{width:91%}.column-92{width:92%}.column-93{width:93%}.column-94{width:94%}.column-95{width:95%}.column-96{width:96%}.column-97{width:97%}.column-98{width:98%}.column-99{width:99%}.column-100{width:100%}.draggable-row-handle{cursor:move;color:#dedede}.draggable-row-handle:hover{color:#333}tr.draggable-item-selected{background:#fff;border:2px solid #666;box-shadow:4px 2px 10px -4px rgba(0,0,0,0.55)}tr.draggable-item-selected td{border-top:none;border-bottom:none}tr.draggable-item-selected td:first-child{border-left:none}tr.draggable-item-selected td:last-child{border-right:none}.table-stripped tr.draggable-item-hover,.table-stripped tr.draggable-item-hover{background:#FEFFF2}form{margin-bottom:20px}label{cursor:pointer;display:block;margin-top:10px}input[type="number"],input[type="date"],input[type="email"],input[type="password"],input[type="text"]:not(.input-addon-field){color:#999;border:1px solid #ccc;width:300px;max-width:95%;font-size:1em;height:25px;padding-bottom:0;font-family:sans-serif;margin-top:10px;-webkit-appearance:none;-moz-appearance:none}input[type="number"]::-webkit-input-placeholder,input[type="date"]::-webkit-input-placeholder,input[type="email"]::-webkit-input-placeholder,input[type="password"]::-webkit-input-placeholder,input[type="text"]:not(.input-addon-field)::-webkit-input-placeholder{color:#dedede}input[type="number"]:-moz-placeholder,input[type="date"]:-moz-placeholder,input[type="email"]:-moz-placeholder,input[type="password"]:-moz-placeholder,input[type="text"]:not(.input-addon-field):-moz-placeholder{color:#dedede}input[type="number"]::-moz-placeholder,input[type="date"]::-moz-placeholder,input[type="email"]::-moz-placeholder,input[type="password"]::-moz-placeholder,input[type="text"]:not(.input-addon-field)::-moz-placeholder{color:#dedede}input[type="number"]:-ms-input-placeholder,input[type="date"]:-ms-input-placeholder,input[type="email"]:-ms-input-placeholder,input[type="password"]:-ms-input-placeholder,input[type="text"]:not(.input-addon-field):-ms-input-placeholder{color:#dedede}input[type="number"]:focus,input[type="date"]:focus,input[type="email"]:focus,input[type="password"]:focus,input[type="text"]:focus{color:#000;border-color:rgba(82,168,236,0.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,0.6)}input[type="number"]{width:70px}input[type="text"]:not(.input-addon-field).form-numeric{width:70px}input[type="text"]:not(.input-addon-field).form-datetime,input[type="text"]:not(.input-addon-field).form-date{width:150px}input[type="text"]:not(.input-addon-field).form-input-large{width:400px}input[type="text"]:not(.input-addon-field).form-input-small{width:150px}textarea:focus{color:#000;border-color:rgba(82,168,236,0.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,0.6)}textarea{border:1px solid #ccc;width:400px;max-width:99%;height:200px;font-family:sans-serif}select{max-width:95%}select:focus{outline:0}.tag-autocomplete{width:400px}span.select2-container{margin-top:2px}.form-actions{padding-top:20px;clear:both}.form-required{color:red;padding-left:5px;font-weight:bold}@media (max-width: 480px){.form-required{display:none}}input.form-error,textarea.form-error{border:2px solid #b94a48}input.form-error:focus,textarea.form-error:focus{box-shadow:none;border:2px solid #b94a48}.form-errors{color:#b94a48;list-style-type:none}ul.form-errors li{margin-left:0}.form-help{font-size:0.8em;color:brown;margin-bottom:15px}.form-inline{padding:0;margin:0;border:none}.form-inline label{display:inline}.form-inline input,.form-inline select{margin:0 15px 0 0}.form-inline .form-required{display:none}.form-inline-group{display:inline}.form-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:flex-start;justify-content:flex-start}.form-columns .form-column{margin-right:25px}.form-login{max-width:350px;margin:8% auto 0}.form-login li{margin-left:25px;line-height:25px}.form-login h2{margin-bottom:30px;font-weight:bold}.reset-password{margin-top:20px}.reset-password a{color:#999}.input-addon{display:flex}.input-addon-field{flex:1;font-size:1em;color:#999;margin:0;-webkit-appearance:none;-moz-appearance:none}.input-addon-item{background-color:rgba(147,128,108,0.1);color:#666;font:inherit;font-weight:normal}@media (max-width: 480px){.input-addon-item .dropdown .fa-caret-down{display:none}}.input-addon-field,.input-addon-item{border:1px solid rgba(147,128,108,0.25);padding:4px 0.75em}.input-addon-field:not(:first-child),.input-addon-item:not(:first-child){border-left:0}.input-addon-field:first-child,.input-addon-item:first-child{border-radius:5px 0 0 5px}.input-addon-field:last-child,.input-addon-item:last-child{border-radius:0 5px 5px 0}.alert{padding:8px 35px 8px 14px;margin-top:5px;margin-bottom:5px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-normal{color:#333;background-color:#f0f0f0;border-color:#ddd}.alert ul{margin-top:10px;margin-bottom:10px}.alert li{margin-left:25px}.alert-fade-out{text-align:center;position:fixed;bottom:0;left:20%;width:60%;padding-top:5px;padding-bottom:5px;margin-bottom:0;border-width:1px 0 0;border-radius:4px 4px 0 0;z-index:9999}a.btn{text-decoration:none}.btn{-webkit-appearance:none;-moz-appearance:none;font-size:1.2em;font-weight:normal;cursor:pointer;display:inline-block;border-radius:2px;padding:3px 10px;margin:0;border:1px solid #ddd;background:#f5f5f5;color:#333}.btn:hover,.btn:focus{border-color:#bbb;background:#fafafa;color:#000}.btn-red{border-color:#b0281a;background:#d14836;color:#fff}.btn-red:hover,.btn-red:focus{border-color:#b0281a;background:#c53727;color:#fff}.btn-blue{border-color:#3079ed;background:#4d90fe;color:#fff}.btn-blue:hover,.btn-blue:focus{border-color:#3079ed;background:#357ae8;color:#fff}.btn:disabled{color:#ccc;border-color:#ccc;background:#f7f7f7}.buttons-header{font-size:0.8em;margin-top:5px;margin-bottom:15px}.tooltip-arrow:after{background:#fff;border:1px solid #aaaaaa;box-shadow:0 0 5px #aaa}div.ui-tooltip{min-width:200px;max-width:600px}.tooltip-arrow{width:20px;height:10px;overflow:hidden;position:absolute}.tooltip-arrow.top{top:-10px}.tooltip-arrow.bottom{bottom:-10px}.tooltip-arrow.align-left{left:10px}.tooltip-arrow.align-right{right:10px}.tooltip-arrow:after{content:"";position:absolute;width:14px;height:14px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.tooltip-arrow.bottom:after{top:-10px}.tooltip-arrow.top:after{bottom:-10px}.tooltip-arrow.align-left:after{left:0}.tooltip-arrow.align-right:after{right:0}.tooltip-large{width:600px}.ui-tooltip-content .markdown p{margin-bottom:0}.ui-tooltip li{list-style-type:none}.tooltip .fa-info-circle{color:#999}.dropdown{display:inline;position:relative}.dropdown ul{display:none}ul.dropdown-submenu-open{display:block;position:absolute;z-index:1000;min-width:285px;list-style:none;margin:3px 0 0 1px;padding:6px 0;background-color:#fff;border:1px solid #b2b2b2;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,0.15)}.dropdown-submenu-open li{display:block;margin:0;padding:8px 10px;font-size:0.9em;border-bottom:1px solid #f8f8f8;cursor:pointer}.dropdown-submenu-open li.no-hover{cursor:default}.dropdown-submenu-open li:last-child{border:none}.dropdown-submenu-open li:not(.no-hover):hover{background:#4078C0;color:#fff}.dropdown-submenu-open li:hover a{color:#fff}.dropdown-submenu-open a{text-decoration:none;color:#333}.dropdown-submenu-open a:focus{text-decoration:underline}.dropdown-menu-link-text,.dropdown-menu-link-icon{color:#333;text-decoration:none}.dropdown-menu-link-text:hover{text-decoration:underline}.accordion-title{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NEQ5RDgxQzc2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NEQ5RDgxQzg2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0RDlEODFDNTZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0RDlEODFDNjZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvXFWFAAAAAYSURBVHjaYvj//z8D0/Pnz/8zgFgAAQYAS5UJscReGMIAAAAASUVORK5CYII=) repeat-x scroll 0 10px}.accordion-title h3{display:inline;padding-right:5px;background:#fff}.accordion-content{margin-top:15px;margin-bottom:25px}.accordion-toggle{color:#333;text-decoration:none}.accordion-toggle:focus{color:#333}.accordion-toggle:hover{color:#999}.accordion-toggle:before{content:"\f0d7"}.accordion-collapsed{margin-bottom:25px}.accordion-collapsed .accordion-toggle:before{content:"\f0da"}.accordion-collapsed .accordion-content{display:none}#main .confirm{max-width:700px}#popover-container{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);overflow:auto;z-index:100}#popover-content{position:absolute;width:75%;left:12%;top:1%;padding:15px;background:#fff;overflow:auto;max-height:95%}@media (max-width: 480px){#popover-content{left:0;width:100%}}@media (max-width: 768px){#popover-content{left:2.5%;width:85%}}.popover-form{margin-bottom:0}.pagination{text-align:center}.pagination-next{margin-left:5px}.pagination-previous{margin-right:5px}header{box-sizing:border-box;display:flex;flex-wrap:wrap;margin-top:5px;margin-bottom:5px;border-bottom:1px solid #dedede}header>*{box-sizing:border-box}header>*{width:1%}header .menus-container{width:10%}@media (min-width: 768px) and (max-width: 1150px){header .menus-container{width:15%}}@media (max-width: 768px){header .menus-container{width:65%;order:2}}header .board-selector-container{width:15%}@media (min-width: 768px) and (max-width: 1150px){header .board-selector-container{width:20%}}@media (max-width: 768px){header .board-selector-container{width:35%;order:1;margin-bottom:5px}}header .title-container{width:75%}@media (min-width: 768px) and (max-width: 1150px){header .title-container{width:65%}}@media (max-width: 768px){header .title-container{width:100%;order:3}}header h1{font-size:1.5em}header h1 .tooltip{opacity:0.3;font-size:0.7em}.web-notification-icon{color:#36c}.web-notification-icon:focus,.web-notification-icon:hover{color:#000}.logo a{opacity:0.5;color:#d40000;text-decoration:none}.logo span{color:#333}.logo a:hover{opacity:0.8;color:#333}.logo a:focus span,.logo a:hover span{color:#d40000}.page-header{margin-bottom:20px}.page-header .dropdown{padding-right:10px}.page-header h2{margin:0;padding:0;font-weight:bold;border-bottom:1px dotted #ccc}.page-header h2 a{color:#333;text-decoration:none}.page-header h2 a:focus,.page-header h2 a:hover{color:#999}.page-header ul{text-align:left;margin-top:5px;display:inline-block}.page-header li{display:inline;padding-right:15px}@media (max-width: 480px){.page-header li{display:block;line-height:1.5em}}.page-header li.active a{color:#333;text-decoration:none;font-weight:bold}.page-header li.active a:hover,.page-header li.active a:focus{text-decoration:underline}.menu-inline{margin-bottom:5px}.menu-inline li{display:inline;padding-right:15px}.menu-inline li .active a{font-weight:bold;color:#000;text-decoration:none}.sidebar-container{box-sizing:border-box;display:flex;flex-wrap:wrap}.sidebar-container>*{box-sizing:border-box}.sidebar-container>*{width:1%}.sidebar-content{padding-left:10px;width:82%}@media (max-width: 480px){.sidebar-content{width:100%}}.sidebar{max-width:240px;min-width:190px;width:18%}@media (max-width: 480px){.sidebar{width:100%;max-width:99%;min-width:0}}.sidebar h2{margin-top:0}.sidebar>ul a{text-decoration:none;color:#999;font-weight:300}.sidebar>ul a:hover{color:#333}.sidebar>ul li{list-style-type:none;line-height:35px;border-bottom:1px dotted #efefef;padding-left:13px}.sidebar>ul li:hover{border-left:5px solid #555;padding-left:8px}.sidebar>ul li.active{border-left:5px solid #333;padding-left:8px}.sidebar>ul li.active a{color:#333;font-weight:bold}.sidebar-icons>ul li{padding-left:0}.sidebar-icons>ul li:hover,.sidebar-icons>ul li.active{padding-left:0;border-left:none}.sidebar>ul li.active a:focus,.sidebar>ul li.active a:hover{color:#555}.sidebar>ul li:last-child{margin-bottom:15px}.avatar img{vertical-align:bottom}.avatar-left{float:left;margin-right:10px}.avatar-inline{display:inline-block;margin-right:3px}.avatar-48 img,.avatar-48 div{border-radius:30px}.avatar-48 .avatar-letter{line-height:48px;width:48px;font-size:25px}.avatar-20 img,.avatar-20 div{border-radius:10px}.avatar-20 .avatar-letter{line-height:20px;width:20px;font-size:11px}.avatar-letter{color:#fff;text-align:center}#file-dropzone,#screenshot-zone{position:relative;border:2px dashed #ccc;width:99%;height:250px;overflow:auto}#file-dropzone-inner,#screenshot-inner{position:absolute;left:0;bottom:48%;width:100%;text-align:center;color:#aaa}#screenshot-zone.screenshot-pasted{border:2px solid #333}#file-list{margin:20px}#file-list li{list-style-type:none;padding-top:8px;padding-bottom:8px;border-bottom:1px dotted #ddd;width:95%}#file-list li.file-error{font-weight:bold;color:#b94a48}.file-thumbnails{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:flex-start;justify-content:flex-start}.file-thumbnail{width:250px;border:1px solid #efefef;border-radius:5px;margin-bottom:20px;box-shadow:4px 2px 10px -6px rgba(0,0,0,0.55);margin-right:15px}.file-thumbnail img{border-top-left-radius:5px;border-top-right-radius:5px}.file-thumbnail img:hover{opacity:0.5}.file-thumbnail-content{padding-left:8px;padding-right:8px}.file-thumbnail-title{font-weight:700;font-size:0.9em;color:#555}.file-thumbnail-description{font-size:0.8em;color:#999;margin-top:8px;margin-bottom:5px}.file-viewer{position:relative}.file-viewer img{max-width:95%;max-height:85%;margin-top:10px}.color-picker{width:180px}.color-picker-option{height:25px}.color-picker-square{display:inline-block;width:18px;height:18px;margin-right:5px;border:1px solid #000}.color-picker-label{display:inline-block;vertical-align:bottom;padding-bottom:3px}.filter-box{max-width:800px}.action-menu{color:#333;text-decoration:none}.action-menu:hover,.action-menu:focus{text-decoration:underline}.project-creation-options{max-width:500px;border-left:3px dotted #efefef;margin-top:20px;padding-left:15px;padding-bottom:5px;padding-top:5px}.project-overview-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center;margin-bottom:20px;font-size:1.4em}@media (max-width: 480px){.project-overview-columns{display:block}}.project-overview-column{text-align:center;margin-right:3%;margin-top:5px;padding:3px 15px 3px 15px;border:1px dashed #ddd}@media (max-width: 480px){.project-overview-column{text-align:left}}.project-overview-column small{color:#999}.project-overview-column strong{color:#555;display:block}@media (max-width: 480px){.project-overview-column strong{display:inline}}.project-header{box-sizing:border-box;display:flex;flex-wrap:wrap;margin-bottom:8px}.project-header>*{box-sizing:border-box}.project-header>*{width:1%}.project-header .dropdown-component{margin-top:4px;width:5%}@media (min-width: 768px) and (max-width: 1150px){.project-header .dropdown-component{width:8%}}@media (max-width: 768px){.project-header .dropdown-component{width:100%}}.project-header .views-switcher-component{margin-top:4px;width:38%}@media (max-width: 1300px){.project-header .views-switcher-component{width:45%}}@media (min-width: 768px) and (max-width: 1150px){.project-header .views-switcher-component{width:92%}}@media (max-width: 768px){.project-header .views-switcher-component{margin-top:0;width:100%}}.project-header .filter-box-component{margin:0;width:55%}@media (max-width: 1300px){.project-header .filter-box-component{width:50%}}@media (min-width: 768px) and (max-width: 1150px){.project-header .filter-box-component{width:100%;margin-top:10px}.project-header .filter-box-component .filter-box{max-width:100%}}@media (max-width: 768px){.project-header .filter-box-component{width:100%;margin-top:10px}.project-header .filter-box-component .filter-box{max-width:100%}}.project-header .filter-box-component form{margin:0}.views{display:inline-block;margin-right:10px;font-size:0.9em}@media (max-width: 560px){.views{width:100%}}@media (max-width: 768px){.views{margin-top:10px;font-size:1em}}@media (max-width: 480px){.views{margin-top:5px}}.views li{white-space:nowrap;background:#fafafa;border:1px solid #ddd;border-right:none;padding:4px 8px;display:inline}@media (max-width: 560px){.views li{display:block;margin-top:5px;border-radius:5px;border:1px solid #ddd}}.views li.active a{font-weight:bold;color:#000;text-decoration:none}.views li:first-child{border-top-left-radius:5px;border-bottom-left-radius:5px}.views li:last-child{border-right:1px solid #ddd;border-top-right-radius:5px;border-bottom-right-radius:5px}.views a{color:#555;text-decoration:none}.views a:hover{color:#333;text-decoration:underline}.dashboard-project-stats small{margin-right:10px;color:#999}.dashboard-table-link{font-weight:bold;color:#000;text-decoration:none}.dashboard-table-link:focus,.dashboard-table-link:hover{color:#999}.public-board{margin-top:5px}.public-task{max-width:800px;margin:5px auto 0}#board-container{overflow-x:auto}#board{table-layout:fixed;margin-bottom:0}#board th.board-column-header{width:240px}#board td{vertical-align:top}.board-container-compact{overflow-x:initial}@media all and (-ms-high-contrast: active), (-ms-high-contrast: none){.board-container-compact #board{table-layout:auto}}#board th.board-column-header.board-column-compact{width:initial}.board-column-collapsed{display:none}td.board-column-task-collapsed{font-weight:bold;background-color:#fbfbfb}#board th.board-column-header-collapsed{width:28px;min-width:28px;text-align:center;overflow:hidden}.board-rotation-wrapper{position:relative;padding:8px 4px;min-height:150px;overflow:hidden}.board-rotation{white-space:nowrap;-webkit-backface-visibility:hidden;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.board-column-title .dropdown-menu{text-decoration:none}.board-add-icon{float:left;padding:0 5px}.board-add-icon a{text-decoration:none;color:#36c;font-size:1.6em;line-height:70%}.board-add-icon a:focus,.board-add-icon a:hover{text-decoration:none;color:red}.board-column-header-task-count{color:#999;font-weight:normal}a.board-swimlane-toggle{text-decoration:none}a.board-swimlane-toggle:hover,a.board-swimlane-toggle:focus{color:#000;text-decoration:none;border:none}.board-task-list{min-height:60px}.board-task-list-limit{background-color:#DF5353}.draggable-item{cursor:pointer;user-select:none;-webkit-user-select:none;-moz-user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-board{position:relative;margin-bottom:4px;border:1px solid #000;padding:2px;word-wrap:break-word;font-size:0.9em}div.task-board-recent{border-width:2px}div.task-board-status-closed{user-select:none;border:1px dotted #555}.task-board a{color:#000;text-decoration:none}.task-board .dropdown-menu{font-weight:bold}.task-board .task-score{font-weight:bold}.task-board-collapsed{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.task-board-title{margin-top:5px;margin-bottom:8px}.task-board-title a:hover{text-decoration:underline}.task-board-saving-state{opacity:0.3}.task-board-saving-icon{position:absolute;margin:auto;width:100%;text-align:center;color:#000}.task-board-category-container{text-align:right;margin-top:8px;margin-bottom:8px}.task-board-category{font-weight:500;color:#000;border:1px solid #555;padding:1px 2px 1px 2px;border-radius:4px}.task-board-category:hover{opacity:0.6}.task-board-avatars{text-align:right;float:right}.task-board-change-assignee{cursor:pointer}.task-board-change-assignee:hover{opacity:0.6}.task-board-icons{font-size:0.8em;text-align:right;margin-top:4px;margin-bottom:2px}.task-board-icons a{opacity:0.5}.task-board-icons span{opacity:0.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1.0}.flag-milestone{color:green}.task-board-age{display:inline-block}span.task-board-age-total{border:#666 1px solid;padding:1px 3px 1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-board-age-column{border:#666 1px solid;border-left:none;margin-left:-5px;padding:1px 3px 1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}.task-board-date{font-weight:bold;color:#000}span.task-board-date-today{opacity:1.0}span.task-board-date-overdue{opacity:1.0}.task-tags li{display:inline-block;margin:3px 3px 0 0;padding:1px 3px 1px 3px;color:#333;border:1px solid #333;border-radius:4px}.task-summary-container .task-tags{margin-top:10px}#task-summary{margin-bottom:15px}#task-summary h2{color:#555;font-size:1.6em;margin-top:0;padding-top:0}.task-summary-container{border:2px solid #000;border-radius:8px;padding:15px}.task-summary-columns{display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}@media (max-width: 480px){.task-summary-columns{display:block}}.task-summary-column{color:#333}.task-summary-column span{color:#555}.task-summary-column li{line-height:23px}.task-table .dropdown-menu{color:#000;text-decoration:none;font-weight:bold}.task-table .dropdown-menu:focus,.task-table .dropdown-menu:hover{text-decoration:underline}td.task-table a{color:#000;text-decoration:none}td.task-table a:hover{text-decoration:underline}.comment-sorting{text-align:right}.comment-sorting a{color:#555;font-weight:normal;text-decoration:none}.comment-sorting a:hover{color:#999}.comment{padding:5px;margin-bottom:15px}.comment:hover{background:#fafafa}.comment-title{border-bottom:1px dotted #eee;margin-left:55px;margin-bottom:10px}.comment-date{color:#999;font-weight:200}.comment-actions{font-size:0.8em;margin-left:55px;margin-top:8px}.comment-actions li{display:inline}.comment-actions a{color:#999;text-decoration:none}.comment-actions a:focus,.comment-actions a:hover{color:#333;text-decoration:underline}.comment-content{margin-left:55px}.subtasks-table td{vertical-align:middle}.task-links-table td{vertical-align:middle}.task-links-task-count{color:#999}.task-link-closed{text-decoration:line-through}.markdown-editor-container{max-width:400px}div.CodeMirror,div.CodeMirror-scroll{max-height:250px;min-height:200px}.markdown-editor-small div.CodeMirror,.markdown-editor-small div.CodeMirror-scroll{min-height:100px;max-height:180px}.form-column div.CodeMirror{margin-bottom:10px}.markdown{line-height:1.4em}.markdown h1{margin-top:5px;margin-bottom:10px;font-weight:bold}.markdown h2{font-weight:bold}.markdown p{margin-bottom:10px}.markdown ol,.markdown ul{margin-left:25px;margin-top:10px;margin-bottom:10px}.markdown pre{background:#fbfbfb;padding:10px;border-radius:5px;border:1px solid #ddd;overflow:auto;color:#555}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left:10px;margin-bottom:10px;margin-left:20px}.markdown img{display:block;max-width:80%;margin-top:10px}.documentation{margin:0 auto;padding:20px;max-width:850px;background:#fefefe;border:1px solid #ccc;border-radius:5px;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;margin-bottom:30px}.documentation h2{text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px}.listing{border-radius:4px;padding:8px 35px 8px 14px;margin-bottom:20px;border:1px solid #ddd;color:#333;background-color:#fcfcfc;overflow:auto}.listing li{list-style-type:square;margin-left:20px;margin-bottom:3px}.listing ul{margin-top:15px;margin-bottom:15px}.activity-event{margin-bottom:15px;padding:10px}.activity-event:hover{background:#fafafa}.activity-date{margin-left:10px;font-weight:normal;color:#999}.activity-content{margin-left:55px}.activity-title{font-weight:bold;color:#000;border-bottom:1px dotted #efefef}.activity-description{color:#555;margin-top:10px}@media (max-width: 480px){.activity-description{overflow:auto}}.activity-description li{list-style-type:circle}.activity-description ul{margin-top:10px;margin-left:20px}div.ganttview-hzheader-month,div.ganttview-hzheader-day,div.ganttview-vtheader,div.ganttview-vtheader-item-name,div.ganttview-vtheader-series,div.ganttview-grid,div.ganttview-grid-row-cell{float:left}div.ganttview-hzheader-month,div.ganttview-hzheader-day{text-align:center}div.ganttview-grid-row-cell.last,div.ganttview-hzheader-day.last,div.ganttview-hzheader-month.last{border-right:none}div.ganttview{border:1px solid #999}div.ganttview-hzheader-month{width:60px;height:20px;border-right:1px solid #d0d0d0;line-height:20px;overflow:hidden}div.ganttview-hzheader-day{width:20px;height:20px;border-right:1px solid #f0f0f0;border-top:1px solid #d0d0d0;line-height:20px;color:#555}div.ganttview-vtheader{margin-top:41px;width:400px;overflow:hidden;background-color:#fff}div.ganttview-vtheader-item{color:#555}div.ganttview-vtheader-series-name{width:400px;height:31px;line-height:31px;padding-left:3px;border-top:1px solid #d0d0d0;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}div.ganttview-vtheader-series-name a{color:#555;text-decoration:none}div.ganttview-vtheader-series-name a:hover{color:#333;text-decoration:underline}div.ganttview-vtheader-series-name a i{color:#000}div.ganttview-vtheader-series-name a:hover i{color:#555}div.ganttview-slide-container{overflow:auto;border-left:1px solid #999}div.ganttview-grid-row-cell{width:20px;height:31px;border-right:1px solid #f0f0f0;border-top:1px solid #f0f0f0}div.ganttview-grid-row-cell.ganttview-weekend{background-color:#fafafa}div.ganttview-blocks{margin-top:40px}div.ganttview-block-container{height:28px;padding-top:4px}div.ganttview-block{position:relative;height:25px;background-color:#E5ECF9;border:1px solid #c0c0c0;border-radius:3px}.ganttview-block-movable{cursor:move}div.ganttview-block-not-defined{border-color:#000;background-color:#000}div.ganttview-block-text{position:absolute;height:12px;font-size:0.7em;color:#999;padding:2px 3px}div.ganttview-block div.ui-resizable-handle.ui-resizable-s{bottom:-0}.user-mention-link{font-weight:bold;color:#000;text-decoration:none}.user-mention-link:hover{color:#555}
diff --git a/assets/sass/_project.sass b/assets/sass/_project.sass
index 0f91d92a..a1eb78bd 100644
--- a/assets/sass/_project.sass
+++ b/assets/sass/_project.sass
@@ -1,9 +1,5 @@
@import variables
-.project-header
- margin-top: 8px
- margin-bottom: 20px
-
.action-menu
color: color('primary')
text-decoration: none
diff --git a/assets/sass/_project_header.sass b/assets/sass/_project_header.sass
index 4e6c496f..0a4e08b8 100644
--- a/assets/sass/_project_header.sass
+++ b/assets/sass/_project_header.sass
@@ -3,8 +3,10 @@
.project-header
@include grid(100)
+ margin-bottom: 8px
.dropdown-component
+ margin-top: 4px
@include grid_width(5/100)
@include md-device
@include grid_width(8/100)
@@ -12,12 +14,14 @@
@include grid_width(1)
.views-switcher-component
+ margin-top: 4px
@include grid_width(38/100)
@include custom-device(1300px)
@include grid_width(45/100)
@include md-device
@include grid_width(92/100)
@include sm-device
+ margin-top: 0
@include grid_width(1)
.filter-box-component
diff --git a/assets/sass/_project_views_switcher.sass b/assets/sass/_project_views_switcher.sass
index 4ead9e2b..591277ef 100644
--- a/assets/sass/_project_views_switcher.sass
+++ b/assets/sass/_project_views_switcher.sass
@@ -12,6 +12,8 @@ $breakdown-switcher: 560px
@include sm-device
margin-top: 10px
font-size: size('normal')
+ @include xs-device
+ margin-top: 5px
li
white-space: nowrap
background: #fafafa
diff --git a/assets/sass/_task_tags.sass b/assets/sass/_task_tags.sass
index 30bafd67..5858039b 100644
--- a/assets/sass/_task_tags.sass
+++ b/assets/sass/_task_tags.sass
@@ -2,12 +2,12 @@
@import mixins
.task-tags li
- display: inline
- margin: 0 4px 0 0
- padding: 2px
- color: color('medium')
- border: 1px solid #666
- border-radius: 2px
+ display: inline-block
+ margin: 3px 3px 0 0
+ padding: 1px 3px 1px 3px
+ color: color('primary')
+ border: 1px solid color('primary')
+ border-radius: 4px
.task-summary-container .task-tags
margin-top: 10px
diff --git a/config.default.php b/config.default.php
index d0e93a8e..b9dc8d77 100644
--- a/config.default.php
+++ b/config.default.php
@@ -4,6 +4,9 @@
/* Rename this file to config.php if you want to change the values */
/*******************************************************************/
+// Data folder (must be writeable by the web server user)
+define('DATA_DIR', 'data');
+
// Enable/Disable debug
define('DEBUG', false);
@@ -14,9 +17,15 @@ define('LOG_DRIVER', '');
define('LOG_FILE', DATA_DIR.DIRECTORY_SEPARATOR.'debug.log');
// Plugins directory
-define('PLUGINS_DIR', ROOT_DIR.DIRECTORY_SEPARATOR.'plugins');
+define('PLUGINS_DIR', 'plugins');
+
+// Available cache drivers are "file" and "memory"
+define('CACHE_DRIVER', 'memory');
+
+// Cache folder to use if cache driver is "file" (must be writeable by the web server user)
+define('CACHE_DIR', DATA_DIR.DIRECTORY_SEPARATOR.'cache');
-// Folder for uploaded files
+// Folder for uploaded files (must be writeable by the web server user)
define('FILES_DIR', DATA_DIR.DIRECTORY_SEPARATOR.'files');
// E-mail address for the "From" header (notifications)
diff --git a/doc/api-authentication.markdown b/doc/api-authentication.markdown
index c44a6ef9..3ba1e8f5 100644
--- a/doc/api-authentication.markdown
+++ b/doc/api-authentication.markdown
@@ -1,4 +1,4 @@
-API de autentificación
+API Authentication
==================
API endpoint
@@ -7,39 +7,33 @@ API endpoint
URL: `https://YOUR_SERVER/jsonrpc.php`
-Metedo por default (HTTP Basico)
+Default method (HTTP Basic)
---------------------------
-### Aplicación de credenciales
+### Application credentials
- Username: `jsonrpc`
-- Password: API token para la configuración de pagina
+- Password: API token on the settings page
-### Credencial de usuario
+### User credentials
-- Usar el usuario real y su password
+- Use the real username and password
-La API usa la [Autentificación Basica del esquema HTTP descrita en el RFC2617](http://www.ietf.org/rfc/rfc2617.txt).
+The API use the [HTTP Basic Authentication Scheme described in the RFC2617](http://www.ietf.org/rfc/rfc2617.txt).
-Modificar el header HTTP
-------------------------
+Custom HTTP header
+------------------
-Se puede usar un hedear HTTP alternativo para la autentificación si tu servidor es muy especifico
-You can use an alternative HTTP header for the authentication if your server have a very specific
-configuración.
-configuration.
+You can use an alternative HTTP header for the authentication if your server have a very specific configuration.
- The header name can be anything you want, by example `X-API-Auth`.
- The header value is the `username:password` encoded in Base64.
-Configuración:
+Configuration:
-1. Definir tu cabecera modificada en tu `config.php`: `define('API_AUTHENTICATION_HEADER', 'X-API-Auth');`
1. Define your custom header in your `config.php`: `define('API_AUTHENTICATION_HEADER', 'X-API-Auth');`
-2. Codificar las credenciales en Base64, ejemplo con PHP
2. Encode the credentials in Base64, example with PHP `base64_encode('jsonrpc:19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929');`
-3. Verificar con curl
3. Test with curl:
```bash
@@ -49,19 +43,13 @@ curl \
http://localhost/kanboard/jsonrpc.php
```
-Error de autentificación
--------------------------
Authentication error
--------------------
-Si las credenciales son , recibiras un `401 Not Authorized` y el correspondiente respuesta del JSON.
If the credentials are wrong, you will receive a `401 Not Authorized` and the corresponding JSON response.
-
-Error de Autorización
-----------------------
+
Authorization error
-------------------
-Si el usuario no , recibira un `403 Forbidden`.
If the connected user is not allowed to access to the resource, you will receive a `403 Forbidden`.
diff --git a/doc/bruteforce-protection.markdown b/doc/bruteforce-protection.markdown
index a7bef45e..2f75b919 100644
--- a/doc/bruteforce-protection.markdown
+++ b/doc/bruteforce-protection.markdown
@@ -12,6 +12,9 @@ However, **after three authentication failure through the user API**, the accoun
Kanboard doesn't block any IP addresses since bots can use several anonymous proxies. However, you can use external tools like [fail2ban](http://www.fail2ban.org) to avoid massive scans.
+Configuration
+-------------
+
Default settings can be changed with these configuration variables:
```php
@@ -24,3 +27,9 @@ define('BRUTEFORCE_LOCKDOWN', 6);
// Lock account duration in minutes
define('BRUTEFORCE_LOCKDOWN_DURATION', 15);
```
+
+Unlocking users
+---------------
+
+If you don't want to wait 15 minutes, you can unlock a user from the user interface.
+As administrator, go to the user profile and click on "Unlock this user".
diff --git a/doc/calendar-configuration.markdown b/doc/calendar-configuration.markdown
index 6e5a099a..e46c66c3 100644
--- a/doc/calendar-configuration.markdown
+++ b/doc/calendar-configuration.markdown
@@ -1,45 +1,42 @@
-Configuración de calendarios
+Calendar settings
=================
-Ir al menu de configuraciones, despues elegir cofiguracion de calendarios que se encuentra al lado izquierdo
+Go to the menu **Settings**, then choose **Calendar settings** on the left.
-![Configuración de calendarios](https://kanboard.net/screenshots/documentation/calendar-settings.png)
+![Calendar settings](https://kanboard.net/screenshots/documentation/calendar-settings.png)
-Existe dos diferentes calendarios en kanboard :
+There are two different calendars in Kanboard:
-- Calendarios de projectos
-- Calendario por usuario (disponible desde el dashboard)
+- Project calendar
+- User calendar (available from the dashboard)
-Calendario por projectos
+Project calendar
----------------
-Este calendario visualiza las tareas que se le asignan fechas de vencimiento y las tareas estan basadas sobre
-la fecha de creación o el inicio de fecha
+This calendar shows tasks with defined due date and tasks based on the creation date or the start date.
-### Visualizar tareas basadas en la fecha de creacion
+### Show tasks based on the creation date
-- El inicio de fecha del evento del calendario es la fecha de creacion de la tarea
-- El finalización de fecha del evento es cuendo se completa una tarea
+- The start date of the calendar event is the creation date of the task.
+- The end date of the event is the date of completion.
-### Visualizar tareas basadas en las fechas de inicio
+### Show tasks based on the start date
-- La fecha de inicio del evento del calendario is la fecha de incio de la tarea
-- Esta fecha puede ser definida manualmente.
-- La fecha de finalización del evento es la fecha de terminación
-- Si no hay una fecha de inicio de la tarea no aparece en el calendario.
+- The start date of the calendar event is the start date of the task.
+- This date can be defined manually.
+- The end date of the event is the date of completion.
+- If there is no start date the task will not appear on the calendar.
-Calendarios por usuarios
+User calendar
-------------
-Este calendario visualiza solo las tareas asignadas para el usuario y opcionalmente la información de las subtareas
+This calendar shows only tasks assigned to the user and optionally sub-tasks information.
-###
-Visualizar subtareas basadas en el tiempo de tracking
+### Show sub-tasks based on the time tracking
-- Despliega la información de las subtareas desde el calendario o en el registro de la tabla de seguimiento de tiempo
-- La intersección con los usuarios timetable es calculad
+- Display sub-tasks in the calendar from the information recorded in the time tracking table.
+- The intersection with the user timetable is also calculated.
-### Las estimaciones muestran las subtareas ( la previsión de los trabajos futuros )
+### Show sub-task estimates (forecast of future work)
--
-Mostrar la estimación de los trabajos futuros de las subtareas en estado de "todo" y con un valor definido " estimación " .
+- Display the estimate of future work for sub-tasks in status "todo" and with a defined "estimate" value.
diff --git a/doc/config.markdown b/doc/config.markdown
index e51fd54a..853fa6f2 100644
--- a/doc/config.markdown
+++ b/doc/config.markdown
@@ -37,6 +37,17 @@ Folder for uploaded files
define('FILES_DIR', 'data/files');
```
+Cache parameters
+----------------
+
+```php
+// Available cache drivers are "file" and "memory"
+define('CACHE_DRIVER', 'memory');
+
+// Cache folder to use if cache driver is "file" (must be writeable by the web server user)
+define('CACHE_DIR', DATA_DIR.DIRECTORY_SEPARATOR.'cache');
+```
+
Enable/disable url rewrite
--------------------------
diff --git a/doc/debian-installation.markdown b/doc/debian-installation.markdown
index befa2649..dab4bac7 100644
--- a/doc/debian-installation.markdown
+++ b/doc/debian-installation.markdown
@@ -1,12 +1,12 @@
-Como instalar Kanboard en debian
+How to install Kanboard on Debian?
==================================
-Nota: Algunas caracteristicas de Kanboard requieren que tu corras [un job en background diariamente](cronjob.markdown).
+Note: Some features of Kanboard require that you run [a daily background job](cronjob.markdown).
Debian 8 (Jessie)
-----------------
-Instalar Apache y PHP :
+Install Apache and PHP:
```bash
apt-get update
@@ -14,7 +14,7 @@ apt-get install -y php5 php5-sqlite php5-gd unzip
service apache2 restart
```
-Instalar Kanboard
+Install Kanboard:
```bash
cd /var/www/html
@@ -27,14 +27,14 @@ rm kanboard-latest.zip
Debian 7 (Wheezy)
-----------------
-Instalar Apache y PHP
+Install Apache and PHP:
```bash
apt-get update
apt-get install -y php5 php5-sqlite php5-gd unzip
```
-Instalar Kanboard
+Install Kanboard:
```bash
cd /var/www
@@ -47,7 +47,7 @@ rm kanboard-latest.zip
Debian 6 (Squeeze)
------------------
-Instalar Apache y PHP
+Install Apache and PHP:
```bash
apt-get update
diff --git a/doc/email-configuration.markdown b/doc/email-configuration.markdown
index 2f044700..61d5980a 100644
--- a/doc/email-configuration.markdown
+++ b/doc/email-configuration.markdown
@@ -1,38 +1,38 @@
-Configuración del Email
+Email configuration
===================
-Configuración de usuarios
+User settings
-------------
-Para recibir notificaciones por email los usuarios de Kanboard deben tener
+To receive email notifications, users of Kanboard must have:
-- Activar las notificaciones de su perfil
-- Tener una dirección valida de email en su perfil
-- Ser miembro del proyecto y que este tenga activo la opción de notificaciones
+- Activated notifications in their profile
+- Have a valid email address in their profile
+- Be a member of the project that will trigger notifications
-Nota: El usuario que genera una sesión y que realiza alguna acción no recibe ninguna notificación, sólo otros miembros del proyecto.
+Note: The logged user who performs the action doesn't receive any notifications, only other project members.
-Comunicación con correos electronicos
+Email transports
----------------
There are several email transports available:
- SMTP
- Sendmail
-- PHP mail funcion nativa
-- Otros métodos que pueden ser proporcionados por externos : Postmark, Sendgrid and Mailgun
+- PHP native mail function
+- Other methods can be provided by external plugins: Postmark, Sendgrid and Mailgun
-Configuración del servidor
+Server settings
---------------
-Por default, Kanboard usa el bundled PHP mail function para el envio de emails.
-Porque usualmente el servidor no requiere una configuración y así tu servidor puede estar listo para enviar emails.
+By default, Kanboard will use the bundled PHP mail function to send emails.
+Usually that requires no configuration if your server can already send emails.
-Sin embargo, es posible usar otros metodos, como el protocolo SMTP y Sendmail
+However, it's possible to use other methods, the SMTP protocol and Sendmail.
-### Configuración SMTP
+### SMTP configuration
-Renombrar el archivo `config.default.php` a `config.php` y modificar estos valores:
+Rename the file `config.default.php` to `config.php` and change these values:
```php
// We choose "smtp" as mail transport
@@ -47,17 +47,17 @@ define('MAIL_SMTP_USERNAME', 'username');
define('MAIL_SMTP_PASSWORD', 'super password');
```
-También es posible utilizar una conexión segura, TLS or SSL:
+It's also possible to use a secure connection, TLS or SSL:
```php
define('MAIL_SMTP_ENCRYPTION', 'ssl'); // Valid values are "null", "ssl" or "tls"
```
-### Configuración Sendmail
+### Sendmail configuration
-Por default el comando para el sendmail esta `/usr/sbin/sendmail -bs` Pero usted puede personalizarlo en su archivo de configuración.
+By default the sendmail command will be `/usr/sbin/sendmail -bs` but you can customize that in your config file.
-Ejemplo:
+Example:
```php
// We choose "sendmail" as mail transport
@@ -67,51 +67,48 @@ define('MAIL_TRANSPORT', 'sendmail');
define('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs');
```
-### PHP funcion nativa de email
+### PHP native mail function
-Esta es la configuración por default
+This is the default configuration:
```php
define('MAIL_TRANSPORT', 'mail');
```
-### La dirección de correo electrónico del remitente
+### The sender email address
-Por default, los correos electrónicos utilizarán la dirección del remitente `notifications@kanboard.local`.
-con este correo no es posible responderle
+By default, emails will use the sender address `notifications@kanboard.local`.
+It's not possible to reply to this address.
-Tu puedes personalizar esta direccion cambiando el valor de la constante `MAIL_FROM` en tu archivo de configuración
+You can customize this address by changing the value of the constant `MAIL_FROM` in your config file.
```php
define('MAIL_FROM', 'kanboard@mydomain.tld');
```
+That can be useful if your SMTP server configuration doesn't accept the default address.
-Esto puede ser útil si su configuracion del servidor SMTP no acepta una dirección por default.
+### How to display a link to the task in notifications?
-### Cómo mostrar un enlace a la tarea en las notificaciones ?
+To do that, you have to specify the URL of your Kanboard installation in your [Application Settings](https://kanboard.net/documentation/application-configuration).
+By default, nothing is defined, so no links will be displayed.
-Para hacer eso, tu tienes que especificar la URL de tu instalación de tu kanboard [Application Settings](https://kanboard.net/documentation/application-configuration).
-
-De manera predeterminada, no se define nada, por lo que no se mostrará los enlaces.
-
-Ejemplos :
+Examples:
- http://demo.kanboard.net/
- http://myserver/kanboard/
- http://kanboard.mydomain.com/
-No se olvide de la barra final `/`.
+Don't forget the ending slash `/`.
-Es necesario definir de forma manual debido a que Kanboard no puede adivinar la dirección URL de una secuencia de comandos de línea de comandos y algunas personas tienen una configuración muy específica.
+You need to define that manually because Kanboard cannot guess the URL from a command line script and some people have a very specific configuration.
-Solución de problemas
+Troubleshooting
---------------
+If no emails are sent and you are sure that everything is configured correctly:
-Si no hay mensajes de correo electrónico se envían y que está seguro de que todo está configurado correctamente entonces:
-
-- Verificar el correo de spam
-- Habilita el modo debug y verifique el archivo `data/debug.log`, Debería ver el error exacto
-- Asegúrese de que el servidor o el proveedor de alojamiento le permite enviar mensajes de correo electrónico
-- Si usa Selinux Permitir a PHP enviar emails
+- Check your spam folder
+- Enable the debug mode and check the debug file `data/debug.log`, you should see the exact error
+- Be sure that your server or your hosting provider allows you to send emails
+- If you use SeLinux, allow PHP to send emails
diff --git a/doc/es_ES/analytics.markdown b/doc/es_ES/analytics.markdown
index 87d0f717..b2a3d7ae 100644
--- a/doc/es_ES/analytics.markdown
+++ b/doc/es_ES/analytics.markdown
@@ -1,24 +1,24 @@
Analisis
=========
-Cada proyecto tiene una sección de análisis . Dependiendo de cómo se está utilizando Kanboard , se puede ver estos informes :
+Cada proyecto tiene una sección de análisis . Dependiendo de cómo se está utilizando Kanboard, se puede ver estos informes :
Repartición de usuarios
-------------------------
+-----------------------
![User repartition](https://kanboard.net/screenshots/documentation/user-repartition.png)
Esta gráfico de sectores muestra el numero de tareas abiertas asignadas por usuario.
Distribución de tareas
-------------------------
+----------------------
![Task distribution](https://kanboard.net/screenshots/documentation/task-distribution.png)
Este gráfico de sectores da una visión general del numero de tareas abiertas por columnas.
Diagrama de flujo acumulado
-----------------------------
+---------------------------
![Cumulative flow diagram](https://kanboard.net/screenshots/documentation/cfd.png)
@@ -56,4 +56,4 @@ Este gráfico muestra el promedio de avances y ciclo de tiempos para las ultimas
Esos indicadores se calculan y registran todos los días durante todo el proyecto.
-Nota: No olvidar ejecutar todos los dias el cronjob para tener estadísticas precisas. \ No newline at end of file
+Nota: No olvidar ejecutar todos los dias el cronjob para tener estadísticas precisas.
diff --git a/doc/es_ES/debian-installation.markdown b/doc/es_ES/debian-installation.markdown
new file mode 100644
index 00000000..5636744b
--- /dev/null
+++ b/doc/es_ES/debian-installation.markdown
@@ -0,0 +1,65 @@
+Como instalar Kanboard en debian
+================================
+
+Nota: Algunas caracteristicas de Kanboard requieren que tu corras [un job en background diariamente](cronjob.markdown).
+
+Debian 8 (Jessie)
+-----------------
+
+Instalar Apache y PHP :
+
+```bash
+apt-get update
+apt-get install -y php5 php5-sqlite php5-gd unzip
+service apache2 restart
+```
+
+Instalar Kanboard
+
+```bash
+cd /var/www/html
+wget https://kanboard.net/kanboard-latest.zip
+unzip kanboard-latest.zip
+chown -R www-data:www-data kanboard/data
+rm kanboard-latest.zip
+```
+
+Debian 7 (Wheezy)
+-----------------
+
+Instalar Apache y PHP
+
+```bash
+apt-get update
+apt-get install -y php5 php5-sqlite php5-gd unzip
+```
+
+Instalar Kanboard
+
+```bash
+cd /var/www
+wget https://kanboard.net/kanboard-latest.zip
+unzip kanboard-latest.zip
+chown -R www-data:www-data kanboard/data
+rm kanboard-latest.zip
+```
+
+Debian 6 (Squeeze)
+------------------
+
+Instalar Apache y PHP
+
+```bash
+apt-get update
+apt-get install -y libapache2-mod-php5 php5-sqlite php5-gd unzip
+```
+
+Instalar Kanboard:
+
+```bash
+cd /var/www
+wget https://kanboard.net/kanboard-latest.zip
+unzip kanboard-latest.zip
+chown -R www-data:www-data kanboard/data
+rm kanboard-latest.zip
+```
diff --git a/doc/faq.markdown b/doc/faq.markdown
index 879b070f..76ee07ab 100644
--- a/doc/faq.markdown
+++ b/doc/faq.markdown
@@ -129,3 +129,9 @@ Are there some tutorials about Kanboard in other languages?
-----------------------------------------------------------
- [German article series about Kanboard](http://demaya.de/wp/2014/07/kanboard-eine-jira-alternative-im-detail-installation/)
+
+
+Tips
+----
+
+- [Easy way to remove name constraint in SQLite database](https://github.com/kanboard/kanboard/issues/1508)
diff --git a/doc/kanban-vs-todo-and-scrum.markdown b/doc/kanban-vs-todo-and-scrum.markdown
index 184a3f6b..4e083ff8 100644
--- a/doc/kanban-vs-todo-and-scrum.markdown
+++ b/doc/kanban-vs-todo-and-scrum.markdown
@@ -1,35 +1,37 @@
Kanban vs Todo lists and Scrum
+==============================
Kanban vs Todo lists
+--------------------
-Todo lists [ lista de tareas ]:
+### Todo lists:
-Fase unica (es solo una lista de tareas)
-Multitarea posible (no eficiente)
+- Single phase (just a list of items)
+- Multitasking possible (not efficient)
-Kanban:
-
-Multi fases,
-Concentración absoluta para evitar multitareas por que se puede establecer un limite por columna para mejorar el progreso
+### Kanban:
+- Multiple phases, each column represent a step
+- Bring focus and avoid multitasking because you can set a work in progress limit per column
Kanban vs Scrum
-
-Scrum:
-
-Los sprints son time-boxed, usualmente 2 o 4 semanas
-No permitir cambios durante la iteración
-La estimación es requerida
-Utiliza la velocidad como métrica predeterminada
-El tablero de Scrum se borra entre cada sprint
-Scrum tiene funciones predefinidas como scrum master , los dueños del producto y el equipo
-Una gran cantidad de reuniones: planeaciones, backlogs grooming, daily stand-up, retrospectiva
-
-Kanban:
-
-Fluido continuo
-Los cambios se pueden crear en cualquier momento
-La estimacion es opcional
-Usa la iniciativa del tiempo de ciclo para apresurar el performance
-el tablero Kanban board es persistente
-Kanban no impone estrictas restricciones y reuniones, el proceso es mas flexible
+---------------
+
+### Scrum:
+
+- Sprints are time-boxed, usually 2 or 4 weeks
+- Do not allow changes during the iteration
+- Estimation is required
+- Uses velocity as default metric
+- Scrum board is cleared between each sprint
+- Scrum has pre-defined roles like scrum master, product owners and the team
+- A lot of meetings: planning, backlogs grooming, daily stand-up, retrospective
+
+### Kanban:
+
+- Continuous flow
+- Changes can be made at anytime
+- Estimation is optional
+- Use lead and cycle time to measure performance
+- Kanban board is persistent
+- Kanban doesn't impose strict constraints or meetings, process is more flexible
diff --git a/doc/plugin-hooks.markdown b/doc/plugin-hooks.markdown
index 444b76db..b5d25a40 100644
--- a/doc/plugin-hooks.markdown
+++ b/doc/plugin-hooks.markdown
@@ -218,6 +218,8 @@ List of template hooks:
| `template:project:integrations` | Integration page in projects settings |
| `template:project:sidebar` | Sidebar in project settings |
| `template:project-user:sidebar` | Sidebar on project user overview page |
+| `template:project-list:menu:before` | Project list: before menu entries |
+| `template:project-list:menu:after` | Project list: after menu entries |
| `template:task:layout:top` | Task layout top (after page header) |
| `template:task:details:top` | Task summary top |
| `template:task:details:bottom` | Task summary bottom |
diff --git a/tests/configtest/DefaultConfigFileTest.php b/tests/configtest/DefaultConfigFileTest.php
new file mode 100644
index 00000000..0840925b
--- /dev/null
+++ b/tests/configtest/DefaultConfigFileTest.php
@@ -0,0 +1,9 @@
+<?php
+
+class DefaultConfigFileTest extends PHPUnit_Framework_TestCase
+{
+ public function testThatFileCanBeImported()
+ {
+ $this->assertNotFalse(include __DIR__.'/../../config.default.php');
+ }
+}
diff --git a/tests/units/Action/TaskAssignColorSwimlaneTest.php b/tests/units/Action/TaskAssignColorSwimlaneTest.php
new file mode 100644
index 00000000..811ffac3
--- /dev/null
+++ b/tests/units/Action/TaskAssignColorSwimlaneTest.php
@@ -0,0 +1,70 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Event\TaskEvent;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Action\TaskAssignColorSwimlane;
+
+class TaskAssignColorSwimlaneTest extends Base
+{
+ public function testChangeSwimlane()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test')));
+
+ $event = new TaskEvent(array(
+ 'task_id' => 1,
+ 'task' => array(
+ 'project_id' => 1,
+ 'swimlane_id' => 2,
+ )
+ ));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertNotEquals('red', $task['color_id']);
+
+ $action = new TaskAssignColorSwimlane($this->container);
+ $action->setProjectId(1);
+ $action->setParam('color_id', 'red');
+ $action->setParam('swimlane_id', 2);
+
+ $this->assertTrue($action->execute($event, TaskModel::EVENT_MOVE_SWIMLANE));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals('red', $task['color_id']);
+ }
+
+ public function testWithWrongSwimlane()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test')));
+
+ $event = new TaskEvent(array(
+ 'task_id' => 1,
+ 'task' => array(
+ 'project_id' => 1,
+ 'swimlane_id' => 3,
+ )
+ ));
+
+ $action = new TaskAssignColorSwimlane($this->container);
+ $action->setProjectId(1);
+ $action->setParam('color_id', 'red');
+ $action->setParam('swimlane_id', 2);
+
+ $this->assertFalse($action->execute($event, TaskModel::EVENT_MOVE_SWIMLANE));
+ }
+}
diff --git a/tests/units/Action/TaskAssignPrioritySwimlaneTest.php b/tests/units/Action/TaskAssignPrioritySwimlaneTest.php
new file mode 100644
index 00000000..39c833bf
--- /dev/null
+++ b/tests/units/Action/TaskAssignPrioritySwimlaneTest.php
@@ -0,0 +1,46 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Event\TaskEvent;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Action\TaskAssignPrioritySwimlane;
+
+class TaskAssignPrioritySwimlaneTest extends Base
+{
+ public function testChangeSwimlane()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'priority' => 1)));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(1, $task['priority']);
+
+ $event = new TaskEvent(array(
+ 'task_id' => 1,
+ 'task' => array(
+ 'project_id' => 1,
+ 'swimlane_id' => 2,
+ )
+ ));
+
+ $action = new TaskAssignPrioritySwimlane($this->container);
+ $action->setProjectId(1);
+ $action->setParam('priority', 2);
+ $action->setParam('swimlane_id', 2);
+
+ $this->assertTrue($action->execute($event, TaskModel::EVENT_MOVE_SWIMLANE));
+
+ $task = $taskFinderModel->getById(1);
+ $this->assertNotEmpty($task);
+ $this->assertEquals(2, $task['priority']);
+ }
+}
diff --git a/tests/units/Base.php b/tests/units/Base.php
index e44223ce..722a1335 100644
--- a/tests/units/Base.php
+++ b/tests/units/Base.php
@@ -38,6 +38,7 @@ abstract class Base extends PHPUnit_Framework_TestCase
}
$this->container = new Pimple\Container;
+ $this->container->register(new Kanboard\ServiceProvider\CacheProvider());
$this->container->register(new Kanboard\ServiceProvider\HelperProvider());
$this->container->register(new Kanboard\ServiceProvider\AuthenticationProvider());
$this->container->register(new Kanboard\ServiceProvider\DatabaseProvider());
diff --git a/tests/units/Core/Cache/FileCacheTest.php b/tests/units/Core/Cache/FileCacheTest.php
new file mode 100644
index 00000000..b6336581
--- /dev/null
+++ b/tests/units/Core/Cache/FileCacheTest.php
@@ -0,0 +1,186 @@
+<?php
+
+namespace Kanboard\Core\Cache;
+
+require_once __DIR__.'/../../Base.php';
+
+function file_put_contents($filename, $data)
+{
+ return FileCacheTest::$functions->file_put_contents($filename, $data);
+}
+
+function file_get_contents($filename)
+{
+ return FileCacheTest::$functions->file_get_contents($filename);
+}
+
+function mkdir($filename, $mode = 0777, $recursif = false)
+{
+ return FileCacheTest::$functions->mkdir($filename, $mode, $recursif);
+}
+
+function is_dir($filename)
+{
+ return FileCacheTest::$functions->is_dir($filename);
+}
+
+function file_exists($filename)
+{
+ return FileCacheTest::$functions->file_exists($filename);
+}
+
+function unlink($filename)
+{
+ return FileCacheTest::$functions->unlink($filename);
+}
+
+class FileCacheTest extends \Base
+{
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ public static $functions;
+
+ public function setUp()
+ {
+ parent::setup();
+
+ self::$functions = $this
+ ->getMockBuilder('stdClass')
+ ->setMethods(array(
+ 'file_put_contents',
+ 'file_get_contents',
+ 'file_exists',
+ 'mkdir',
+ 'is_dir',
+ 'unlink',
+ ))
+ ->getMock();
+ }
+
+ public function tearDown()
+ {
+ parent::tearDown();
+ self::$functions = null;
+ }
+
+ public function testSet()
+ {
+ $key = 'mykey';
+ $data = 'data';
+ $cache = new FileCache();
+
+ self::$functions
+ ->expects($this->at(0))
+ ->method('is_dir')
+ ->with(
+ $this->equalTo(CACHE_DIR)
+ )
+ ->will($this->returnValue(false));
+
+ self::$functions
+ ->expects($this->at(1))
+ ->method('mkdir')
+ ->with(
+ $this->equalTo(CACHE_DIR),
+ 0755
+ )
+ ->will($this->returnValue(true));
+
+ self::$functions
+ ->expects($this->at(2))
+ ->method('file_put_contents')
+ ->with(
+ $this->equalTo(CACHE_DIR.DIRECTORY_SEPARATOR.$key),
+ $this->equalTo(serialize($data))
+ )
+ ->will($this->returnValue(true));
+
+ $cache->set($key, $data);
+ }
+
+ public function testGet()
+ {
+ $key = 'mykey';
+ $data = 'data';
+ $cache = new FileCache();
+
+ self::$functions
+ ->expects($this->at(0))
+ ->method('file_exists')
+ ->with(
+ $this->equalTo(CACHE_DIR.DIRECTORY_SEPARATOR.$key)
+ )
+ ->will($this->returnValue(true));
+
+ self::$functions
+ ->expects($this->at(1))
+ ->method('file_get_contents')
+ ->with(
+ $this->equalTo(CACHE_DIR.DIRECTORY_SEPARATOR.$key)
+ )
+ ->will($this->returnValue(serialize($data)));
+
+ $this->assertSame($data, $cache->get($key));
+ }
+
+ public function testGetWithKeyNotFound()
+ {
+ $key = 'mykey';
+ $cache = new FileCache();
+
+ self::$functions
+ ->expects($this->at(0))
+ ->method('file_exists')
+ ->with(
+ $this->equalTo(CACHE_DIR.DIRECTORY_SEPARATOR.$key)
+ )
+ ->will($this->returnValue(false));
+
+ $this->assertNull($cache->get($key));
+ }
+
+ public function testRemoveWithKeyNotFound()
+ {
+ $key = 'mykey';
+ $cache = new FileCache();
+
+ self::$functions
+ ->expects($this->at(0))
+ ->method('file_exists')
+ ->with(
+ $this->equalTo(CACHE_DIR.DIRECTORY_SEPARATOR.$key)
+ )
+ ->will($this->returnValue(false));
+
+ self::$functions
+ ->expects($this->never())
+ ->method('unlink');
+
+ $cache->remove($key);
+ }
+
+ public function testRemove()
+ {
+ $key = 'mykey';
+ $cache = new FileCache();
+
+ self::$functions
+ ->expects($this->at(0))
+ ->method('file_exists')
+ ->with(
+ $this->equalTo(CACHE_DIR.DIRECTORY_SEPARATOR.$key)
+ )
+ ->will($this->returnValue(true));
+
+ self::$functions
+ ->expects($this->at(1))
+ ->method('unlink')
+ ->with(
+ $this->equalTo(CACHE_DIR.DIRECTORY_SEPARATOR.$key)
+ )
+ ->will($this->returnValue(true));
+
+ $cache->remove($key);
+ }
+}
diff --git a/tests/units/Core/FileStorageTest.php b/tests/units/Core/ObjectStorage/FileStorageTest.php
index a3ad2448..ed77dedd 100644
--- a/tests/units/Core/FileStorageTest.php
+++ b/tests/units/Core/ObjectStorage/FileStorageTest.php
@@ -2,7 +2,7 @@
namespace Kanboard\Core\ObjectStorage;
-require_once __DIR__.'/../Base.php';
+require_once __DIR__.'/../../Base.php';
function file_put_contents($filename, $data)
{
@@ -105,7 +105,7 @@ class FileStorageTest extends \Base
->method('file_put_contents')
->with(
$this->equalTo('somewhere'.DIRECTORY_SEPARATOR.'mykey'),
- $this->equalTo('data')
+ $this->equalTo($data)
)
->will($this->returnValue(true));
diff --git a/tests/units/Core/User/UserSessionTest.php b/tests/units/Core/User/UserSessionTest.php
index 64413f98..2a118079 100644
--- a/tests/units/Core/User/UserSessionTest.php
+++ b/tests/units/Core/User/UserSessionTest.php
@@ -83,27 +83,6 @@ class UserSessionTest extends Base
$this->assertFalse($us->isAdmin());
}
- public function testCommentSorting()
- {
- $us = new UserSession($this->container);
- $this->assertEquals('ASC', $us->getCommentSorting());
-
- $us->setCommentSorting('DESC');
- $this->assertEquals('DESC', $us->getCommentSorting());
- }
-
- public function testBoardCollapseMode()
- {
- $us = new UserSession($this->container);
- $this->assertFalse($us->isBoardCollapsed(2));
-
- $us->setBoardDisplayMode(3, false);
- $this->assertFalse($us->isBoardCollapsed(3));
-
- $us->setBoardDisplayMode(3, true);
- $this->assertTrue($us->isBoardCollapsed(3));
- }
-
public function testFilters()
{
$us = new UserSession($this->container);
diff --git a/tests/units/Decorator/MetadataCacheDecoratorTest.php b/tests/units/Decorator/MetadataCacheDecoratorTest.php
new file mode 100644
index 00000000..3e4dd320
--- /dev/null
+++ b/tests/units/Decorator/MetadataCacheDecoratorTest.php
@@ -0,0 +1,127 @@
+<?php
+
+use Kanboard\Decorator\MetadataCacheDecorator;
+
+require_once __DIR__.'/../Base.php';
+
+class MetadataCacheDecoratorTest extends Base
+{
+ protected $cachePrefix = 'cache_prefix';
+ protected $entityId = 123;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $cacheMock;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $metadataModelMock;
+
+ /**
+ * @var MetadataCacheDecorator
+ */
+ protected $metadataCacheDecorator;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->cacheMock = $this
+ ->getMockBuilder('\Kanboard\Core\Cache\MemoryCache')
+ ->setMethods(array(
+ 'set',
+ 'get',
+ ))
+ ->getMock();
+
+ $this->metadataModelMock = $this
+ ->getMockBuilder('\Kanboard\Model\UserMetadataModel')
+ ->setConstructorArgs(array($this->container))
+ ->setMethods(array(
+ 'getAll',
+ 'save',
+ ))
+ ->getMock()
+ ;
+
+ $this->metadataCacheDecorator = new MetadataCacheDecorator(
+ $this->cacheMock,
+ $this->metadataModelMock,
+ $this->cachePrefix,
+ $this->entityId
+ );
+ }
+
+ public function testSet()
+ {
+ $this->cacheMock
+ ->expects($this->once())
+ ->method('set');
+
+ $this->metadataModelMock
+ ->expects($this->at(0))
+ ->method('save');
+
+ $this->metadataModelMock
+ ->expects($this->at(1))
+ ->method('getAll')
+ ->with($this->entityId)
+ ;
+
+ $this->metadataCacheDecorator->set('key', 'value');
+ }
+
+ public function testGetWithCache()
+ {
+ $this->cacheMock
+ ->expects($this->once())
+ ->method('get')
+ ->with($this->cachePrefix.$this->entityId)
+ ->will($this->returnValue(array('key' => 'foobar')))
+ ;
+
+ $this->assertEquals('foobar', $this->metadataCacheDecorator->get('key', 'default'));
+ }
+
+ public function testGetWithCacheAndDefaultValue()
+ {
+ $this->cacheMock
+ ->expects($this->once())
+ ->method('get')
+ ->with($this->cachePrefix.$this->entityId)
+ ->will($this->returnValue(array('key1' => 'foobar')))
+ ;
+
+ $this->assertEquals('default', $this->metadataCacheDecorator->get('key', 'default'));
+ }
+
+ public function testGetWithoutCache()
+ {
+ $this->cacheMock
+ ->expects($this->at(0))
+ ->method('get')
+ ->with($this->cachePrefix.$this->entityId)
+ ->will($this->returnValue(null))
+ ;
+
+ $this->cacheMock
+ ->expects($this->at(1))
+ ->method('set')
+ ->with(
+ $this->cachePrefix.$this->entityId,
+ array('key' => 'something')
+ )
+ ;
+
+ $this->metadataModelMock
+ ->expects($this->once())
+ ->method('getAll')
+ ->with($this->entityId)
+ ->will($this->returnValue(array('key' => 'something')))
+ ;
+
+ $this->assertEquals('something', $this->metadataCacheDecorator->get('key', 'default'));
+ }
+}