1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/kanboard_ynh.git synced 2024-09-03 19:36:17 +02:00

Update kanboard v1.0.8

This commit is contained in:
mbugeia 2014-10-22 19:59:09 +02:00
parent a4efad8870
commit e6f0a222fd
426 changed files with 37537 additions and 3720 deletions

6
sources/.gitignore vendored
View file

@ -32,6 +32,8 @@
###################### ######################
.buildpath .buildpath
.project .project
/.settings/
.idea
# OS generated files # # OS generated files #
###################### ######################
@ -51,5 +53,5 @@ Thumbs.db
# App specific # # App specific #
################ ################
config.php #config.php
data/files #data/files

View file

@ -7,4 +7,4 @@ php:
- "5.3" - "5.3"
before_script: wget https://phar.phpunit.de/phpunit.phar before_script: wget https://phar.phpunit.de/phpunit.phar
script: php phpunit.phar script: php phpunit.phar -c tests/units.sqlite.xml

View file

@ -20,24 +20,21 @@ Features
-------- --------
- Multiple boards/projects - Multiple boards/projects
- Boards customization, rename or add columns - Boards customization, rename/add/remove columns
- Tasks with different colors, categories, sub-tasks, attachments, Markdown support for the description - Tasks with different colors, categories, sub-tasks, attachments, comments and Markdown support for the description
- Automatic actions - Automatic actions based on events
- Users management with a basic privileges separation (administrator or regular user) - Users management with a basic privileges separation (administrator or regular user)
- External authentication: Google and GitHub accounts as well as LDAP/ActiveDirectory - Email notifications
- External authentication: Google, GitHub, LDAP/ActiveDirectory and Reverse-Proxy
- Webhooks to create tasks from an external software - Webhooks to create tasks from an external software
- A basic command line interface
- Host anywhere (shared hosting, VPS, Raspberry Pi or localhost) - Host anywhere (shared hosting, VPS, Raspberry Pi or localhost)
- No external dependencies - No external dependencies
- **Super easy setup**, copy and paste files and you are done! - **Super easy setup**, copy and paste files and you are done!
- Translations in English, French, Brazilian Portuguese, Spanish, German, Polish, Swedish and Chinese - Translations in English, French, Brazilian Portuguese, Spanish, German, Polish, Swedish, Finnish, Italian, Chinese, Russian...
Roadmap Known bugs and feature requests
------- -------------------------------
Kanboard is under active development, have a look to the roadmap: <http://kanboard.net/#roadmap>
Known bugs
----------
See Issues: <https://github.com/fguillot/kanboard/issues> See Issues: <https://github.com/fguillot/kanboard/issues>
@ -46,56 +43,45 @@ License
GNU Affero General Public License version 3: <http://www.gnu.org/licenses/agpl-3.0.txt> GNU Affero General Public License version 3: <http://www.gnu.org/licenses/agpl-3.0.txt>
Authors
-------
Original author: [Frédéric Guillot](http://fredericguillot.com/)
Contributors:
- Alex Butum: https://github.com/dZkF9RWJT6wN8ux
- Claudio Lobo
- Gavlepeter: https://github.com/gavlepeter
- Jesusaplsoft: https://github.com/jesusaplsoft
- Kiswa: https://github.com/kiswa
- Levlaz: https://github.com/levlaz
- Mathgl67: https://github.com/mathgl67
- Matthieu Keller: https://github.com/maggick
- Maxime: https://github.com/EpocDotFr
- Moraxy: https://github.com/moraxy
- Nala Ginrut: https://github.com/NalaGinrut
- Nekohayo: https://github.com/nekohayo
- Olivier Maridat: https://github.com/oliviermaridat
- Poikilotherm: https://github.com/poikilotherm
- Raphaël Doursenaud: https://github.com/rdoursenaud
- Rzeka: https://github.com/rzeka
- Sebastien pacilly: https://github.com/spacilly
- Toomyem: https://github.com/Toomyem
- Troloo: https://github.com/troloo
- Typz: https://github.com/Typz
There is also many people who have reported bugs or proposed awesome ideas.
Documentation Documentation
------------- -------------
### Using Kanboard ### Using Kanboard
#### Introduction
- [Usage examples](docs/usage-examples.markdown) - [Usage examples](docs/usage-examples.markdown)
- [Manage users](docs/manage-users.markdown)
- [Syntax guide](docs/syntax-guide.markdown) #### Working with projects
- [Creating projects](docs/creating-projects.markdown)
- [Editing projects](docs/editing-projects.markdown)
- [Sharing boards and tasks](docs/sharing-projects.markdown)
- [Automatic actions](docs/automatic-actions.markdown) - [Automatic actions](docs/automatic-actions.markdown)
#### Working with tasks
- [Creating tasks](docs/creating-tasks.markdown)
#### Working with users
- [User management](docs/manage-users.markdown)
#### More
- [Syntax guide](docs/syntax-guide.markdown)
### Technical details ### Technical details
#### Installation #### Installation
- [Installation instructions](docs/installation.markdown) - [Installation instructions](docs/installation.markdown)
- [Upgrade Kanboard to a new version](docs/update.markdown)
- [Installation on Ubuntu](docs/ubuntu-installation.markdown) - [Installation on Ubuntu](docs/ubuntu-installation.markdown)
- [Installation on Debian](docs/debian-installation.markdown) - [Installation on Debian](docs/debian-installation.markdown)
- [Installation on Centos](docs/centos-installation.markdown) - [Installation on Centos](docs/centos-installation.markdown)
- [Upgrade Kanboard to a new version](docs/update.markdown) - [Installation on Windows Server with IIS](docs/windows-iis-installation.markdown)
- [Secure connections (HTTPS)](docs/secure-connections.markdown) - [Example with Nginx + HTTPS + SPDY + PHP-FPM](docs/nginx-ssl-php-fpm.markdown)
#### Database #### Database
@ -108,12 +94,22 @@ Documentation
- [LDAP authentication](docs/ldap-authentication.markdown) - [LDAP authentication](docs/ldap-authentication.markdown)
- [Google authentication](docs/google-authentication.markdown) - [Google authentication](docs/google-authentication.markdown)
- [GitHub authentication](docs/github-authentication.markdown) - [GitHub authentication](docs/github-authentication.markdown)
- [Reverse proxy authentication](docs/reverse-proxy-authentication.markdown)
#### Developers #### Developers and sysadmins
- [Board configuration](docs/board-configuration.markdown)
- [Email configuration](docs/email-configuration.markdown)
- [Command line interface](docs/cli.markdown)
- [Json-RPC API](docs/api-json-rpc.markdown) - [Json-RPC API](docs/api-json-rpc.markdown)
- [How to use Kanboard with Vagrant](docs/vagrant.markdown)
- [Webhooks](docs/webhooks.markdown) - [Webhooks](docs/webhooks.markdown)
- [How to use Kanboard with Vagrant](docs/vagrant.markdown)
### Contributors
- [Translations](docs/translations.markdown)
- [Coding standards](docs/coding-standards.markdown)
- [Running tests](docs/tests.markdown)
The documentation is written in [Markdown](http://en.wikipedia.org/wiki/Markdown). The documentation is written in [Markdown](http://en.wikipedia.org/wiki/Markdown).
If you want to improve the documentation, just send a pull-request. If you want to improve the documentation, just send a pull-request.
@ -122,3 +118,48 @@ FAQ
--- ---
Go to the official website: <http://kanboard.net/faq> Go to the official website: <http://kanboard.net/faq>
Authors
-------
Original author: [Frédéric Guillot](http://fredericguillot.com/)
Contributors:
- Alex Butum
- Ashish Kulkarni: https://github.com/ashkulz
- Claudio Lobo
- Cmer: https://github.com/chncsu
- Floaltvater: https://github.com/floaltvater
- Gavlepeter: https://github.com/gavlepeter
- Janne Mäntyharju: https://github.com/JanneMantyharju
- Jesusaplsoft: https://github.com/jesusaplsoft
- Kiswa: https://github.com/kiswa
- Kralo: https://github.com/kralo
- Levlaz: https://github.com/levlaz
- Lim Yuen Hoe: https://github.com/jasonmoofang
- Mathgl67: https://github.com/mathgl67
- Matthieu Keller: https://github.com/maggick
- Mauro Mariño: https://github.com/moromarino
- Maxime: https://github.com/EpocDotFr
- Moraxy: https://github.com/moraxy
- Nala Ginrut: https://github.com/NalaGinrut
- Nekohayo: https://github.com/nekohayo
- Nramel: https://github.com/nramel
- Null-Kelvin: https://github.com/Null-Kelvin
- Olivier Maridat: https://github.com/oliviermaridat
- Poikilotherm: https://github.com/poikilotherm
- Rafaelrossa: https://github.com/rafaelrossa
- Raphaël Doursenaud: https://github.com/rdoursenaud
- Rzeka: https://github.com/rzeka
- Sebastien pacilly: https://github.com/spacilly
- Sylvain Veyrié: https://github.com/turb
- Toomyem: https://github.com/Toomyem
- Tony G. Bolaño: https://github.com/tonybolanyo
- Torsten: https://github.com/misterfu
- Troloo: https://github.com/troloo
- Typz: https://github.com/Typz
- Vedovator: https://github.com/vedovator
- Ybarc: https://github.com/ybarc
There is also many people who have reported bugs or proposed awesome ideas.

View file

@ -139,4 +139,15 @@ abstract class Base implements Listener
return false; return false;
} }
/**
* Return class information
*
* @access public
* @return string
*/
public function __toString()
{
return get_called_class();
}
} }

View file

@ -75,7 +75,7 @@ class TaskAssignCategoryColor extends Base
$this->task->update(array( $this->task->update(array(
'id' => $data['task_id'], 'id' => $data['task_id'],
'category_id' => $this->getParam('category_id'), 'category_id' => $this->getParam('category_id'),
)); ), false);
return true; return true;
} }

View file

@ -75,7 +75,7 @@ class TaskAssignColorCategory extends Base
$this->task->update(array( $this->task->update(array(
'id' => $data['task_id'], 'id' => $data['task_id'],
'color_id' => $this->getParam('color_id'), 'color_id' => $this->getParam('color_id'),
)); ), false);
return true; return true;
} }

View file

@ -75,7 +75,7 @@ class TaskAssignColorUser extends Base
$this->task->update(array( $this->task->update(array(
'id' => $data['task_id'], 'id' => $data['task_id'],
'color_id' => $this->getParam('color_id'), 'color_id' => $this->getParam('color_id'),
)); ), false);
return true; return true;
} }

View file

@ -85,7 +85,7 @@ class TaskAssignCurrentUser extends Base
$this->task->update(array( $this->task->update(array(
'id' => $data['task_id'], 'id' => $data['task_id'],
'owner_id' => $this->acl->getUserId(), 'owner_id' => $this->acl->getUserId(),
)); ), false);
return true; return true;
} }

View file

@ -75,7 +75,7 @@ class TaskAssignSpecificUser extends Base
$this->task->update(array( $this->task->update(array(
'id' => $data['task_id'], 'id' => $data['task_id'],
'owner_id' => $this->getParam('user_id'), 'owner_id' => $this->getParam('user_id'),
)); ), false);
return true; return true;
} }

View file

@ -73,7 +73,8 @@ class TaskDuplicateAnotherProject extends Base
{ {
if ($data['column_id'] == $this->getParam('column_id') && $data['project_id'] != $this->getParam('project_id')) { if ($data['column_id'] == $this->getParam('column_id') && $data['project_id'] != $this->getParam('project_id')) {
$this->task->duplicateToAnotherProject($data['task_id'], $this->getParam('project_id')); $task = $this->task->getById($data['task_id']);
$this->task->duplicateToAnotherProject($this->getParam('project_id'), $task);
return true; return true;
} }

View file

@ -0,0 +1,84 @@
<?php
namespace Action;
use Model\Task;
/**
* Move a task to another project
*
* @package action
* @author Frederic Guillot
*/
class TaskMoveAnotherProject extends Base
{
/**
* Task model
*
* @accesss private
* @var \Model\Task
*/
private $task;
/**
* Constructor
*
* @access public
* @param integer $project_id Project id
* @param \Model\Task $task Task model instance
*/
public function __construct($project_id, Task $task)
{
parent::__construct($project_id);
$this->task = $task;
}
/**
* Get the required parameter for the action (defined by the user)
*
* @access public
* @return array
*/
public function getActionRequiredParameters()
{
return array(
'column_id' => t('Column'),
'project_id' => t('Project'),
);
}
/**
* Get the required parameter for the event
*
* @access public
* @return string[]
*/
public function getEventRequiredParameters()
{
return array(
'task_id',
'column_id',
'project_id',
);
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function doAction(array $data)
{
if ($data['column_id'] == $this->getParam('column_id') && $data['project_id'] != $this->getParam('project_id')) {
$task = $this->task->getById($data['task_id']);
$this->task->moveToAnotherProject($this->getParam('project_id'), $task);
return true;
}
return false;
}
}

59
sources/app/Auth/Base.php Normal file
View file

@ -0,0 +1,59 @@
<?php
namespace Auth;
use Core\Tool;
use Core\Registry;
/**
* Base auth class
*
* @package auth
* @author Frederic Guillot
*
* @property \Model\Acl $acl
* @property \Model\LastLogin $lastLogin
* @property \Model\User $user
*/
abstract class Base
{
/**
* Database instance
*
* @access protected
* @var \PicoDb\Database
*/
protected $db;
/**
* Registry instance
*
* @access protected
* @var \Core\Registry
*/
protected $registry;
/**
* Constructor
*
* @access public
* @param \Core\Registry $registry Registry instance
*/
public function __construct(Registry $registry)
{
$this->registry = $registry;
$this->db = $this->registry->shared('db');
}
/**
* Load automatically models
*
* @access public
* @param string $name Model name
* @return mixed
*/
public function __get($name)
{
return Tool::loadModel($this->registry, $name);
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Auth;
use Model\User;
/**
* Database authentication
*
* @package auth
* @author Frederic Guillot
*/
class Database extends Base
{
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'Database';
/**
* Authenticate a user
*
* @access public
* @param string $username Username
* @param string $password Password
* @return boolean
*/
public function authenticate($username, $password)
{
$user = $this->db->table(User::TABLE)->eq('username', $username)->eq('is_ldap_user', 0)->findOne();
if ($user && password_verify($password, $user['password'])) {
// Update user session
$this->user->updateSession($user);
// Update login history
$this->lastLogin->create(
self::AUTH_NAME,
$user['id'],
$this->user->getIpAddress(),
$this->user->getUserAgent()
);
return true;
}
return false;
}
}

View file

@ -1,6 +1,6 @@
<?php <?php
namespace Model; namespace Auth;
require __DIR__.'/../../vendor/OAuth/bootstrap.php'; require __DIR__.'/../../vendor/OAuth/bootstrap.php';
@ -11,12 +11,19 @@ use OAuth\ServiceFactory;
use OAuth\Common\Http\Exception\TokenResponseException; use OAuth\Common\Http\Exception\TokenResponseException;
/** /**
* GitHub model * GitHub backend
* *
* @package model * @package auth
*/ */
class GitHub extends Base class GitHub extends Base
{ {
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'Github';
/** /**
* Authenticate a GitHub user * Authenticate a GitHub user
* *
@ -26,22 +33,19 @@ class GitHub extends Base
*/ */
public function authenticate($github_id) public function authenticate($github_id)
{ {
$userModel = new User($this->db, $this->event); $user = $this->user->getByGitHubId($github_id);
$user = $userModel->getByGitHubId($github_id);
if ($user) { if ($user) {
// Create the user session // Create the user session
$userModel->updateSession($user); $this->user->updateSession($user);
// Update login history // Update login history
$lastLogin = new LastLogin($this->db, $this->event); $this->lastLogin->create(
$lastLogin->create( self::AUTH_NAME,
LastLogin::AUTH_GITHUB,
$user['id'], $user['id'],
$userModel->getIpAddress(), $this->user->getIpAddress(),
$userModel->getUserAgent() $this->user->getUserAgent()
); );
return true; return true;
@ -59,9 +63,7 @@ class GitHub extends Base
*/ */
public function unlink($user_id) public function unlink($user_id)
{ {
$userModel = new User($this->db, $this->event); return $this->user->update(array(
return $userModel->update(array(
'id' => $user_id, 'id' => $user_id,
'github_id' => '', 'github_id' => '',
)); ));
@ -78,9 +80,7 @@ class GitHub extends Base
*/ */
public function updateUser($user_id, array $profile) public function updateUser($user_id, array $profile)
{ {
$userModel = new User($this->db, $this->event); return $this->user->update(array(
return $userModel->update(array(
'id' => $user_id, 'id' => $user_id,
'github_id' => $profile['id'], 'github_id' => $profile['id'],
'email' => $profile['email'], 'email' => $profile['email'],
@ -141,16 +141,14 @@ class GitHub extends Base
try { try {
$gitHubService = $this->getService(); $gitHubService = $this->getService();
$gitHubService->requestAccessToken($code); $gitHubService->requestAccessToken($code);
return json_decode($gitHubService->request('user'), true); return json_decode($gitHubService->request('user'), true);
} }
catch (TokenResponseException $e) { catch (TokenResponseException $e) {
return false; return false;
} }
return false;
} }
/** /**
* Revokes this user's GitHub tokens for Kanboard * Revokes this user's GitHub tokens for Kanboard
* *
@ -172,7 +170,5 @@ class GitHub extends Base
catch (TokenResponseException $e) { catch (TokenResponseException $e) {
return false; return false;
} }
return false;
} }
} }

View file

@ -1,6 +1,6 @@
<?php <?php
namespace Model; namespace Auth;
require __DIR__.'/../../vendor/OAuth/bootstrap.php'; require __DIR__.'/../../vendor/OAuth/bootstrap.php';
@ -11,13 +11,20 @@ use OAuth\ServiceFactory;
use OAuth\Common\Http\Exception\TokenResponseException; use OAuth\Common\Http\Exception\TokenResponseException;
/** /**
* Google model * Google backend
* *
* @package model * @package auth
* @author Frederic Guillot * @author Frederic Guillot
*/ */
class Google extends Base class Google extends Base
{ {
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'Google';
/** /**
* Authenticate a Google user * Authenticate a Google user
* *
@ -27,21 +34,19 @@ class Google extends Base
*/ */
public function authenticate($google_id) public function authenticate($google_id)
{ {
$userModel = new User($this->db, $this->event); $user = $this->user->getByGoogleId($google_id);
$user = $userModel->getByGoogleId($google_id);
if ($user) { if ($user) {
// Create the user session // Create the user session
$userModel->updateSession($user); $this->user->updateSession($user);
// Update login history // Update login history
$lastLogin = new LastLogin($this->db, $this->event); $this->lastLogin->create(
$lastLogin->create( self::AUTH_NAME,
LastLogin::AUTH_GOOGLE,
$user['id'], $user['id'],
$userModel->getIpAddress(), $this->user->getIpAddress(),
$userModel->getUserAgent() $this->user->getUserAgent()
); );
return true; return true;
@ -59,9 +64,7 @@ class Google extends Base
*/ */
public function unlink($user_id) public function unlink($user_id)
{ {
$userModel = new User($this->db, $this->event); return $this->user->update(array(
return $userModel->update(array(
'id' => $user_id, 'id' => $user_id,
'google_id' => '', 'google_id' => '',
)); ));
@ -77,9 +80,7 @@ class Google extends Base
*/ */
public function updateUser($user_id, array $profile) public function updateUser($user_id, array $profile)
{ {
$userModel = new User($this->db, $this->event); return $this->user->update(array(
return $userModel->update(array(
'id' => $user_id, 'id' => $user_id,
'google_id' => $profile['id'], 'google_id' => $profile['id'],
'email' => $profile['email'], 'email' => $profile['email'],
@ -146,7 +147,5 @@ class Google extends Base
catch (TokenResponseException $e) { catch (TokenResponseException $e) {
return false; return false;
} }
return false;
} }
} }

208
sources/app/Auth/Ldap.php Normal file
View file

@ -0,0 +1,208 @@
<?php
namespace Auth;
/**
* LDAP model
*
* @package auth
* @author Frederic Guillot
*/
class Ldap extends Base
{
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'LDAP';
/**
* Authenticate the user
*
* @access public
* @param string $username Username
* @param string $password Password
* @return boolean
*/
public function authenticate($username, $password)
{
$result = $this->findUser($username, $password);
if (is_array($result)) {
$user = $this->user->getByUsername($username);
if ($user) {
// There is already a local user with that name
if ($user['is_ldap_user'] == 0) {
return false;
}
}
else {
// We create automatically a new user
if ($this->createUser($username, $result['name'], $result['email'])) {
$user = $this->user->getByUsername($username);
}
else {
return false;
}
}
// We open the session
$this->user->updateSession($user);
// Update login history
$this->lastLogin->create(
self::AUTH_NAME,
$user['id'],
$this->user->getIpAddress(),
$this->user->getUserAgent()
);
return true;
}
return false;
}
/**
* Create a new local user after the LDAP authentication
*
* @access public
* @param string $username Username
* @param string $name Name of the user
* @param string $email Email address
* @return bool
*/
public function createUser($username, $name, $email)
{
$values = array(
'username' => $username,
'name' => $name,
'email' => $email,
'is_admin' => 0,
'is_ldap_user' => 1,
);
return $this->user->create($values);
}
/**
* Find the user from the LDAP server
*
* @access public
* @param string $username Username
* @param string $password Password
* @return boolean|array
*/
public function findUser($username, $password)
{
$ldap = $this->connect();
if ($this->bind($ldap, $username, $password)) {
return $this->search($ldap, $username, $password);
}
return false;
}
/**
* LDAP connection
*
* @access private
* @return resource $ldap LDAP connection
*/
private function connect()
{
if (! function_exists('ldap_connect')) {
die('The PHP LDAP extension is required');
}
// Skip SSL certificate verification
if (! LDAP_SSL_VERIFY) {
putenv('LDAPTLS_REQCERT=never');
}
$ldap = ldap_connect(LDAP_SERVER, LDAP_PORT);
if (! is_resource($ldap)) {
die('Unable to connect to the LDAP server: "'.LDAP_SERVER.'"');
}
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
return $ldap;
}
/**
* LDAP bind
*
* @access private
* @param resource $ldap LDAP connection
* @param string $username Username
* @param string $password Password
* @return boolean
*/
private function bind($ldap, $username, $password)
{
if (LDAP_BIND_TYPE === 'user') {
$ldap_username = sprintf(LDAP_USERNAME, $username);
$ldap_password = $password;
}
else if (LDAP_BIND_TYPE === 'proxy') {
$ldap_username = LDAP_USERNAME;
$ldap_password = LDAP_PASSWORD;
}
else {
$ldap_username = null;
$ldap_password = null;
}
if (! @ldap_bind($ldap, $ldap_username, $ldap_password)) {
return false;
}
return true;
}
/**
* LDAP user lookup
*
* @access private
* @param resource $ldap LDAP connection
* @param string $username Username
* @param string $password Password
* @return boolean|array
*/
private function search($ldap, $username, $password)
{
$sr = @ldap_search($ldap, LDAP_ACCOUNT_BASE, sprintf(LDAP_USER_PATTERN, $username), array(LDAP_ACCOUNT_FULLNAME, LDAP_ACCOUNT_EMAIL));
if ($sr === false) {
return false;
}
$info = ldap_get_entries($ldap, $sr);
// User not found
if (count($info) == 0 || $info['count'] == 0) {
return false;
}
// We got our user
if (@ldap_bind($ldap, $info[0]['dn'], $password)) {
return array(
'username' => $username,
'name' => isset($info[0][LDAP_ACCOUNT_FULLNAME][0]) ? $info[0][LDAP_ACCOUNT_FULLNAME][0] : '',
'email' => isset($info[0][LDAP_ACCOUNT_EMAIL][0]) ? $info[0][LDAP_ACCOUNT_EMAIL][0] : '',
);
}
return false;
}
}

View file

@ -1,17 +1,25 @@
<?php <?php
namespace Model; namespace Auth;
use Core\Security; use Core\Security;
use Core\Tool;
/** /**
* RememberMe model * RememberMe model
* *
* @package model * @package auth
* @author Frederic Guillot * @author Frederic Guillot
*/ */
class RememberMe extends Base class RememberMe extends Base
{ {
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'RememberMe';
/** /**
* SQL table name * SQL table name
* *
@ -92,11 +100,16 @@ class RememberMe extends Base
); );
// Create the session // Create the session
$user = new User($this->db, $this->event); $this->user->updateSession($this->user->getById($record['user_id']));
$acl = new Acl($this->db, $this->event); $this->acl->isRememberMe(true);
$user->updateSession($user->getById($record['user_id'])); // Update last login infos
$acl->isRememberMe(true); $this->lastLogin->create(
self::AUTH_NAME,
$this->acl->getUserId(),
$this->user->getIpAddress(),
$this->user->getUserAgent()
);
return true; return true;
} }
@ -297,7 +310,7 @@ class RememberMe extends Base
$expiration, $expiration,
BASE_URL_DIRECTORY, BASE_URL_DIRECTORY,
null, null,
! empty($_SERVER['HTTPS']), Tool::isHTTPS(),
true true
); );
} }
@ -330,7 +343,7 @@ class RememberMe extends Base
time() - 3600, time() - 3600,
BASE_URL_DIRECTORY, BASE_URL_DIRECTORY,
null, null,
! empty($_SERVER['HTTPS']), Tool::isHTTPS(),
true true
); );
} }

View file

@ -0,0 +1,79 @@
<?php
namespace Auth;
use Core\Security;
/**
* ReverseProxy backend
*
* @package auth
* @author Sylvain Veyrié
*/
class ReverseProxy extends Base
{
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'ReverseProxy';
/**
* Authenticate the user with the HTTP header
*
* @access public
* @return bool
*/
public function authenticate()
{
if (isset($_SERVER[REVERSE_PROXY_USER_HEADER])) {
$login = $_SERVER[REVERSE_PROXY_USER_HEADER];
$user = $this->user->getByUsername($login);
if (! $user) {
$this->createUser($login);
$user = $this->user->getByUsername($login);
}
// Create the user session
$this->user->updateSession($user);
// Update login history
$this->lastLogin->create(
self::AUTH_NAME,
$user['id'],
$this->user->getIpAddress(),
$this->user->getUserAgent()
);
return true;
}
return false;
}
/**
* Create automatically a new local user after the authentication
*
* @access private
* @param string $login Username
* @return bool
*/
private function createUser($login)
{
$email = strpos($login, '@') !== false ? $login : '';
if (REVERSE_PROXY_DEFAULT_DOMAIN !== '' && empty($email)) {
$email = $login.'@'.REVERSE_PROXY_DEFAULT_DOMAIN;
}
return $this->user->create(array(
'email' => $email,
'username' => $login,
'is_admin' => REVERSE_PROXY_DEFAULT_ADMIN === $login,
'is_ldap_user' => 1,
));
}
}

View file

@ -17,15 +17,9 @@ class Action extends Base
*/ */
public function index() public function index()
{ {
$project_id = $this->request->getIntegerParam('project_id'); $project = $this->getProject();
$project = $this->project->getById($project_id);
if (! $project) { $this->response->html($this->projectLayout('action_index', array(
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$this->response->html($this->template->layout('action_index', array(
'values' => array('project_id' => $project['id']), 'values' => array('project_id' => $project['id']),
'project' => $project, 'project' => $project,
'actions' => $this->action->getAllByProject($project['id']), 'actions' => $this->action->getAllByProject($project['id']),
@ -49,18 +43,11 @@ class Action extends Base
*/ */
public function params() public function params()
{ {
$project_id = $this->request->getIntegerParam('project_id'); $project = $this->getProject();
$project = $this->project->getById($project_id);
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$values = $this->request->getValues(); $values = $this->request->getValues();
$action = $this->action->load($values['action_name'], $values['project_id']); $action = $this->action->load($values['action_name'], $values['project_id']);
$this->response->html($this->template->layout('action_params', array( $this->response->html($this->projectLayout('action_params', array(
'values' => $values, 'values' => $values,
'action_params' => $action->getActionRequiredParameters(), 'action_params' => $action->getActionRequiredParameters(),
'columns_list' => $this->board->getColumnsList($project['id']), 'columns_list' => $this->board->getColumnsList($project['id']),
@ -81,14 +68,7 @@ class Action extends Base
*/ */
public function create() public function create()
{ {
$project_id = $this->request->getIntegerParam('project_id'); $project = $this->getProject();
$project = $this->project->getById($project_id);
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$values = $this->request->getValues(); $values = $this->request->getValues();
list($valid,) = $this->action->validateCreation($values); list($valid,) = $this->action->validateCreation($values);
@ -113,10 +93,13 @@ class Action extends Base
*/ */
public function confirm() public function confirm()
{ {
$this->response->html($this->template->layout('action_remove', array( $project = $this->getProject();
$this->response->html($this->projectLayout('action_remove', array(
'action' => $this->action->getById($this->request->getIntegerParam('action_id')), 'action' => $this->action->getById($this->request->getIntegerParam('action_id')),
'available_events' => $this->action->getAvailableEvents(), 'available_events' => $this->action->getAvailableEvents(),
'available_actions' => $this->action->getAvailableActions(), 'available_actions' => $this->action->getAvailableActions(),
'project' => $project,
'menu' => 'projects', 'menu' => 'projects',
'title' => t('Remove an action') 'title' => t('Remove an action')
))); )));

View file

@ -2,6 +2,7 @@
namespace Controller; namespace Controller;
use Core\Tool;
use Core\Registry; use Core\Registry;
use Core\Security; use Core\Security;
use Core\Translator; use Core\Translator;
@ -12,22 +13,25 @@ use Model\LastLogin;
* *
* @package controller * @package controller
* @author Frederic Guillot * @author Frederic Guillot
* @property \Model\Acl $acl *
* @property \Model\Action $action * @property \Model\Acl $acl
* @property \Model\Board $board * @property \Model\Authentication $authentication
* @property \Model\Category $category * @property \Model\Action $action
* @property \Model\Comment $comment * @property \Model\Board $board
* @property \Model\Config $config * @property \Model\Category $category
* @property \Model\File $file * @property \Model\Comment $comment
* @property \Model\Google $google * @property \Model\Config $config
* @property \Model\GitHub $gitHub * @property \Model\File $file
* @property \Model\LastLogin $lastLogin * @property \Model\LastLogin $lastLogin
* @property \Model\Ldap $ldap * @property \Model\Notification $notification
* @property \Model\Project $project * @property \Model\Project $project
* @property \Model\RememberMe $rememberMe * @property \Model\SubTask $subTask
* @property \Model\SubTask $subTask * @property \Model\Task $task
* @property \Model\Task $task * @property \Model\TaskHistory $taskHistory
* @property \Model\User $user * @property \Model\CommentHistory $commentHistory
* @property \Model\SubtaskHistory $subtaskHistory
* @property \Model\User $user
* @property \Model\Webhook $webhook
*/ */
abstract class Base abstract class Base
{ {
@ -91,9 +95,7 @@ abstract class Base
*/ */
public function __get($name) public function __get($name)
{ {
$class = '\Model\\'.ucfirst($name); return Tool::loadModel($this->registry, $name);
$this->registry->$name = new $class($this->registry->shared('db'), $this->registry->shared('event'));
return $this->registry->shared($name);
} }
/** /**
@ -121,26 +123,8 @@ abstract class Base
date_default_timezone_set($this->config->get('timezone', 'UTC')); date_default_timezone_set($this->config->get('timezone', 'UTC'));
// Authentication // Authentication
if (! $this->acl->isLogged() && ! $this->acl->isPublicAction($controller, $action)) { if (! $this->authentication->isAuthenticated($controller, $action)) {
$this->response->redirect('?controller=user&action=login');
// Try the remember me authentication first
if (! $this->rememberMe->authenticate()) {
// Redirect to the login form if not authenticated
$this->response->redirect('?controller=user&action=login');
}
else {
$this->lastLogin->create(
LastLogin::AUTH_REMEMBER_ME,
$this->acl->getUserId(),
$this->user->getIpAddress(),
$this->user->getUserAgent()
);
}
}
else if ($this->rememberMe->hasCookie()) {
$this->rememberMe->refresh();
} }
// Check if the user is allowed to see this page // Check if the user is allowed to see this page
@ -149,28 +133,57 @@ abstract class Base
} }
// Attach events // Attach events
$this->action->attachEvents(); $this->attachEvents();
$this->project->attachEvents(); }
/**
* Attach events
*
* @access private
*/
private function attachEvents()
{
$models = array(
'action',
'project',
'webhook',
'notification',
'taskHistory',
'commentHistory',
'subtaskHistory',
);
foreach ($models as $model) {
$this->$model->attachEvents();
}
} }
/** /**
* Application not found page (404 error) * Application not found page (404 error)
* *
* @access public * @access public
* @param boolean $no_layout Display the layout or not
*/ */
public function notfound() public function notfound($no_layout = false)
{ {
$this->response->html($this->template->layout('app_notfound', array('title' => t('Page not found')))); $this->response->html($this->template->layout('app_notfound', array(
'title' => t('Page not found'),
'no_layout' => $no_layout,
)));
} }
/** /**
* Application forbidden page * Application forbidden page
* *
* @access public * @access public
* @param boolean $no_layout Display the layout or not
*/ */
public function forbidden() public function forbidden($no_layout = false)
{ {
$this->response->html($this->template->layout('app_forbidden', array('title' => t('Access Forbidden')))); $this->response->html($this->template->layout('app_forbidden', array(
'title' => t('Access Forbidden'),
'no_layout' => $no_layout,
)));
} }
/** /**
@ -228,6 +241,22 @@ abstract class Base
return $this->template->layout('task_layout', $params); return $this->template->layout('task_layout', $params);
} }
/**
* Common layout for project views
*
* @access protected
* @param string $template Template name
* @param array $params Template parameters
* @return string
*/
protected function projectLayout($template, array $params)
{
$content = $this->template->load($template, $params);
$params['project_content_for_layout'] = $content;
return $this->template->layout('project_layout', $params);
}
/** /**
* Common method to get a task for task views * Common method to get a task for task views
* *
@ -246,4 +275,26 @@ abstract class Base
return $task; return $task;
} }
/**
* Common method to get a project
*
* @access protected
* @param integer $project_id Default project id
* @return array
*/
protected function getProject($project_id = 0)
{
$project_id = $this->request->getIntegerParam('project_id', $project_id);
$project = $this->project->getById($project_id);
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$this->checkProjectPermissions($project['id']);
return $project;
}
} }

View file

@ -51,39 +51,27 @@ class Board extends Base
* *
* @access public * @access public
*/ */
public function assign() public function changeAssignee()
{ {
$task = $this->task->getById($this->request->getIntegerParam('task_id')); $task = $this->getTask();
$project = $this->project->getById($task['project_id']); $project = $this->project->getById($task['project_id']);
$projects = $this->project->getListByStatus(ProjectModel::ACTIVE); $projects = $this->project->getAvailableList($this->acl->getUserId());
$params = array(
if ($this->acl->isRegularUser()) { 'errors' => array(),
$projects = $this->project->filterListByAccess($projects, $this->acl->getUserId()); 'values' => $task,
} 'users_list' => $this->project->getUsersList($project['id']),
'projects' => $projects,
if (! $project) $this->notfound(); 'current_project_id' => $project['id'],
$this->checkProjectPermissions($project['id']); 'current_project_name' => $project['name'],
);
if ($this->request->isAjax()) { if ($this->request->isAjax()) {
$this->response->html($this->template->load('board_assign', array( $this->response->html($this->template->load('board_assignee', $params));
'errors' => array(),
'values' => $task,
'users_list' => $this->project->getUsersList($project['id']),
'projects' => $projects,
'current_project_id' => $project['id'],
'current_project_name' => $project['name'],
)));
} }
else { else {
$this->response->html($this->template->layout('board_assign', array( $this->response->html($this->template->layout('board_assignee', $params + array(
'errors' => array(),
'values' => $task,
'users_list' => $this->project->getUsersList($project['id']),
'projects' => $projects,
'current_project_id' => $project['id'],
'current_project_name' => $project['name'],
'menu' => 'boards', 'menu' => 'boards',
'title' => t('Change assignee').' - '.$task['title'], 'title' => t('Change assignee').' - '.$task['title'],
))); )));
@ -95,7 +83,7 @@ class Board extends Base
* *
* @access public * @access public
*/ */
public function assignTask() public function updateAssignee()
{ {
$values = $this->request->getValues(); $values = $this->request->getValues();
$this->checkProjectPermissions($values['project_id']); $this->checkProjectPermissions($values['project_id']);
@ -112,6 +100,60 @@ class Board extends Base
$this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']); $this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']);
} }
/**
* Change a task category directly from the board
*
* @access public
*/
public function changeCategory()
{
$task = $this->getTask();
$project = $this->project->getById($task['project_id']);
$projects = $this->project->getAvailableList($this->acl->getUserId());
$params = array(
'errors' => array(),
'values' => $task,
'categories_list' => $this->category->getList($project['id']),
'projects' => $projects,
'current_project_id' => $project['id'],
'current_project_name' => $project['name'],
);
if ($this->request->isAjax()) {
$this->response->html($this->template->load('board_category', $params));
}
else {
$this->response->html($this->template->layout('board_category', $params + array(
'menu' => 'boards',
'title' => t('Change category').' - '.$task['title'],
)));
}
}
/**
* Validate a category modification
*
* @access public
*/
public function updateCategory()
{
$values = $this->request->getValues();
$this->checkProjectPermissions($values['project_id']);
list($valid,) = $this->task->validateCategoryModification($values);
if ($valid && $this->task->update($values)) {
$this->session->flash(t('Task updated successfully.'));
}
else {
$this->session->flashError(t('Unable to update your task.'));
}
$this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']);
}
/** /**
* Display the public version of a board * Display the public version of a board
* Access checked by a simple token, no user login, read only, auto-refresh * Access checked by a simple token, no user login, read only, auto-refresh
@ -125,7 +167,7 @@ class Board extends Base
// Token verification // Token verification
if (! $project) { if (! $project) {
$this->response->text('Not Authorized', 401); $this->forbidden(true);
} }
// Display the board with a specific layout // Display the board with a specific layout
@ -136,6 +178,7 @@ class Board extends Base
'title' => $project['name'], 'title' => $project['name'],
'no_layout' => true, 'no_layout' => true,
'auto_refresh' => true, 'auto_refresh' => true,
'not_editable' => true,
))); )));
} }
@ -146,62 +189,55 @@ class Board extends Base
*/ */
public function index() public function index()
{ {
$projects = $this->project->getListByStatus(ProjectModel::ACTIVE); $last_seen_project_id = $this->user->getLastSeenProjectId();
$project_id = 0; $favorite_project_id = $this->user->getFavoriteProjectId();
$project_name = ''; $project_id = $last_seen_project_id ?: $favorite_project_id;
if ($this->acl->isRegularUser()) { if (! $project_id) {
$projects = $this->project->filterListByAccess($projects, $this->acl->getUserId()); $projects = $this->project->getAvailableList($this->acl->getUserId());
}
if (empty($projects)) { if (empty($projects)) {
if ($this->acl->isAdminUser()) { if ($this->acl->isAdminUser()) {
$this->redirectNoProject(); $this->redirectNoProject();
}
$this->forbidden();
} }
else {
$this->response->redirect('?controller=project&action=forbidden'); $project_id = key($projects);
}
}
else if (! empty($_SESSION['user']['default_project_id']) && isset($projects[$_SESSION['user']['default_project_id']])) {
$project_id = $_SESSION['user']['default_project_id'];
$project_name = $projects[$_SESSION['user']['default_project_id']];
}
else {
list($project_id, $project_name) = each($projects);
} }
$this->response->redirect('?controller=board&action=show&project_id='.$project_id); $this->show($project_id);
} }
/** /**
* Show a board for a given project * Show a board for a given project
* *
* @access public * @access public
* @param integer $project_id Default project id
*/ */
public function show() public function show($project_id = 0)
{ {
$project_id = $this->request->getIntegerParam('project_id'); $project = $this->getProject($project_id);
$user_id = $this->request->getIntegerParam('user_id', UserModel::EVERYBODY_ID);
$this->checkProjectPermissions($project_id);
$projects = $this->project->getAvailableList($this->acl->getUserId()); $projects = $this->project->getAvailableList($this->acl->getUserId());
if (! isset($projects[$project_id])) { $board_selector = $projects;
$this->notfound(); unset($board_selector[$project['id']]);
}
$this->user->storeLastSeenProjectId($project['id']);
$this->response->html($this->template->layout('board_index', array( $this->response->html($this->template->layout('board_index', array(
'users' => $this->project->getUsersList($project_id, true, true), 'users' => $this->project->getUsersList($project['id'], true, true),
'filters' => array('user_id' => $user_id), 'filters' => array('user_id' => UserModel::EVERYBODY_ID),
'projects' => $projects, 'projects' => $projects,
'current_project_id' => $project_id, 'current_project_id' => $project['id'],
'current_project_name' => $projects[$project_id], 'current_project_name' => $projects[$project['id']],
'board' => $this->board->get($project_id), 'board' => $this->board->get($project['id']),
'categories' => $this->category->getList($project_id, true, true), 'categories' => $this->category->getList($project['id'], true, true),
'menu' => 'boards', 'menu' => 'boards',
'title' => $projects[$project_id], 'title' => $projects[$project['id']],
'board_selector' => $projects, 'board_selector' => $board_selector,
))); )));
} }
@ -212,12 +248,8 @@ class Board extends Base
*/ */
public function edit() public function edit()
{ {
$project_id = $this->request->getIntegerParam('project_id'); $project = $this->getProject();
$project = $this->project->getById($project_id); $columns = $this->board->getColumns($project['id']);
if (! $project) $this->notfound();
$columns = $this->board->getColumns($project_id);
$values = array(); $values = array();
foreach ($columns as $column) { foreach ($columns as $column) {
@ -225,9 +257,9 @@ class Board extends Base
$values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null; $values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null;
} }
$this->response->html($this->template->layout('board_edit', array( $this->response->html($this->projectLayout('board_edit', array(
'errors' => array(), 'errors' => array(),
'values' => $values + array('project_id' => $project_id), 'values' => $values + array('project_id' => $project['id']),
'columns' => $columns, 'columns' => $columns,
'project' => $project, 'project' => $project,
'menu' => 'projects', 'menu' => 'projects',
@ -242,12 +274,8 @@ class Board extends Base
*/ */
public function update() public function update()
{ {
$project_id = $this->request->getIntegerParam('project_id'); $project = $this->getProject();
$project = $this->project->getById($project_id); $columns = $this->board->getColumns($project['id']);
if (! $project) $this->notfound();
$columns = $this->board->getColumns($project_id);
$data = $this->request->getValues(); $data = $this->request->getValues();
$values = $columns_list = array(); $values = $columns_list = array();
@ -270,9 +298,9 @@ class Board extends Base
} }
} }
$this->response->html($this->template->layout('board_edit', array( $this->response->html($this->projectLayout('board_edit', array(
'errors' => $errors, 'errors' => $errors,
'values' => $values + array('project_id' => $project_id), 'values' => $values + array('project_id' => $project['id']),
'columns' => $columns, 'columns' => $columns,
'project' => $project, 'project' => $project,
'menu' => 'projects', 'menu' => 'projects',
@ -287,12 +315,8 @@ class Board extends Base
*/ */
public function add() public function add()
{ {
$project_id = $this->request->getIntegerParam('project_id'); $project = $this->getProject();
$project = $this->project->getById($project_id); $columns = $this->board->getColumnsList($project['id']);
if (! $project) $this->notfound();
$columns = $this->board->getColumnsList($project_id);
$data = $this->request->getValues(); $data = $this->request->getValues();
$values = array(); $values = array();
@ -304,7 +328,7 @@ class Board extends Base
if ($valid) { if ($valid) {
if ($this->board->add($data)) { if ($this->board->addColumn($project['id'], $data['title'])) {
$this->session->flash(t('Board updated successfully.')); $this->session->flash(t('Board updated successfully.'));
$this->response->redirect('?controller=board&action=edit&project_id='.$project['id']); $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']);
} }
@ -313,7 +337,7 @@ class Board extends Base
} }
} }
$this->response->html($this->template->layout('board_edit', array( $this->response->html($this->projectLayout('board_edit', array(
'errors' => $errors, 'errors' => $errors,
'values' => $values + $data, 'values' => $values + $data,
'columns' => $columns, 'columns' => $columns,
@ -330,8 +354,11 @@ class Board extends Base
*/ */
public function confirm() public function confirm()
{ {
$this->response->html($this->template->layout('board_remove', array( $project = $this->getProject();
$this->response->html($this->projectLayout('board_remove', array(
'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')), 'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')),
'project' => $project,
'menu' => 'projects', 'menu' => 'projects',
'title' => t('Remove a column from a board') 'title' => t('Remove a column from a board')
))); )));
@ -363,27 +390,31 @@ class Board extends Base
*/ */
public function save() public function save()
{ {
if ($this->request->isAjax()) { $project_id = $this->request->getIntegerParam('project_id');
if ($project_id > 0 && $this->request->isAjax()) {
if (! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) {
$this->response->status(401);
}
$project_id = $this->request->getIntegerParam('project_id');
$values = $this->request->getValues(); $values = $this->request->getValues();
if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) { if ($this->task->movePosition($project_id, $values['task_id'], $values['column_id'], $values['position'])) {
$this->response->text('Not Authorized', 401);
}
if (isset($values['positions'])) { $this->response->html(
$this->board->saveTasksPosition($values['positions']); $this->template->load('board_show', array(
'current_project_id' => $project_id,
'board' => $this->board->get($project_id),
'categories' => $this->category->getList($project_id, false),
)),
201
);
} }
else {
$this->response->html( $this->response->status(400);
$this->template->load('board_show', array( }
'current_project_id' => $project_id,
'board' => $this->board->get($project_id),
'categories' => $this->category->getList($project_id, false),
)),
201
);
} }
else { else {
$this->response->status(401); $this->response->status(401);

View file

@ -10,25 +10,6 @@ namespace Controller;
*/ */
class Category extends Base class Category extends Base
{ {
/**
* Get the current project (common method between actions)
*
* @access private
* @return array
*/
private function getProject()
{
$project_id = $this->request->getIntegerParam('project_id');
$project = $this->project->getById($project_id);
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
return $project;
}
/** /**
* Get the category (common method between actions) * Get the category (common method between actions)
* *
@ -57,7 +38,7 @@ class Category extends Base
{ {
$project = $this->getProject(); $project = $this->getProject();
$this->response->html($this->template->layout('category_index', array( $this->response->html($this->projectLayout('category_index', array(
'categories' => $this->category->getList($project['id'], false), 'categories' => $this->category->getList($project['id'], false),
'values' => array('project_id' => $project['id']), 'values' => array('project_id' => $project['id']),
'errors' => array(), 'errors' => array(),
@ -90,7 +71,7 @@ class Category extends Base
} }
} }
$this->response->html($this->template->layout('category_index', array( $this->response->html($this->projectLayout('category_index', array(
'categories' => $this->category->getList($project['id'], false), 'categories' => $this->category->getList($project['id'], false),
'values' => $values, 'values' => $values,
'errors' => $errors, 'errors' => $errors,
@ -110,7 +91,7 @@ class Category extends Base
$project = $this->getProject(); $project = $this->getProject();
$category = $this->getCategory($project['id']); $category = $this->getCategory($project['id']);
$this->response->html($this->template->layout('category_edit', array( $this->response->html($this->projectLayout('category_edit', array(
'values' => $category, 'values' => $category,
'errors' => array(), 'errors' => array(),
'project' => $project, 'project' => $project,
@ -142,7 +123,7 @@ class Category extends Base
} }
} }
$this->response->html($this->template->layout('category_edit', array( $this->response->html($this->projectLayout('category_edit', array(
'values' => $values, 'values' => $values,
'errors' => $errors, 'errors' => $errors,
'project' => $project, 'project' => $project,
@ -161,7 +142,7 @@ class Category extends Base
$project = $this->getProject(); $project = $this->getProject();
$category = $this->getCategory($project['id']); $category = $this->getCategory($project['id']);
$this->response->html($this->template->layout('category_remove', array( $this->response->html($this->projectLayout('category_remove', array(
'project' => $project, 'project' => $project,
'category' => $category, 'category' => $category,
'menu' => 'projects', 'menu' => 'projects',

View file

@ -25,25 +25,15 @@ class Comment extends Base
} }
if (! $this->acl->isAdminUser() && $comment['user_id'] != $this->acl->getUserId()) { if (! $this->acl->isAdminUser() && $comment['user_id'] != $this->acl->getUserId()) {
$this->forbidden(); $this->response->html($this->template->layout('comment_forbidden', array(
'menu' => 'tasks',
'title' => t('Access Forbidden')
)));
} }
return $comment; return $comment;
} }
/**
* Forbidden page for comments
*
* @access public
*/
public function forbidden()
{
$this->response->html($this->template->layout('comment_forbidden', array(
'menu' => 'tasks',
'title' => t('Access Forbidden')
)));
}
/** /**
* Add comment form * Add comment form
* *

View file

@ -19,16 +19,13 @@ class Config extends Base
{ {
$this->response->html($this->template->layout('config_index', array( $this->response->html($this->template->layout('config_index', array(
'db_size' => $this->config->getDatabaseSize(), 'db_size' => $this->config->getDatabaseSize(),
'user' => $_SESSION['user'],
'projects' => $this->project->getList(),
'languages' => $this->config->getLanguages(), 'languages' => $this->config->getLanguages(),
'values' => $this->config->getAll(), 'values' => $this->config->getAll(),
'errors' => array(), 'errors' => array(),
'menu' => 'config', 'menu' => 'config',
'title' => t('Settings'), 'title' => t('Settings'),
'timezones' => $this->config->getTimezones(), 'timezones' => $this->config->getTimezones(),
'remember_me_sessions' => $this->rememberMe->getAll($this->acl->getUserId()), 'default_columns' => implode(', ', $this->board->getDefaultColumns()),
'last_logins' => $this->lastLogin->getAll($this->acl->getUserId()),
))); )));
} }
@ -56,16 +53,13 @@ class Config extends Base
$this->response->html($this->template->layout('config_index', array( $this->response->html($this->template->layout('config_index', array(
'db_size' => $this->config->getDatabaseSize(), 'db_size' => $this->config->getDatabaseSize(),
'user' => $_SESSION['user'],
'projects' => $this->project->getList(),
'languages' => $this->config->getLanguages(), 'languages' => $this->config->getLanguages(),
'values' => $values, 'values' => $values,
'errors' => $errors, 'errors' => $errors,
'menu' => 'config', 'menu' => 'config',
'title' => t('Settings'), 'title' => t('Settings'),
'timezones' => $this->config->getTimezones(), 'timezones' => $this->config->getTimezones(),
'remember_me_sessions' => $this->rememberMe->getAll($this->acl->getUserId()), 'default_columns' => implode(', ', $this->board->getDefaultColumns()),
'last_logins' => $this->lastLogin->getAll($this->acl->getUserId()),
))); )));
} }
@ -106,16 +100,4 @@ class Config extends Base
$this->session->flash(t('All tokens have been regenerated.')); $this->session->flash(t('All tokens have been regenerated.'));
$this->response->redirect('?controller=config'); $this->response->redirect('?controller=config');
} }
/**
* Remove a "RememberMe" token
*
* @access public
*/
public function removeRememberMeToken()
{
$this->checkCSRFParam();
$this->rememberMe->remove($this->request->getIntegerParam('id'));
$this->response->redirect('?controller=config&action=index#remember-me');
}
} }

View file

@ -3,6 +3,7 @@
namespace Controller; namespace Controller;
use Model\Task as TaskModel; use Model\Task as TaskModel;
use Core\Translator;
/** /**
* Project controller * Project controller
@ -12,94 +13,6 @@ use Model\Task as TaskModel;
*/ */
class Project extends Base class Project extends Base
{ {
/**
* Task search for a given project
*
* @access public
*/
public function search()
{
$project_id = $this->request->getIntegerParam('project_id');
$search = $this->request->getStringParam('search');
$project = $this->project->getById($project_id);
$tasks = array();
$nb_tasks = 0;
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$this->checkProjectPermissions($project['id']);
if ($search !== '') {
$filters = array(
array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id),
'or' => array(
array('column' => 'title', 'operator' => 'like', 'value' => '%'.$search.'%'),
//array('column' => 'description', 'operator' => 'like', 'value' => '%'.$search.'%'),
)
);
$tasks = $this->task->find($filters);
$nb_tasks = count($tasks);
}
$this->response->html($this->template->layout('project_search', array(
'tasks' => $tasks,
'nb_tasks' => $nb_tasks,
'values' => array(
'search' => $search,
'controller' => 'project',
'action' => 'search',
'project_id' => $project['id'],
),
'menu' => 'projects',
'project' => $project,
'columns' => $this->board->getColumnsList($project_id),
'categories' => $this->category->getList($project['id'], false),
'title' => $project['name'].($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '')
)));
}
/**
* List of completed tasks for a given project
*
* @access public
*/
public function tasks()
{
$project_id = $this->request->getIntegerParam('project_id');
$project = $this->project->getById($project_id);
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$this->checkProjectPermissions($project['id']);
$filters = array(
array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id),
array('column' => 'is_active', 'operator' => 'eq', 'value' => TaskModel::STATUS_CLOSED),
);
$tasks = $this->task->find($filters);
$nb_tasks = count($tasks);
$this->response->html($this->template->layout('project_tasks', array(
'menu' => 'projects',
'project' => $project,
'columns' => $this->board->getColumnsList($project_id),
'categories' => $this->category->getList($project['id'], false),
'tasks' => $tasks,
'nb_tasks' => $nb_tasks,
'title' => $project['name'].' ('.$nb_tasks.')'
)));
}
/** /**
* List of projects * List of projects
* *
@ -107,11 +20,23 @@ class Project extends Base
*/ */
public function index() public function index()
{ {
$projects = $this->project->getAll(true, $this->acl->isRegularUser()); $projects = $this->project->getAll($this->acl->isRegularUser());
$nb_projects = count($projects); $nb_projects = count($projects);
$active_projects = array();
$inactive_projects = array();
foreach ($projects as $project) {
if ($project['is_active'] == 1) {
$active_projects[] = $project;
}
else {
$inactive_projects[] = $project;
}
}
$this->response->html($this->template->layout('project_index', array( $this->response->html($this->template->layout('project_index', array(
'projects' => $projects, 'active_projects' => $active_projects,
'inactive_projects' => $inactive_projects,
'nb_projects' => $nb_projects, 'nb_projects' => $nb_projects,
'menu' => 'projects', 'menu' => 'projects',
'title' => t('Projects').' ('.$nb_projects.')' 'title' => t('Projects').' ('.$nb_projects.')'
@ -119,49 +44,108 @@ class Project extends Base
} }
/** /**
* Display a form to create a new project * Show the project information page
* *
* @access public * @access public
*/ */
public function create() public function show()
{ {
$this->response->html($this->template->layout('project_new', array( $project = $this->getProject();
'errors' => array(),
'values' => array(), $this->response->html($this->projectLayout('project_show', array(
'project' => $project,
'stats' => $this->project->getStats($project['id']),
'menu' => 'projects', 'menu' => 'projects',
'title' => t('New project') 'title' => $project['name'],
))); )));
} }
/** /**
* Validate and save a new project * Task export
* *
* @access public * @access public
*/ */
public function save() public function export()
{ {
$values = $this->request->getValues(); $project = $this->getProject();
list($valid, $errors) = $this->project->validateCreation($values); $from = $this->request->getStringParam('from');
$to = $this->request->getStringParam('to');
if ($valid) { if ($from && $to) {
$data = $this->task->export($project['id'], $from, $to);
if ($this->project->create($values)) { $this->response->forceDownload('Export_'.date('Y_m_d_H_i_S').'.csv');
$this->session->flash(t('Your project have been created successfully.')); $this->response->csv($data);
$this->response->redirect('?controller=project');
}
else {
$this->session->flashError(t('Unable to create your project.'));
}
} }
$this->response->html($this->template->layout('project_new', array( $this->response->html($this->projectLayout('project_export', array(
'errors' => $errors, 'values' => array(
'values' => $values, 'controller' => 'project',
'action' => 'export',
'project_id' => $project['id'],
'from' => $from,
'to' => $to,
),
'errors' => array(),
'menu' => 'projects', 'menu' => 'projects',
'title' => t('New Project') 'project' => $project,
'title' => t('Tasks Export')
))); )));
} }
/**
* Public access management
*
* @access public
*/
public function share()
{
$project = $this->getProject();
$this->response->html($this->projectLayout('project_share', array(
'project' => $project,
'menu' => 'projects',
'title' => t('Public access'),
)));
}
/**
* Enable public access for a project
*
* @access public
*/
public function enablePublic()
{
$this->checkCSRFParam();
$project_id = $this->request->getIntegerParam('project_id');
if ($project_id && $this->project->enablePublicAccess($project_id)) {
$this->session->flash(t('Project updated successfully.'));
} else {
$this->session->flashError(t('Unable to update this project.'));
}
$this->response->redirect('?controller=project&action=share&project_id='.$project_id);
}
/**
* Disable public access for a project
*
* @access public
*/
public function disablePublic()
{
$this->checkCSRFParam();
$project_id = $this->request->getIntegerParam('project_id');
if ($project_id && $this->project->disablePublicAccess($project_id)) {
$this->session->flash(t('Project updated successfully.'));
} else {
$this->session->flashError(t('Unable to update this project.'));
}
$this->response->redirect('?controller=project&action=share&project_id='.$project_id);
}
/** /**
* Display a form to edit a project * Display a form to edit a project
* *
@ -169,16 +153,12 @@ class Project extends Base
*/ */
public function edit() public function edit()
{ {
$project = $this->project->getById($this->request->getIntegerParam('project_id')); $project = $this->getProject();
if (! $project) { $this->response->html($this->projectLayout('project_edit', array(
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$this->response->html($this->template->layout('project_edit', array(
'errors' => array(), 'errors' => array(),
'values' => $project, 'values' => $project,
'project' => $project,
'menu' => 'projects', 'menu' => 'projects',
'title' => t('Edit project') 'title' => t('Edit project')
))); )));
@ -191,6 +171,7 @@ class Project extends Base
*/ */
public function update() public function update()
{ {
$project = $this->getProject();
$values = $this->request->getValues() + array('is_active' => 0); $values = $this->request->getValues() + array('is_active' => 0);
list($valid, $errors) = $this->project->validateModification($values); list($valid, $errors) = $this->project->validateModification($values);
@ -198,114 +179,32 @@ class Project extends Base
if ($this->project->update($values)) { if ($this->project->update($values)) {
$this->session->flash(t('Project updated successfully.')); $this->session->flash(t('Project updated successfully.'));
$this->response->redirect('?controller=project'); $this->response->redirect('?controller=project&action=edit&project_id='.$project['id']);
} }
else { else {
$this->session->flashError(t('Unable to update this project.')); $this->session->flashError(t('Unable to update this project.'));
} }
} }
$this->response->html($this->template->layout('project_edit', array( $this->response->html($this->projectLayout('project_edit', array(
'errors' => $errors, 'errors' => $errors,
'values' => $values, 'values' => $values,
'project' => $project,
'menu' => 'projects', 'menu' => 'projects',
'title' => t('Edit Project') 'title' => t('Edit Project')
))); )));
} }
/** /**
* Confirmation dialog before to remove a project
*
* @access public
*/
public function confirm()
{
$project = $this->project->getById($this->request->getIntegerParam('project_id'));
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$this->response->html($this->template->layout('project_remove', array(
'project' => $project,
'menu' => 'projects',
'title' => t('Remove project')
)));
}
/**
* Remove a project
*
* @access public
*/
public function remove()
{
$this->checkCSRFParam();
$project_id = $this->request->getIntegerParam('project_id');
if ($project_id && $this->project->remove($project_id)) {
$this->session->flash(t('Project removed successfully.'));
} else {
$this->session->flashError(t('Unable to remove this project.'));
}
$this->response->redirect('?controller=project');
}
/**
* Enable a project
*
* @access public
*/
public function enable()
{
$this->checkCSRFParam();
$project_id = $this->request->getIntegerParam('project_id');
if ($project_id && $this->project->enable($project_id)) {
$this->session->flash(t('Project activated successfully.'));
} else {
$this->session->flashError(t('Unable to activate this project.'));
}
$this->response->redirect('?controller=project');
}
/**
* Disable a project
*
* @access public
*/
public function disable()
{
$this->checkCSRFParam();
$project_id = $this->request->getIntegerParam('project_id');
if ($project_id && $this->project->disable($project_id)) {
$this->session->flash(t('Project disabled successfully.'));
} else {
$this->session->flashError(t('Unable to disable this project.'));
}
$this->response->redirect('?controller=project');
}
/**
* Users list for the selected project * Users list for the selected project
* *
* @access public * @access public
*/ */
public function users() public function users()
{ {
$project = $this->project->getById($this->request->getIntegerParam('project_id')); $project = $this->getProject();
if (! $project) { $this->response->html($this->projectLayout('project_users', array(
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$this->response->html($this->template->layout('project_users', array(
'project' => $project, 'project' => $project,
'users' => $this->project->getAllUsers($project['id']), 'users' => $this->project->getAllUsers($project['id']),
'menu' => 'projects', 'menu' => 'projects',
@ -364,4 +263,298 @@ class Project extends Base
$this->response->redirect('?controller=project&action=users&project_id='.$values['project_id']); $this->response->redirect('?controller=project&action=users&project_id='.$values['project_id']);
} }
/**
* Confirmation dialog before to remove a project
*
* @access public
*/
public function confirmRemove()
{
$project = $this->getProject();
$this->response->html($this->projectLayout('project_remove', array(
'project' => $project,
'menu' => 'projects',
'title' => t('Remove project')
)));
}
/**
* Remove a project
*
* @access public
*/
public function remove()
{
$this->checkCSRFParam();
$project_id = $this->request->getIntegerParam('project_id');
if ($project_id && $this->project->remove($project_id)) {
$this->session->flash(t('Project removed successfully.'));
} else {
$this->session->flashError(t('Unable to remove this project.'));
}
$this->response->redirect('?controller=project');
}
/**
* Confirmation dialog before to clone a project
*
* @access public
*/
public function confirmDuplicate()
{
$project = $this->getProject();
$this->response->html($this->projectLayout('project_duplicate', array(
'project' => $project,
'menu' => 'projects',
'title' => t('Clone this project')
)));
}
/**
* Duplicate a project
*
* @author Antonio Rabelo
* @access public
*/
public function duplicate()
{
$this->checkCSRFParam();
$project_id = $this->request->getIntegerParam('project_id');
if ($project_id && $this->project->duplicate($project_id)) {
$this->session->flash(t('Project cloned successfully.'));
} else {
$this->session->flashError(t('Unable to clone this project.'));
}
$this->response->redirect('?controller=project');
}
/**
* Confirmation dialog before to disable a project
*
* @access public
*/
public function confirmDisable()
{
$project = $this->getProject();
$this->response->html($this->projectLayout('project_disable', array(
'project' => $project,
'menu' => 'projects',
'title' => t('Project activation')
)));
}
/**
* Disable a project
*
* @access public
*/
public function disable()
{
$this->checkCSRFParam();
$project_id = $this->request->getIntegerParam('project_id');
if ($project_id && $this->project->disable($project_id)) {
$this->session->flash(t('Project disabled successfully.'));
} else {
$this->session->flashError(t('Unable to disable this project.'));
}
$this->response->redirect('?controller=project&action=show&project_id='.$project_id);
}
/**
* Confirmation dialog before to enable a project
*
* @access public
*/
public function confirmEnable()
{
$project = $this->getProject();
$this->response->html($this->projectLayout('project_enable', array(
'project' => $project,
'menu' => 'projects',
'title' => t('Project activation')
)));
}
/**
* Enable a project
*
* @access public
*/
public function enable()
{
$this->checkCSRFParam();
$project_id = $this->request->getIntegerParam('project_id');
if ($project_id && $this->project->enable($project_id)) {
$this->session->flash(t('Project activated successfully.'));
} else {
$this->session->flashError(t('Unable to activate this project.'));
}
$this->response->redirect('?controller=project&action=show&project_id='.$project_id);
}
/**
* RSS feed for a project
*
* @access public
*/
public function feed()
{
$token = $this->request->getStringParam('token');
$project = $this->project->getByToken($token);
// Token verification
if (! $project) {
$this->forbidden(true);
}
$this->response->xml($this->template->load('project_feed', array(
'events' => $this->project->getActivity($project['id']),
'project' => $project,
)));
}
/**
* Activity page for a project
*
* @access public
*/
public function activity()
{
$project = $this->getProject();
$this->response->html($this->template->layout('project_activity', array(
'events' => $this->project->getActivity($project['id']),
'menu' => 'projects',
'project' => $project,
'title' => t('%s\'s activity', $project['name'])
)));
}
/**
* Task search for a given project
*
* @access public
*/
public function search()
{
$project = $this->getProject();
$search = $this->request->getStringParam('search');
$tasks = array();
$nb_tasks = 0;
if ($search !== '') {
$filters = array(
array('column' => 'project_id', 'operator' => 'eq', 'value' => $project['id']),
'or' => array(
array('column' => 'title', 'operator' => 'like', 'value' => '%'.$search.'%'),
//array('column' => 'description', 'operator' => 'like', 'value' => '%'.$search.'%'),
)
);
$tasks = $this->task->find($filters);
$nb_tasks = count($tasks);
}
$this->response->html($this->template->layout('project_search', array(
'tasks' => $tasks,
'nb_tasks' => $nb_tasks,
'values' => array(
'search' => $search,
'controller' => 'project',
'action' => 'search',
'project_id' => $project['id'],
),
'menu' => 'projects',
'project' => $project,
'columns' => $this->board->getColumnsList($project['id']),
'categories' => $this->category->getList($project['id'], false),
'title' => $project['name'].($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '')
)));
}
/**
* List of completed tasks for a given project
*
* @access public
*/
public function tasks()
{
$project = $this->getProject();
$filters = array(
array('column' => 'project_id', 'operator' => 'eq', 'value' => $project['id']),
array('column' => 'is_active', 'operator' => 'eq', 'value' => TaskModel::STATUS_CLOSED),
);
$tasks = $this->task->find($filters);
$nb_tasks = count($tasks);
$this->response->html($this->template->layout('project_tasks', array(
'menu' => 'projects',
'project' => $project,
'columns' => $this->board->getColumnsList($project['id']),
'categories' => $this->category->getList($project['id'], false),
'tasks' => $tasks,
'nb_tasks' => $nb_tasks,
'title' => $project['name'].' ('.$nb_tasks.')'
)));
}
/**
* Display a form to create a new project
*
* @access public
*/
public function create()
{
$this->response->html($this->template->layout('project_new', array(
'errors' => array(),
'values' => array(),
'menu' => 'projects',
'title' => t('New project')
)));
}
/**
* Validate and save a new project
*
* @access public
*/
public function save()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->project->validateCreation($values);
if ($valid) {
if ($this->project->create($values)) {
$this->session->flash(t('Your project have been created successfully.'));
$this->response->redirect('?controller=project');
}
else {
$this->session->flashError(t('Unable to create your project.'));
}
}
$this->response->html($this->template->layout('project_new', array(
'errors' => $errors,
'values' => $values,
'menu' => 'projects',
'title' => t('New Project')
)));
}
} }

View file

@ -58,7 +58,7 @@ class Subtask extends Base
$task = $this->getTask(); $task = $this->getTask();
$values = $this->request->getValues(); $values = $this->request->getValues();
list($valid, $errors) = $this->subTask->validate($values); list($valid, $errors) = $this->subTask->validateCreation($values);
if ($valid) { if ($valid) {
@ -119,7 +119,7 @@ class Subtask extends Base
$subtask = $this->getSubtask(); $subtask = $this->getSubtask();
$values = $this->request->getValues(); $values = $this->request->getValues();
list($valid, $errors) = $this->subTask->validate($values); list($valid, $errors) = $this->subTask->validateModification($values);
if ($valid) { if ($valid) {

View file

@ -30,17 +30,13 @@ class Task extends Base
$values = array( $values = array(
'title' => $this->request->getStringParam('title'), 'title' => $this->request->getStringParam('title'),
'description' => $this->request->getStringParam('description'), 'description' => $this->request->getStringParam('description'),
'color_id' => $this->request->getStringParam('color_id', 'blue'), 'color_id' => $this->request->getStringParam('color_id'),
'project_id' => $this->request->getIntegerParam('project_id', $defaultProject['id']), 'project_id' => $this->request->getIntegerParam('project_id', $defaultProject['id']),
'owner_id' => $this->request->getIntegerParam('owner_id'), 'owner_id' => $this->request->getIntegerParam('owner_id'),
'column_id' => $this->request->getIntegerParam('column_id'), 'column_id' => $this->request->getIntegerParam('column_id'),
'category_id' => $this->request->getIntegerParam('category_id'), 'category_id' => $this->request->getIntegerParam('category_id'),
); );
if ($values['column_id'] == 0) {
$values['column_id'] = $this->board->getFirstColumn($values['project_id']);
}
list($valid,) = $this->task->validateCreation($values); list($valid,) = $this->task->validateCreation($values);
if ($valid && $this->task->create($values)) { if ($valid && $this->task->create($values)) {
@ -50,6 +46,40 @@ class Task extends Base
$this->response->text('FAILED'); $this->response->text('FAILED');
} }
/**
* Public access (display a task)
*
* @access public
*/
public function readonly()
{
$project = $this->project->getByToken($this->request->getStringParam('token'));
// Token verification
if (! $project) {
$this->forbidden(true);
}
$task = $this->task->getById($this->request->getIntegerParam('task_id'), true);
if (! $task) {
$this->notfound(true);
}
$this->response->html($this->template->layout('task_public', array(
'project' => $project,
'comments' => $this->comment->getAll($task['id']),
'subtasks' => $this->subTask->getAll($task['id']),
'task' => $task,
'columns_list' => $this->board->getColumnsList($task['project_id']),
'colors_list' => $this->task->getColors(),
'title' => $task['title'],
'no_layout' => true,
'auto_refresh' => true,
'not_editable' => true,
)));
}
/** /**
* Show a task * Show a task
* *
@ -60,6 +90,7 @@ class Task extends Base
$task = $this->getTask(); $task = $this->getTask();
$this->response->html($this->taskLayout('task_show', array( $this->response->html($this->taskLayout('task_show', array(
'project' => $this->project->getById($task['project_id']),
'files' => $this->file->getAll($task['id']), 'files' => $this->file->getAll($task['id']),
'comments' => $this->comment->getAll($task['id']), 'comments' => $this->comment->getAll($task['id']),
'subtasks' => $this->subTask->getAll($task['id']), 'subtasks' => $this->subTask->getAll($task['id']),
@ -168,7 +199,6 @@ class Task extends Base
'values' => $task, 'values' => $task,
'errors' => array(), 'errors' => array(),
'task' => $task, 'task' => $task,
'columns_list' => $this->board->getColumnsList($task['project_id']),
'users_list' => $this->project->getUsersList($task['project_id']), 'users_list' => $this->project->getUsersList($task['project_id']),
'colors_list' => $this->task->getColors(), 'colors_list' => $this->task->getColors(),
'categories_list' => $this->category->getList($task['project_id']), 'categories_list' => $this->category->getList($task['project_id']),
@ -233,27 +263,21 @@ class Task extends Base
*/ */
public function close() public function close()
{ {
$this->checkCSRFParam();
$task = $this->getTask(); $task = $this->getTask();
if ($this->task->close($task['id'])) { if ($this->request->getStringParam('confirmation') === 'yes') {
$this->session->flash(t('Task closed successfully.'));
} else { $this->checkCSRFParam();
$this->session->flashError(t('Unable to close this task.'));
if ($this->task->close($task['id'])) {
$this->session->flash(t('Task closed successfully.'));
} else {
$this->session->flashError(t('Unable to close this task.'));
}
$this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
} }
$this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
}
/**
* Confirmation dialog before to close a task
*
* @access public
*/
public function confirmClose()
{
$task = $this->getTask();
$this->response->html($this->taskLayout('task_close', array( $this->response->html($this->taskLayout('task_close', array(
'task' => $task, 'task' => $task,
'menu' => 'tasks', 'menu' => 'tasks',
@ -268,27 +292,21 @@ class Task extends Base
*/ */
public function open() public function open()
{ {
$this->checkCSRFParam();
$task = $this->getTask(); $task = $this->getTask();
if ($this->task->open($task['id'])) { if ($this->request->getStringParam('confirmation') === 'yes') {
$this->session->flash(t('Task opened successfully.'));
} else { $this->checkCSRFParam();
$this->session->flashError(t('Unable to open this task.'));
if ($this->task->open($task['id'])) {
$this->session->flash(t('Task opened successfully.'));
} else {
$this->session->flashError(t('Unable to open this task.'));
}
$this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
} }
$this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
}
/**
* Confirmation dialog before to open a task
*
* @access public
*/
public function confirmOpen()
{
$task = $this->getTask();
$this->response->html($this->taskLayout('task_open', array( $this->response->html($this->taskLayout('task_open', array(
'task' => $task, 'task' => $task,
'menu' => 'tasks', 'menu' => 'tasks',
@ -303,27 +321,21 @@ class Task extends Base
*/ */
public function remove() public function remove()
{ {
$this->checkCSRFParam();
$task = $this->getTask(); $task = $this->getTask();
if ($this->task->remove($task['id'])) { if ($this->request->getStringParam('confirmation') === 'yes') {
$this->session->flash(t('Task removed successfully.'));
} else { $this->checkCSRFParam();
$this->session->flashError(t('Unable to remove this task.'));
if ($this->task->remove($task['id'])) {
$this->session->flash(t('Task removed successfully.'));
} else {
$this->session->flashError(t('Unable to remove this task.'));
}
$this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
} }
$this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
}
/**
* Confirmation dialog before removing a task
*
* @access public
*/
public function confirmRemove()
{
$task = $this->getTask();
$this->response->html($this->taskLayout('task_remove', array( $this->response->html($this->taskLayout('task_remove', array(
'task' => $task, 'task' => $task,
'menu' => 'tasks', 'menu' => 'tasks',
@ -332,7 +344,7 @@ class Task extends Base
} }
/** /**
* Duplicate a task (fill the form for a new task) * Duplicate a task
* *
* @access public * @access public
*/ */
@ -340,26 +352,24 @@ class Task extends Base
{ {
$task = $this->getTask(); $task = $this->getTask();
if (! empty($task['date_due'])) { if ($this->request->getStringParam('confirmation') === 'yes') {
$task['date_due'] = date(t('m/d/Y'), $task['date_due']);
} $this->checkCSRFParam();
else { $task_id = $this->task->duplicateSameProject($task);
$task['date_due'] = '';
if ($task_id) {
$this->session->flash(t('Task created successfully.'));
$this->response->redirect('?controller=task&action=show&task_id='.$task_id);
} else {
$this->session->flashError(t('Unable to create this task.'));
$this->response->redirect('?controller=task&action=duplicate&task_id='.$task['id']);
}
} }
$task['score'] = $task['score'] ?: ''; $this->response->html($this->taskLayout('task_duplicate', array(
'task' => $task,
$this->response->html($this->template->layout('task_new', array(
'errors' => array(),
'values' => $task,
'projects_list' => $this->project->getListByStatus(ProjectModel::ACTIVE),
'columns_list' => $this->board->getColumnsList($task['project_id']),
'users_list' => $this->project->getUsersList($task['project_id']),
'colors_list' => $this->task->getColors(),
'categories_list' => $this->category->getList($task['project_id']),
'duplicate' => true,
'menu' => 'tasks', 'menu' => 'tasks',
'title' => t('New task') 'title' => t('Duplicate a task')
))); )));
} }
@ -368,19 +378,49 @@ class Task extends Base
* *
* @access public * @access public
*/ */
public function editDescription() public function description()
{ {
$task = $this->getTask(); $task = $this->getTask();
$ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
if ($this->request->isPost()) {
$values = $this->request->getValues();
list($valid, $errors) = $this->task->validateDescriptionCreation($values);
if ($valid) {
if ($this->task->update($values)) {
$this->session->flash(t('Task updated successfully.'));
}
else {
$this->session->flashError(t('Unable to update your task.'));
}
if ($ajax) {
$this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
}
else {
$this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
}
}
}
else {
$values = $task;
$errors = array();
}
$params = array( $params = array(
'values' => $task, 'values' => $values,
'errors' => array(), 'errors' => $errors,
'task' => $task, 'task' => $task,
'ajax' => $this->request->isAjax(), 'ajax' => $ajax,
'menu' => 'tasks', 'menu' => 'tasks',
'title' => t('Edit the description') 'title' => t('Edit the description'),
); );
if ($this->request->isAjax()) {
if ($ajax) {
$this->response->html($this->template->load('task_edit_description', $params)); $this->response->html($this->template->load('task_edit_description', $params));
} }
else { else {
@ -389,40 +429,63 @@ class Task extends Base
} }
/** /**
* Save and validation the description * Move a task to another project
* *
* @access public * @access public
*/ */
public function saveDescription() public function move()
{
$this->toAnotherProject('move');
}
/**
* Duplicate a task to another project
*
* @access public
*/
public function copy()
{
$this->toAnotherProject('duplicate');
}
/**
* Common methods between the actions "move" and "copy"
*
* @access private
*/
private function toAnotherProject($action)
{ {
$task = $this->getTask(); $task = $this->getTask();
$values = $this->request->getValues(); $values = $task;
$errors = array();
$projects_list = $this->project->getAvailableList($this->acl->getUserId());
list($valid, $errors) = $this->task->validateDescriptionCreation($values); unset($projects_list[$task['project_id']]);
if ($valid) { if ($this->request->isPost()) {
if ($this->task->update($values)) { $values = $this->request->getValues();
$this->session->flash(t('Task updated successfully.')); list($valid, $errors) = $this->task->validateProjectModification($values);
}
else {
$this->session->flashError(t('Unable to update your task.'));
}
if ($this->request->getIntegerParam('ajax')) { if ($valid) {
$this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); $task_id = $this->task->{$action.'ToAnotherProject'}($values['project_id'], $task);
} if ($task_id) {
else { $this->session->flash(t('Task created successfully.'));
$this->response->redirect('?controller=task&action=show&task_id='.$task['id']); $this->response->redirect('?controller=task&action=show&task_id='.$task_id);
}
else {
$this->session->flashError(t('Unable to create your task.'));
}
} }
} }
$this->response->html($this->taskLayout('task_edit_description', array( $this->response->html($this->taskLayout('task_'.$action.'_project', array(
'values' => $values, 'values' => $values,
'errors' => $errors, 'errors' => $errors,
'task' => $task, 'task' => $task,
'projects_list' => $projects_list,
'menu' => 'tasks', 'menu' => 'tasks',
'title' => t('Edit the description') 'title' => t(ucfirst($action).' the task to another project')
))); )));
} }
} }

View file

@ -18,7 +18,7 @@ class User extends Base
public function logout() public function logout()
{ {
$this->checkCSRFParam(); $this->checkCSRFParam();
$this->rememberMe->destroy($this->acl->getUserId()); $this->authentication->backend('rememberMe')->destroy($this->acl->getUserId());
$this->session->close(); $this->session->close();
$this->response->redirect('?controller=user&action=login'); $this->response->redirect('?controller=user&action=login');
} }
@ -30,7 +30,7 @@ class User extends Base
*/ */
public function login() public function login()
{ {
if (isset($_SESSION['user'])) { if ($this->acl->isLogged()) {
$this->response->redirect('?controller=app'); $this->response->redirect('?controller=app');
} }
@ -50,10 +50,10 @@ class User extends Base
public function check() public function check()
{ {
$values = $this->request->getValues(); $values = $this->request->getValues();
list($valid, $errors) = $this->user->validateLogin($values); list($valid, $errors) = $this->authentication->validateForm($values);
if ($valid) { if ($valid) {
$this->response->redirect('?controller=app'); $this->response->redirect('?controller=board');
} }
$this->response->html($this->template->layout('user_login', array( $this->response->html($this->template->layout('user_login', array(
@ -64,6 +64,48 @@ class User extends Base
))); )));
} }
/**
* Common layout for project views
*
* @access private
* @param string $template Template name
* @param array $params Template parameters
* @return string
*/
private function layout($template, array $params)
{
$content = $this->template->load($template, $params);
$params['user_content_for_layout'] = $content;
$params['menu'] = 'users';
if (isset($params['user'])) {
$params['title'] = $params['user']['name'] ?: $params['user']['username'];
}
return $this->template->layout('user_layout', $params);
}
/**
* Common method to get the user
*
* @access private
* @return array
*/
private function getUser()
{
$user = $this->user->getById($this->request->getIntegerParam('user_id'));
if (! $user) {
$this->notfound();
}
if ($this->acl->isRegularUser() && $this->acl->getUserId() != $user['id']) {
$this->forbidden();
}
return $user;
}
/** /**
* List all users * List all users
* *
@ -130,6 +172,134 @@ class User extends Base
))); )));
} }
/**
* Display user information
*
* @access public
*/
public function show()
{
$user = $this->getUser();
$this->response->html($this->layout('user_show', array(
'projects' => $this->project->getAvailableList($user['id']),
'user' => $user,
)));
}
/**
* Display last connections
*
* @access public
*/
public function last()
{
$user = $this->getUser();
$this->response->html($this->layout('user_last', array(
'last_logins' => $this->lastLogin->getAll($user['id']),
'user' => $user,
)));
}
/**
* Display user sessions
*
* @access public
*/
public function sessions()
{
$user = $this->getUser();
$this->response->html($this->layout('user_sessions', array(
'sessions' => $this->authentication->backend('rememberMe')->getAll($user['id']),
'user' => $user,
)));
}
/**
* Remove a "RememberMe" token
*
* @access public
*/
public function removeSession()
{
$this->checkCSRFParam();
$user = $this->getUser();
$this->authentication->backend('rememberMe')->remove($this->request->getIntegerParam('id'));
$this->response->redirect('?controller=user&action=sessions&user_id='.$user['id']);
}
/**
* Display user notifications
*
* @access public
*/
public function notifications()
{
$user = $this->getUser();
if ($this->request->isPost()) {
$values = $this->request->getValues();
$this->notification->saveSettings($user['id'], $values);
$this->session->flash(t('User updated successfully.'));
$this->response->redirect('?controller=user&action=notifications&user_id='.$user['id']);
}
$this->response->html($this->layout('user_notifications', array(
'projects' => $this->project->getAvailableList($user['id']),
'notifications' => $this->notification->readSettings($user['id']),
'user' => $user,
)));
}
/**
* Display external accounts
*
* @access public
*/
public function external()
{
$user = $this->getUser();
$this->response->html($this->layout('user_external', array(
'last_logins' => $this->lastLogin->getAll($user['id']),
'user' => $user,
)));
}
/**
* Password modification
*
* @access public
*/
public function password()
{
$user = $this->getUser();
$values = array('id' => $user['id']);
$errors = array();
if ($this->request->isPost()) {
$values = $this->request->getValues();
list($valid, $errors) = $this->user->validatePasswordModification($values);
if ($valid) {
if ($this->user->update($values)) {
$this->session->flash(t('Password modified successfully.'));
}
else {
$this->session->flashError(t('Unable to change the password.'));
}
$this->response->redirect('?controller=user&action=show&user_id='.$user['id']);
}
}
$this->response->html($this->layout('user_password', array(
'values' => $values,
'errors' => $errors,
'user' => $user,
)));
}
/** /**
* Display a form to edit a user * Display a form to edit a user
* *
@ -137,85 +307,46 @@ class User extends Base
*/ */
public function edit() public function edit()
{ {
$user = $this->user->getById($this->request->getIntegerParam('user_id')); $user = $this->getUser();
$values = $user;
$errors = array();
if (! $user) $this->notfound(); unset($values['password']);
if ($this->acl->isRegularUser() && $this->acl->getUserId() != $user['id']) { if ($this->request->isPost()) {
$this->forbidden();
}
unset($user['password']); $values = $this->request->getValues();
$this->response->html($this->template->layout('user_edit', array( if ($this->acl->isAdminUser()) {
'projects' => $this->project->filterListByAccess($this->project->getList(), $user['id']), $values += array('is_admin' => 0);
'errors' => array(),
'values' => $user,
'menu' => 'users',
'title' => t('Edit user')
)));
}
/**
* Validate and update a user
*
* @access public
*/
public function update()
{
$values = $this->request->getValues();
if ($this->acl->isAdminUser()) {
$values += array('is_admin' => 0);
}
else {
if ($this->acl->getUserId() != $values['id']) {
$this->forbidden();
}
if (isset($values['is_admin'])) {
unset($values['is_admin']); // Regular users can't be admin
}
}
list($valid, $errors) = $this->user->validateModification($values);
if ($valid) {
if ($this->user->update($values)) {
$this->session->flash(t('User updated successfully.'));
$this->response->redirect('?controller=user');
} }
else { else {
$this->session->flashError(t('Unable to update your user.'));
if (isset($values['is_admin'])) {
unset($values['is_admin']); // Regular users can't be admin
}
}
list($valid, $errors) = $this->user->validateModification($values);
if ($valid) {
if ($this->user->update($values)) {
$this->session->flash(t('User updated successfully.'));
}
else {
$this->session->flashError(t('Unable to update your user.'));
}
$this->response->redirect('?controller=user&action=show&user_id='.$user['id']);
} }
} }
$this->response->html($this->template->layout('user_edit', array( $this->response->html($this->layout('user_edit', array(
'projects' => $this->project->filterListByAccess($this->project->getList(), $values['id']),
'errors' => $errors,
'values' => $values, 'values' => $values,
'menu' => 'users', 'errors' => $errors,
'title' => t('Edit user') 'projects' => $this->project->filterListByAccess($this->project->getList(), $user['id']),
)));
}
/**
* Confirmation dialog before to remove a user
*
* @access public
*/
public function confirm()
{
$user = $this->user->getById($this->request->getIntegerParam('user_id'));
if (! $user) $this->notfound();
$this->response->html($this->template->layout('user_remove', array(
'user' => $user, 'user' => $user,
'menu' => 'users',
'title' => t('Remove user')
))); )));
} }
@ -226,16 +357,24 @@ class User extends Base
*/ */
public function remove() public function remove()
{ {
$this->checkCSRFParam(); $user = $this->getUser();
$user_id = $this->request->getIntegerParam('user_id');
if ($user_id && $this->user->remove($user_id)) { if ($this->request->getStringParam('confirmation') === 'yes') {
$this->session->flash(t('User removed successfully.'));
} else { $this->checkCSRFParam();
$this->session->flashError(t('Unable to remove this user.'));
if ($this->user->remove($user['id'])) {
$this->session->flash(t('User removed successfully.'));
} else {
$this->session->flashError(t('Unable to remove this user.'));
}
$this->response->redirect('?controller=user');
} }
$this->response->redirect('?controller=user'); $this->response->html($this->layout('user_remove', array(
'user' => $user,
)));
} }
/** /**
@ -249,23 +388,23 @@ class User extends Base
if ($code) { if ($code) {
$profile = $this->google->getGoogleProfile($code); $profile = $this->authentication->backend('google')->getGoogleProfile($code);
if (is_array($profile)) { if (is_array($profile)) {
// If the user is already logged, link the account otherwise authenticate // If the user is already logged, link the account otherwise authenticate
if ($this->acl->isLogged()) { if ($this->acl->isLogged()) {
if ($this->google->updateUser($this->acl->getUserId(), $profile)) { if ($this->authentication->backend('google')->updateUser($this->acl->getUserId(), $profile)) {
$this->session->flash(t('Your Google Account is linked to your profile successfully.')); $this->session->flash(t('Your Google Account is linked to your profile successfully.'));
} }
else { else {
$this->session->flashError(t('Unable to link your Google Account.')); $this->session->flashError(t('Unable to link your Google Account.'));
} }
$this->response->redirect('?controller=user'); $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId());
} }
else if ($this->google->authenticate($profile['id'])) { else if ($this->authentication->backend('google')->authenticate($profile['id'])) {
$this->response->redirect('?controller=app'); $this->response->redirect('?controller=app');
} }
else { else {
@ -279,7 +418,7 @@ class User extends Base
} }
} }
$this->response->redirect($this->google->getAuthorizationUrl()); $this->response->redirect($this->authentication->backend('google')->getAuthorizationUrl());
} }
/** /**
@ -290,14 +429,14 @@ class User extends Base
public function unlinkGoogle() public function unlinkGoogle()
{ {
$this->checkCSRFParam(); $this->checkCSRFParam();
if ($this->google->unlink($this->acl->getUserId())) { if ($this->authentication->backend('google')->unlink($this->acl->getUserId())) {
$this->session->flash(t('Your Google Account is not linked anymore to your profile.')); $this->session->flash(t('Your Google Account is not linked anymore to your profile.'));
} }
else { else {
$this->session->flashError(t('Unable to unlink your Google Account.')); $this->session->flashError(t('Unable to unlink your Google Account.'));
} }
$this->response->redirect('?controller=user'); $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId());
} }
/** /**
@ -310,23 +449,23 @@ class User extends Base
$code = $this->request->getStringParam('code'); $code = $this->request->getStringParam('code');
if ($code) { if ($code) {
$profile = $this->gitHub->getGitHubProfile($code); $profile = $this->authentication->backend('gitHub')->getGitHubProfile($code);
if (is_array($profile)) { if (is_array($profile)) {
// If the user is already logged, link the account otherwise authenticate // If the user is already logged, link the account otherwise authenticate
if ($this->acl->isLogged()) { if ($this->acl->isLogged()) {
if ($this->gitHub->updateUser($this->acl->getUserId(), $profile)) { if ($this->authentication->backend('gitHub')->updateUser($this->acl->getUserId(), $profile)) {
$this->session->flash(t('Your GitHub account was successfully linked to your profile.')); $this->session->flash(t('Your GitHub account was successfully linked to your profile.'));
} }
else { else {
$this->session->flashError(t('Unable to link your GitHub Account.')); $this->session->flashError(t('Unable to link your GitHub Account.'));
} }
$this->response->redirect('?controller=user'); $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId());
} }
else if ($this->gitHub->authenticate($profile['id'])) { else if ($this->authentication->backend('gitHub')->authenticate($profile['id'])) {
$this->response->redirect('?controller=app'); $this->response->redirect('?controller=app');
} }
else { else {
@ -340,7 +479,7 @@ class User extends Base
} }
} }
$this->response->redirect($this->gitHub->getAuthorizationUrl()); $this->response->redirect($this->authentication->backend('gitHub')->getAuthorizationUrl());
} }
/** /**
@ -352,15 +491,15 @@ class User extends Base
{ {
$this->checkCSRFParam(); $this->checkCSRFParam();
$this->gitHub->revokeGitHubAccess(); $this->authentication->backend('gitHub')->revokeGitHubAccess();
if ($this->gitHub->unlink($this->acl->getUserId())) { if ($this->authentication->backend('gitHub')->unlink($this->acl->getUserId())) {
$this->session->flash(t('Your GitHub account is no longer linked to your profile.')); $this->session->flash(t('Your GitHub account is no longer linked to your profile.'));
} }
else { else {
$this->session->flashError(t('Unable to unlink your GitHub Account.')); $this->session->flashError(t('Unable to unlink your GitHub Account.'));
} }
$this->response->redirect('?controller=user'); $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId());
} }
} }

75
sources/app/Core/Cli.php Normal file
View file

@ -0,0 +1,75 @@
<?php
namespace Core;
use Closure;
/**
* CLI class
*
* @package core
* @author Frederic Guillot
*/
class Cli
{
/**
* Default command name
*
* @access public
* @var string
*/
public $default_command = 'help';
/**
* List of registered commands
*
* @access private
* @var array
*/
private $commands = array();
/**
*
*
* @access public
* @param string $command Command name
* @param Closure $callback Command callback
*/
public function register($command, Closure $callback)
{
$this->commands[$command] = $callback;
}
/**
* Execute a command
*
* @access public
* @param string $command Command name
*/
public function call($command)
{
if (isset($this->commands[$command])) {
$this->commands[$command]();
exit;
}
}
/**
* Determine which command to execute
*
* @access public
*/
public function execute()
{
if (php_sapi_name() !== 'cli') {
die('This script work only from the command line.');
}
if ($GLOBALS['argc'] === 1) {
$this->call($this->default_command);
}
$this->call($GLOBALS['argv'][1]);
$this->call($this->default_command);
}
}

View file

@ -10,18 +10,30 @@ namespace Core;
*/ */
class Loader class Loader
{ {
/**
* List of paths
*
* @access private
* @var array
*/
private $paths = array();
/** /**
* Load the missing class * Load the missing class
* *
* @access public * @access public
* @param string $class Class name * @param string $class Class name with namespace
*/ */
public function load($class) public function load($class)
{ {
$filename = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php'; foreach ($this->paths as $path) {
if (file_exists($filename)) { $filename = $path.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php';
require $filename;
if (file_exists($filename)) {
require $filename;
break;
}
} }
} }
@ -34,4 +46,17 @@ class Loader
{ {
spl_autoload_register(array($this, 'load')); spl_autoload_register(array($this, 'load'));
} }
/**
* Register a new path
*
* @access public
* @param string $path Path
* @return Core\Loader
*/
public function setPath($path)
{
$this->paths[] = $path;
return $this;
}
} }

View file

@ -1,6 +1,7 @@
<?php <?php
namespace Core; namespace Core;
use RuntimeException; use RuntimeException;
/** /**

View file

@ -70,6 +70,22 @@ class Response
exit; exit;
} }
/**
* Send a CSV response
*
* @access public
* @param array $data Data to serialize in csv
* @param integer $status_code HTTP status code
*/
public function csv(array $data, $status_code = 200)
{
$this->status($status_code);
$this->nocache();
header('Content-Type: text/csv');
Tool::csv($data);
exit;
}
/** /**
* Send a Json response * Send a Json response
* *
@ -83,7 +99,6 @@ class Response
$this->nocache(); $this->nocache();
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode($data); echo json_encode($data);
exit; exit;
} }
@ -100,7 +115,6 @@ class Response
$this->nocache(); $this->nocache();
header('Content-Type: text/plain; charset=utf-8'); header('Content-Type: text/plain; charset=utf-8');
echo $data; echo $data;
exit; exit;
} }
@ -117,7 +131,6 @@ class Response
$this->nocache(); $this->nocache();
header('Content-Type: text/html; charset=utf-8'); header('Content-Type: text/html; charset=utf-8');
echo $data; echo $data;
exit; exit;
} }
@ -134,7 +147,6 @@ class Response
$this->nocache(); $this->nocache();
header('Content-Type: text/xml; charset=utf-8'); header('Content-Type: text/xml; charset=utf-8');
echo $data; echo $data;
exit; exit;
} }
@ -169,7 +181,6 @@ class Response
header('Content-Transfer-Encoding: binary'); header('Content-Transfer-Encoding: binary');
header('Content-Type: application/octet-stream'); header('Content-Type: application/octet-stream');
echo $data; echo $data;
exit; exit;
} }
@ -235,7 +246,7 @@ class Response
*/ */
public function hsts() public function hsts()
{ {
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') { if (Tool::isHTTPS()) {
header('Strict-Transport-Security: max-age=31536000'); header('Strict-Transport-Security: max-age=31536000');
} }
} }

View file

@ -13,9 +13,11 @@ class Session
/** /**
* Sesion lifetime * Sesion lifetime
* *
* http://php.net/manual/en/session.configuration.php#ini.session.cookie-lifetime
*
* @var integer * @var integer
*/ */
const SESSION_LIFETIME = 7200; // 2 hours const SESSION_LIFETIME = 0; // Until the browser is closed
/** /**
* Open a session * Open a session
@ -35,7 +37,7 @@ class Session
self::SESSION_LIFETIME, self::SESSION_LIFETIME,
$base_path ?: '/', $base_path ?: '/',
null, null,
! empty($_SERVER['HTTPS']), Tool::isHTTPS(),
true true
); );

View file

@ -2,6 +2,8 @@
namespace Core; namespace Core;
use LogicException;
/** /**
* Template class * Template class
* *
@ -25,31 +27,22 @@ class Template
* $template->load('template_name', ['bla' => 'value']); * $template->load('template_name', ['bla' => 'value']);
* *
* @access public * @access public
* @params string $__template_name Template name
* @params array $__template_args Key/Value map of template variables
* @return string * @return string
*/ */
public function load() public function load($__template_name, array $__template_args = array())
{ {
if (func_num_args() < 1 || func_num_args() > 2) { $__template_file = self::PATH.$__template_name.'.php';
die('Invalid template arguments');
if (! file_exists($__template_file)) {
throw new LogicException('Unable to load the template: "'.$__template_name.'"');
} }
if (! file_exists(self::PATH.func_get_arg(0).'.php')) { extract($__template_args);
die('Unable to load the template: "'.func_get_arg(0).'"');
}
if (func_num_args() === 2) {
if (! is_array(func_get_arg(1))) {
die('Template variables must be an array');
}
extract(func_get_arg(1));
}
ob_start(); ob_start();
include $__template_file;
include self::PATH.func_get_arg(0).'.php';
return ob_get_clean(); return ob_get_clean();
} }

67
sources/app/Core/Tool.php Normal file
View file

@ -0,0 +1,67 @@
<?php
namespace Core;
/**
* Tool class
*
* @package core
* @author Frederic Guillot
*/
class Tool
{
/**
* Write a CSV file
*
* @static
* @access public
* @param array $rows Array of rows
* @param string $filename Output filename
*/
public static function csv(array $rows, $filename = 'php://output')
{
$fp = fopen($filename, 'w');
if (is_resource($fp)) {
foreach ($rows as $fields) {
fputcsv($fp, $fields);
}
fclose($fp);
}
}
/**
* Load and register a model
*
* @static
* @access public
* @param Core\Registry $registry DPI container
* @param string $name Model name
* @return mixed
*/
public static function loadModel(Registry $registry, $name)
{
if (! isset($registry->$name)) {
$class = '\Model\\'.ucfirst($name);
$registry->$name = new $class($registry);
}
return $registry->shared($name);
}
/**
* Check if the page is requested through HTTPS
*
* Note: IIS return the value 'off' and other web servers an empty value when it's not HTTPS
*
* @static
* @access public
* @return boolean
*/
public static function isHTTPS()
{
return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off';
}
}

View file

@ -32,7 +32,7 @@ class Translator
* $translator->translate('I have %d kids', 5); * $translator->translate('I have %d kids', 5);
* *
* @access public * @access public
* @param $identifier * @param string $identifier Default string
* @return string * @return string
*/ */
public function translate($identifier) public function translate($identifier)
@ -52,6 +52,28 @@ class Translator
); );
} }
/**
* Get a translation with no HTML escaping
*
* $translator->translateNoEscaping('I have %d kids', 5);
*
* @access public
* @param string $identifier Default string
* @return string
*/
public function translateNoEscaping($identifier)
{
$args = func_get_args();
array_shift($args);
array_unshift($args, $this->get($identifier, $identifier));
return call_user_func_array(
'sprintf',
$args
);
}
/** /**
* Get a formatted number * Get a formatted number
* *
@ -119,7 +141,6 @@ class Translator
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
$format = str_replace('%e', '%d', $format); $format = str_replace('%e', '%d', $format);
$format = str_replace('%G', '%Y', $format);
$format = str_replace('%k', '%H', $format); $format = str_replace('%k', '%H', $format);
} }

View file

@ -0,0 +1,87 @@
<?php
namespace Event;
use Core\Listener;
use Model\Notification;
/**
* Base notification listener
*
* @package event
* @author Frederic Guillot
*/
abstract class BaseNotificationListener implements Listener
{
/**
* Notification model
*
* @accesss protected
* @var Model\Notification
*/
protected $notification;
/**
* Template name
*
* @accesss private
* @var string
*/
private $template = '';
/**
* Fetch data for the mail template
*
* @access public
* @param array $data Event data
* @return array
*/
abstract public function getTemplateData(array $data);
/**
* Constructor
*
* @access public
* @param \Model\Notification $notification Notification model instance
* @param string $template Template name
*/
public function __construct(Notification $notification, $template)
{
$this->template = $template;
$this->notification = $notification;
}
/**
* Return class information
*
* @access public
* @return string
*/
public function __toString()
{
return get_called_class();
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function execute(array $data)
{
$values = $this->getTemplateData($data);
// Get the list of users to be notified
$users = $this->notification->getUsersList($values['task']['project_id']);
// Send notifications
if ($users) {
$this->notification->sendEmails($this->template, $users, $values);
return true;
}
return false;
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace Event;
use Core\Listener;
use Model\CommentHistory;
/**
* Comment history listener
*
* @package event
* @author Frederic Guillot
*/
class CommentHistoryListener implements Listener
{
/**
* Comment History model
*
* @accesss private
* @var \Model\CommentHistory
*/
private $model;
/**
* Constructor
*
* @access public
* @param \Model\CommentHistory $model Comment History model instance
*/
public function __construct(CommentHistory $model)
{
$this->model = $model;
}
/**
* Return class information
*
* @access public
* @return string
*/
public function __toString()
{
return get_called_class();
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function execute(array $data)
{
$creator_id = $this->model->acl->getUserId();
if ($creator_id && isset($data['task_id']) && isset($data['id'])) {
$task = $this->model->task->getById($data['task_id']);
$this->model->create(
$task['project_id'],
$data['task_id'],
$data['id'],
$creator_id,
$this->model->event->getLastTriggeredEvent(),
$data['comment']
);
}
return false;
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Event;
use Event\BaseNotificationListener;
/**
* Comment notification listener
*
* @package event
* @author Frederic Guillot
*/
class CommentNotificationListener extends BaseNotificationListener
{
/**
* Fetch data for the mail template
*
* @access public
* @param array $data Event data
* @return array
*/
public function getTemplateData(array $data)
{
$values = array();
$values['comment'] = $this->notification->comment->getById($data['id']);
$values['task'] = $this->notification->task->getById($values['comment']['task_id'], true);
return $values;
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Event;
use Event\BaseNotificationListener;
/**
* File notification listener
*
* @package event
* @author Frederic Guillot
*/
class FileNotificationListener extends BaseNotificationListener
{
/**
* Fetch data for the mail template
*
* @access public
* @param array $data Event data
* @return array
*/
public function getTemplateData(array $data)
{
$values = array();
$values['file'] = $data;
$values['task'] = $this->notification->task->getById($data['task_id'], true);
return $values;
}
}

View file

@ -6,12 +6,14 @@ use Core\Listener;
use Model\Project; use Model\Project;
/** /**
* Task modification listener * Project modification date listener
* *
* @package events * Update the last modified field for a project
*
* @package event
* @author Frederic Guillot * @author Frederic Guillot
*/ */
class TaskModification implements Listener class ProjectModificationDate implements Listener
{ {
/** /**
* Project model * Project model
@ -32,6 +34,17 @@ class TaskModification implements Listener
$this->project = $project; $this->project = $project;
} }
/**
* Return class information
*
* @access public
* @return string
*/
public function __toString()
{
return get_called_class();
}
/** /**
* Execute the action * Execute the action
* *
@ -42,8 +55,7 @@ class TaskModification implements Listener
public function execute(array $data) public function execute(array $data)
{ {
if (isset($data['project_id'])) { if (isset($data['project_id'])) {
$this->project->updateModificationDate($data['project_id']); return $this->project->updateModificationDate($data['project_id']);
return true;
} }
return false; return false;

View file

@ -0,0 +1,30 @@
<?php
namespace Event;
use Event\BaseNotificationListener;
/**
* SubTask notification listener
*
* @package event
* @author Frederic Guillot
*/
class SubTaskNotificationListener extends BaseNotificationListener
{
/**
* Fetch data for the mail template
*
* @access public
* @param array $data Event data
* @return array
*/
public function getTemplateData(array $data)
{
$values = array();
$values['subtask'] = $this->notification->subtask->getById($data['id'], true);
$values['task'] = $this->notification->task->getById($data['task_id'], true);
return $values;
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace Event;
use Core\Listener;
use Model\SubtaskHistory;
/**
* Subtask history listener
*
* @package event
* @author Frederic Guillot
*/
class SubtaskHistoryListener implements Listener
{
/**
* Comment History model
*
* @accesss private
* @var \Model\SubtaskHistory
*/
private $model;
/**
* Constructor
*
* @access public
* @param \Model\SubtaskHistory $model Subtask History model instance
*/
public function __construct(SubtaskHistory $model)
{
$this->model = $model;
}
/**
* Return class information
*
* @access public
* @return string
*/
public function __toString()
{
return get_called_class();
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function execute(array $data)
{
$creator_id = $this->model->acl->getUserId();
if ($creator_id && isset($data['task_id']) && isset($data['id'])) {
$task = $this->model->task->getById($data['task_id']);
$this->model->create(
$task['project_id'],
$data['task_id'],
$data['id'],
$creator_id,
$this->model->event->getLastTriggeredEvent(),
''
);
}
return false;
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace Event;
use Core\Listener;
use Model\TaskHistory;
/**
* Task history listener
*
* @package event
* @author Frederic Guillot
*/
class TaskHistoryListener implements Listener
{
/**
* Task History model
*
* @accesss private
* @var \Model\TaskHistory
*/
private $model;
/**
* Constructor
*
* @access public
* @param \Model\TaskHistory $model Task History model instance
*/
public function __construct(TaskHistory $model)
{
$this->model = $model;
}
/**
* Return class information
*
* @access public
* @return string
*/
public function __toString()
{
return get_called_class();
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function execute(array $data)
{
$creator_id = $this->model->acl->getUserId();
if ($creator_id && isset($data['task_id']) && isset($data['project_id'])) {
$this->model->create($data['project_id'], $data['task_id'], $creator_id, $this->model->event->getLastTriggeredEvent());
}
return false;
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Event;
use Event\BaseNotificationListener;
/**
* Task notification listener
*
* @package event
* @author Frederic Guillot
*/
class TaskNotificationListener extends BaseNotificationListener
{
/**
* Fetch data for the mail template
*
* @access public
* @param array $data Event data
* @return array
*/
public function getTemplateData(array $data)
{
$values = array();
$values['task'] = $this->notification->task->getById($data['task_id'], true);
return $values;
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace Event;
use Core\Listener;
use Model\Webhook;
/**
* Webhook task events
*
* @package event
* @author Frederic Guillot
*/
class WebhookListener implements Listener
{
/**
* Webhook model
*
* @accesss private
* @var \Model\Webhook
*/
private $webhook;
/**
* Url to call
*
* @access private
* @var string
*/
private $url = '';
/**
* Constructor
*
* @access public
* @param string $url URL to call
* @param \Model\Webhook $webhook Webhook model instance
*/
public function __construct($url, Webhook $webhook)
{
$this->url = $url;
$this->webhook = $webhook;
}
/**
* Return class information
*
* @access public
* @return string
*/
public function __toString()
{
return get_called_class();
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function execute(array $data)
{
$this->webhook->notify($this->url, $data);
return true;
}
}

View file

@ -1,14 +1,7 @@
<?php <?php
return array( return array(
'English' => 'Englisch', 'None' => 'Keines',
'German' => 'Deutsch',
'French' => 'Französisch',
'Polish' => 'Polnisch',
'Portuguese (Brazilian)' => 'Portugiesisch (Brasilien)',
'Spanish' => 'Spanisch',
'Chinese (Simplified)' => 'Chinesisch (vereinfacht)',
'None' => 'Kein',
'edit' => 'Bearbeiten', 'edit' => 'Bearbeiten',
'Edit' => 'Bearbeiten', 'Edit' => 'Bearbeiten',
'remove' => 'Entfernen', 'remove' => 'Entfernen',
@ -31,12 +24,12 @@ return array(
'Unassigned' => 'Nicht zugeordnet', 'Unassigned' => 'Nicht zugeordnet',
'View this task' => 'Aufgabe ansehen', 'View this task' => 'Aufgabe ansehen',
'Remove user' => 'Benutzer löschen', 'Remove user' => 'Benutzer löschen',
'Do you really want to remove this user: "%s"?' => 'Soll dieser Benutzer wirklich gelöscht werden: «%s»?', 'Do you really want to remove this user: "%s"?' => 'Soll dieser Benutzer wirklich gelöscht werden: "%s"?',
'New user' => 'Neuer Benutzer', 'New user' => 'Neuer Benutzer',
'All users' => 'Alle Benutzer', 'All users' => 'Alle Benutzer',
'Username' => 'Benutzername', 'Username' => 'Benutzername',
'Password' => 'Passwort', 'Password' => 'Passwort',
'Default Project' => 'Standardprojekt', 'Default project' => 'Standardprojekt',
'Administrator' => 'Administrator', 'Administrator' => 'Administrator',
'Sign in' => 'Anmelden', 'Sign in' => 'Anmelden',
'Users' => 'Benutzer', 'Users' => 'Benutzer',
@ -57,35 +50,35 @@ return array(
'Project' => 'Projekt', 'Project' => 'Projekt',
'Status' => 'Status', 'Status' => 'Status',
'Tasks' => 'Aufgabe', 'Tasks' => 'Aufgabe',
'Board' => 'Pinwand', 'Board' => 'Pinnwand',
'Actions' => 'Aktionen', 'Actions' => 'Aktionen',
'Inactive' => 'Inaktiv', 'Inactive' => 'Inaktiv',
'Active' => 'Aktiv', 'Active' => 'Aktiv',
'Column %d' => 'Spalte %d', 'Column %d' => 'Spalte %d',
'Add this column' => 'Diese Spalte hinzufügen', 'Add this column' => 'Diese Spalte hinzufügen',
'%d tasks on the board' => '%d Aufgaben auf dieser Pinwand', '%d tasks on the board' => '%d Aufgaben auf dieser Pinnwand',
'%d tasks in total' => '%d Aufgaben gesamt', '%d tasks in total' => '%d Aufgaben gesamt',
'Unable to update this board.' => 'Ändern dieser Pinwand nicht möglich.', 'Unable to update this board.' => 'Ändern dieser Pinnwand nicht möglich.',
'Edit board' => 'Pinwand bearbeiten', 'Edit board' => 'Pinnwand bearbeiten',
'Disable' => 'Deaktivieren', 'Disable' => 'Deaktivieren',
'Enable' => 'Aktivieren', 'Enable' => 'Aktivieren',
'New project' => 'Neues Projekt', 'New project' => 'Neues Projekt',
'Do you really want to remove this project: "%s"?' => 'Soll dieses Projekt wirklich gelöscht werden: «%s»?', 'Do you really want to remove this project: "%s"?' => 'Soll dieses Projekt wirklich gelöscht werden: "%s"?',
'Remove project' => 'Projekt löschen', 'Remove project' => 'Projekt löschen',
'Boards' => 'Pinwände', 'Boards' => 'Pinnwände',
'Edit the board for "%s"' => 'Pinwand für «%s» bearbeiten', 'Edit the board for "%s"' => 'Pinnwand für "%s" bearbeiten',
'All projects' => 'Alle Projekte', 'All projects' => 'Alle Projekte',
'Change columns' => 'Spalten ändern', 'Change columns' => 'Spalten ändern',
'Add a new column' => 'Neue Spalte hinzufügen', 'Add a new column' => 'Neue Spalte hinzufügen',
'Title' => 'Titel', 'Title' => 'Titel',
'Add Column' => 'Neue Spalte', 'Add Column' => 'Neue Spalte',
'Project "%s"' => 'Projekt «%s»', 'Project "%s"' => 'Projekt "%s"',
'Nobody assigned' => 'Nicht zugeordnet', 'Nobody assigned' => 'Nicht zugeordnet',
'Assigned to %s' => 'Zuständiger: %s', 'Assigned to %s' => 'Zuständig: %s',
'Remove a column' => 'Spalte löschen', 'Remove a column' => 'Spalte löschen',
'Remove a column from a board' => 'Spalte einer Pinwand löschen', 'Remove a column from a board' => 'Spalte einer Pinnwand löschen',
'Unable to remove this column.' => 'Löschen dieser Spalte nicht möglich.', 'Unable to remove this column.' => 'Löschen dieser Spalte nicht möglich.',
'Do you really want to remove this column: "%s"?' => 'Soll diese Spalte wirklich gelöscht werden: «%s»?', 'Do you really want to remove this column: "%s"?' => 'Soll diese Spalte wirklich gelöscht werden: "%s"?',
'This action will REMOVE ALL TASKS associated to this column!' => 'ALLE AUFGABEN dieser Spalte werden GELÖSCHT!', 'This action will REMOVE ALL TASKS associated to this column!' => 'ALLE AUFGABEN dieser Spalte werden GELÖSCHT!',
'Settings' => 'Einstellungen', 'Settings' => 'Einstellungen',
'Application settings' => 'Anwendungskonfiguration', 'Application settings' => 'Anwendungskonfiguration',
@ -94,25 +87,25 @@ return array(
'API token:' => 'API Token:', 'API token:' => 'API Token:',
'More information' => 'Mehr Informationen', 'More information' => 'Mehr Informationen',
'Database size:' => 'Datenbankgröße:', 'Database size:' => 'Datenbankgröße:',
'Download the database' => 'Download der Datenbank', 'Download the database' => 'Datenbank herunterladen',
'Optimize the database' => 'Optimieren der Datenbank', 'Optimize the database' => 'Datenbank optimieren',
'(VACUUM command)' => '(VACUUM Kommando)', '(VACUUM command)' => '(VACUUM Befehl)',
'(Gzip compressed Sqlite file)' => '(Gzip komprimierte Sqlite Datei)', '(Gzip compressed Sqlite file)' => '(Gzip-komprimierte Sqlite Datei)',
'User settings' => 'Benutzereinstellungen', 'User settings' => 'Benutzereinstellungen',
'My default project:' => 'Standardprojekt:', 'My default project:' => 'Standardprojekt:',
'Close a task' => 'Aufgabe abschließen', 'Close a task' => 'Aufgabe abschließen',
'Do you really want to close this task: "%s"?' => 'Soll diese Aufgabe wirklich abgeschlossen werden: «%s»?', 'Do you really want to close this task: "%s"?' => 'Soll diese Aufgabe wirklich abgeschlossen werden: "%s"?',
'Edit a task' => 'Aufgabe bearbeiten', 'Edit a task' => 'Aufgabe bearbeiten',
'Column' => 'Spalte', 'Column' => 'Spalte',
'Color' => 'Farbe', 'Color' => 'Farbe',
'Assignee' => 'Zuständiger', 'Assignee' => 'Zuständig',
'Create another task' => 'Weitere Aufgabe erstellen', 'Create another task' => 'Weitere Aufgabe erstellen',
'New task' => 'Neue Aufgabe', 'New task' => 'Neue Aufgabe',
'Open a task' => 'Öffne eine Aufgabe', 'Open a task' => 'Öffne eine Aufgabe',
'Do you really want to open this task: "%s"?' => 'Soll diese Aufgabe wirklich wieder geöffnet werden: «%s»?', 'Do you really want to open this task: "%s"?' => 'Soll diese Aufgabe wirklich wieder geöffnet werden: "%s"?',
'Back to the board' => 'Zurück zur Pinwand', 'Back to the board' => 'Zurück zur Pinnwand',
'Created on %B %e, %G at %k:%M %p' => 'Erstellt am %d.%m.%Y um %H:%M', 'Created on %B %e, %Y at %k:%M %p' => 'Erstellt am %d.%m.%Y um %H:%M',
'There is nobody assigned' => 'Die Aufgabe wurde niemand zugewiesen', 'There is nobody assigned' => 'Die Aufgabe wurde niemandem zugewiesen',
'Column on the board:' => 'Spalte:', 'Column on the board:' => 'Spalte:',
'Status is open' => 'Status ist geöffnet', 'Status is open' => 'Status ist geöffnet',
'Status is closed' => 'Status ist abgeschlossen', 'Status is closed' => 'Status ist abgeschlossen',
@ -120,14 +113,14 @@ return array(
'Open this task' => 'Aufgabe wieder öffnen', 'Open this task' => 'Aufgabe wieder öffnen',
'There is no description.' => 'Keine Beschreibung vorhanden.', 'There is no description.' => 'Keine Beschreibung vorhanden.',
'Add a new task' => 'Neue Aufgabe hinzufügen', 'Add a new task' => 'Neue Aufgabe hinzufügen',
'The username is required' => 'Der Benutzername ist obligatorisch', 'The username is required' => 'Der Benutzername wird benötigt',
'The maximum length is %d characters' => 'Die maximale Länge sind %d Zeichen', 'The maximum length is %d characters' => 'Die maximale Länge beträgt %d Zeichen',
'The minimum length is %d characters' => 'Die minimale Länge sind %d Zeichen', 'The minimum length is %d characters' => 'Die minimale Länge beträgt %d Zeichen',
'The password is required' => 'Das Passwort ist obligatorisch', 'The password is required' => 'Das Passwort wird benötigt',
'This value must be an integer' => 'Dieser Wert muss eine Ganzzahl sein', 'This value must be an integer' => 'Dieser Wert muss eine ganze Zahl sein',
'The username must be unique' => 'Der Benutzername muss eindeutig sein', 'The username must be unique' => 'Der Benutzername muss eindeutig sein',
'The username must be alphanumeric' => 'Der Benutzername muss alphanumerisch sein', 'The username must be alphanumeric' => 'Der Benutzername muss alphanumerisch sein',
'The user id is required' => 'Die Benutzer ID ist obligatorisch', 'The user id is required' => 'Die Benutzer ID ist anzugeben',
'Passwords don\'t match' => 'Passwörter nicht gleich', 'Passwords don\'t match' => 'Passwörter nicht gleich',
'The confirmation is required' => 'Die Bestätigung ist erforderlich', 'The confirmation is required' => 'Die Bestätigung ist erforderlich',
'The column is required' => 'Die Spalte ist anzugeben', 'The column is required' => 'Die Spalte ist anzugeben',
@ -136,10 +129,10 @@ return array(
'The id is required' => 'Die ID ist anzugeben', 'The id is required' => 'Die ID ist anzugeben',
'The project id is required' => 'Die Projekt ID ist anzugeben', 'The project id is required' => 'Die Projekt ID ist anzugeben',
'The project name is required' => 'Der Projektname ist anzugeben', 'The project name is required' => 'Der Projektname ist anzugeben',
'This project must be unique' => 'Der Projektname muss eindeutig sein', 'This project must be unique' => 'Der Projektname muss eindeutig sein',
'The title is required' => 'Der Titel ist anzugeben', 'The title is required' => 'Der Titel ist anzugeben',
'The language is required' => 'Die Sprache ist erforderlich', 'The language is required' => 'Die Sprache ist anzugeben',
'There is no active project, the first step is to create a new project.' => 'Es gibt kein aktives Projekt. Der erste Schritt ist ein Projekt zu erstellen.', 'There is no active project, the first step is to create a new project.' => 'Es gibt kein aktives Projekt. Zunächst muss ein Projekt erstellt werden.',
'Settings saved successfully.' => 'Einstellungen erfolgreich gespeichert.', 'Settings saved successfully.' => 'Einstellungen erfolgreich gespeichert.',
'Unable to save your settings.' => 'Speichern der Einstellungen nicht möglich.', 'Unable to save your settings.' => 'Speichern der Einstellungen nicht möglich.',
'Database optimization done.' => 'Optimieren der Datenbank abgeschlossen.', 'Database optimization done.' => 'Optimieren der Datenbank abgeschlossen.',
@ -153,10 +146,10 @@ return array(
'Unable to activate this project.' => 'Aktivieren des Projekts nicht möglich.', 'Unable to activate this project.' => 'Aktivieren des Projekts nicht möglich.',
'Project disabled successfully.' => 'Projekt erfolgreich deaktiviert.', 'Project disabled successfully.' => 'Projekt erfolgreich deaktiviert.',
'Unable to disable this project.' => 'Deaktivieren des Projekts nicht möglich.', 'Unable to disable this project.' => 'Deaktivieren des Projekts nicht möglich.',
'Unable to open this task.' => 'Wieder eröffnen der Aufgabe nicht möglich.', 'Unable to open this task.' => 'Wiedereröffnung der Aufgabe nicht möglich.',
'Task opened successfully.' => 'Aufgabe erfolgreich wieder eröffnet.', 'Task opened successfully.' => 'Aufgabe erfolgreich wieder eröffnet.',
'Unable to close this task.' => 'Abschließen der Aufgabe nicht möglich.', 'Unable to close this task.' => 'Abschließen der Aufgabe nicht möglich.',
'Task closed successfully.' => 'Aufgabe erfolgreich geschlossen.', 'Task closed successfully.' => 'Aufgabe erfolgreich abgeschlossen.',
'Unable to update your task.' => 'Aktualisieren der Aufgabe nicht möglich.', 'Unable to update your task.' => 'Aktualisieren der Aufgabe nicht möglich.',
'Task updated successfully.' => 'Aufgabe erfolgreich aktualisiert.', 'Task updated successfully.' => 'Aufgabe erfolgreich aktualisiert.',
'Unable to create your task.' => 'Erstellen der Aufgabe nicht möglich.', 'Unable to create your task.' => 'Erstellen der Aufgabe nicht möglich.',
@ -167,42 +160,41 @@ return array(
'Unable to update your user.' => 'Änderung des Benutzers nicht möglich.', 'Unable to update your user.' => 'Änderung des Benutzers nicht möglich.',
'User removed successfully.' => 'Benutzer erfolgreich gelöscht.', 'User removed successfully.' => 'Benutzer erfolgreich gelöscht.',
'Unable to remove this user.' => 'Löschen des Benutzers nicht möglich.', 'Unable to remove this user.' => 'Löschen des Benutzers nicht möglich.',
'Board updated successfully.' => 'Pinwand erfolgreich geändert.', 'Board updated successfully.' => 'Pinnwand erfolgreich geändert.',
'Ready' => 'Bereit', 'Ready' => 'Bereit',
'Backlog' => 'Ideen', 'Backlog' => 'Ideen',
'Work in progress' => 'In Arbeit', 'Work in progress' => 'In Arbeit',
'Done' => 'Erledigt', 'Done' => 'Erledigt',
'Application version:' => 'Version:', 'Application version:' => 'Version:',
'Completed on %B %e, %G at %k:%M %p' => 'Abgeschlossen am %d.%m.%Y um %H:%M', 'Completed on %B %e, %Y at %k:%M %p' => 'Abgeschlossen am %d.%m.%Y um %H:%M',
'%B %e, %G at %k:%M %p' => '%d.%m.%Y um %H:%M', '%B %e, %Y at %k:%M %p' => '%d.%m.%Y um %H:%M',
'Date created' => 'Erstellt am', 'Date created' => 'Erstellt am',
'Date completed' => 'Abgeschlossen am', 'Date completed' => 'Abgeschlossen am',
'Id' => 'ID', 'Id' => 'ID',
'No task' => 'Keine Aufgabe', 'No task' => 'Keine Aufgabe',
'Completed tasks' => 'Abgeschlossene Aufgaben', 'Completed tasks' => 'Abgeschlossene Aufgaben',
'List of projects' => 'Liste der Projekte', 'List of projects' => 'Liste der Projekte',
'Completed tasks for "%s"' => 'Abgeschlossene Aufgaben für «%s»', 'Completed tasks for "%s"' => 'Abgeschlossene Aufgaben für "%s"',
'%d closed tasks' => '%d abgeschlossene Aufgaben', '%d closed tasks' => '%d abgeschlossene Aufgaben',
'no task for this project' => 'Keine Aufgaben in diesem Projekt', 'No task for this project' => 'Keine Aufgaben in diesem Projekt',
'Public link' => 'Öffentlicher Link', 'Public link' => 'Öffentlicher Link',
'There is no column in your project!' => 'Es gibt keine Spalte in deinem Projekt!', 'There is no column in your project!' => 'Es gibt keine Spalte in deinem Projekt!',
'Change assignee' => 'Zuständigkeit ändern', 'Change assignee' => 'Zuständigkeit ändern',
'Change assignee for the task "%s"' => 'Zuständigkeit für diese Aufgabe ändern: «%s»', 'Change assignee for the task "%s"' => 'Zuständigkeit für diese Aufgabe ändern: "%s"',
'Timezone' => 'Zeitzone', 'Timezone' => 'Zeitzone',
'Sorry, I didn\'t found this information in my database!' => 'Diese Information wurde in der Datenbank nicht gefunden!', 'Sorry, I didn\'t found this information in my database!' => 'Diese Information wurde in der Datenbank nicht gefunden!',
'Page not found' => 'Seite nicht gefunden', 'Page not found' => 'Seite nicht gefunden',
'Story Points' => 'Aufwand (Story Points)', // 'Complexity' => '',
'limit' => 'Limit', 'limit' => 'Limit',
'Task limit' => 'Maximale Anzahl von Aufgaben', 'Task limit' => 'Maximale Anzahl von Aufgaben',
'This value must be greater than %d' => 'Dieser Wert muss größer sein als %d', 'This value must be greater than %d' => 'Dieser Wert muss größer sein als %d',
'Edit project access list' => 'Zugriffsberechtigungen des Projektes bearbeiten', 'Edit project access list' => 'Zugriffsberechtigungen des Projektes bearbeiten',
'Edit users access' => 'Benutzerzugriff', 'Edit users access' => 'Benutzerzugriff ändern',
'Allow this user' => 'Diesen Benutzer authorisieren', 'Allow this user' => 'Diesen Benutzer autorisieren',
'Project access list for "%s"' => 'Zugriffsliste für Projekt «%s»',
'Only those users have access to this project:' => 'Nur diese Benutzer haben Zugang zum Projekt:', 'Only those users have access to this project:' => 'Nur diese Benutzer haben Zugang zum Projekt:',
'Don\'t forget that administrators have access to everything.' => 'Nicht vergessen: Administratoren haben überall Zugang.', 'Don\'t forget that administrators have access to everything.' => 'Nicht vergessen: Administratoren haben überall Zugang.',
'revoke' => 'entfernen', 'revoke' => 'entfernen',
'List of authorized users' => 'Liste der authorisierten Benutzer', 'List of authorized users' => 'Liste der autorisierten Benutzer',
'User' => 'Benutzer', 'User' => 'Benutzer',
'Everybody have access to this project.' => 'Jeder hat Zugang zu diesem Projekt.', 'Everybody have access to this project.' => 'Jeder hat Zugang zu diesem Projekt.',
'You are not allowed to access to this project.' => 'Unzureichende Zugriffsrechte zu diesem Projekt.', 'You are not allowed to access to this project.' => 'Unzureichende Zugriffsrechte zu diesem Projekt.',
@ -217,18 +209,18 @@ return array(
'The description is required' => 'Eine Beschreibung wird benötigt', 'The description is required' => 'Eine Beschreibung wird benötigt',
'Edit this task' => 'Aufgabe bearbeiten', 'Edit this task' => 'Aufgabe bearbeiten',
'Due Date' => 'Fällig am', 'Due Date' => 'Fällig am',
'm/d/Y' => 'd.m.Y', // Date format parsed with php 'm/d/Y' => 'd.m.Y',
'month/day/year' => 'TT.MM.JJJJ', // Help shown to the user 'month/day/year' => 'TT.MM.JJJJ',
'Invalid date' => 'Ungültiges Datum', 'Invalid date' => 'Ungültiges Datum',
'Must be done before %B %e, %G' => 'Muss vor dem %d.%m.%Y erledigt werden', 'Must be done before %B %e, %Y' => 'Muss vor dem %d.%m.%Y erledigt werden',
'%B %e, %G' => '%d.%m.%Y', '%B %e, %Y' => '%d.%m.%Y',
'Automatic actions' => 'Automatische Aktionen', 'Automatic actions' => 'Automatische Aktionen',
'Your automatic action have been created successfully.' => 'Die Automatische Aktion wurde erfolgreich erstellt.', 'Your automatic action have been created successfully.' => 'Die Automatische Aktion wurde erfolgreich erstellt.',
'Unable to create your automatic action.' => 'Automatische Aktion konnte nicht erstellt werden.', 'Unable to create your automatic action.' => 'Erstellen der automatischen Aktion nicht möglich.',
'Remove an action' => 'Aktion löschen', 'Remove an action' => 'Aktion löschen',
'Unable to remove this action.' => 'Aktion konnte nicht gelöscht werden', 'Unable to remove this action.' => 'Löschen der Aktion nicht möglich.',
'Action removed successfully.' => 'Aktion erfolgreich gelöscht.', 'Action removed successfully.' => 'Aktion erfolgreich gelöscht.',
'Automatic actions for the project "%s"' => 'Automatische Aktionen für das Projekt «%s»', 'Automatic actions for the project "%s"' => 'Automatische Aktionen für das Projekt "%s"',
'Defined actions' => 'Definierte Aktionen', 'Defined actions' => 'Definierte Aktionen',
'Add an action' => 'Aktion hinzufügen', 'Add an action' => 'Aktion hinzufügen',
'Event name' => 'Ereignis', 'Event name' => 'Ereignis',
@ -240,17 +232,17 @@ return array(
'Next step' => 'Weiter', 'Next step' => 'Weiter',
'Define action parameters' => 'Aktionsparameter definieren', 'Define action parameters' => 'Aktionsparameter definieren',
'Save this action' => 'Aktion speichern', 'Save this action' => 'Aktion speichern',
'Do you really want to remove this action: "%s"?' => 'Soll diese Aktion wirklich gelöscht werden: «%s»?', 'Do you really want to remove this action: "%s"?' => 'Soll diese Aktion wirklich gelöscht werden: "%s"?',
'Remove an automatic action' => 'Löschen einer automatischen Aktion', 'Remove an automatic action' => 'Löschen einer automatischen Aktion',
'Close the task' => 'Aufgabe abschließen', 'Close the task' => 'Aufgabe abschließen',
'Assign the task to a specific user' => 'Aufgabe einem Benutzer zuordnen', 'Assign the task to a specific user' => 'Aufgabe einem Benutzer zuordnen',
'Assign the task to the person who does the action' => 'Aufgabe dem Benutzer zuordnen, der die Aktion ausgeführt hat', 'Assign the task to the person who does the action' => 'Aufgabe dem Benutzer zuordnen, der die Aktion ausgeführt hat',
'Duplicate the task to another project' => 'Aufgabe in ein anderes Projekt kopieren', 'Duplicate the task to another project' => 'Aufgabe in ein anderes Projekt kopieren',
'Move a task to another column' => 'Aufgabe in andere Spalte verschoben', 'Move a task to another column' => 'Aufgabe in andere Spalte verschieben',
'Move a task to another position in the same column' => 'Aufgabe an andere Position in der gleichen Spalte verschoben', 'Move a task to another position in the same column' => 'Aufgabe an andere Position in der gleichen Spalte verschieben',
'Task modification' => 'Änderung einer Aufgabe', 'Task modification' => 'Aufgabe ändern',
'Task creation' => 'Erstellung einer Aufgabe', 'Task creation' => 'Aufgabe erstellen',
'Open a closed task' => 'Abgeschlossenen Aufgabe wieder eröffnen', 'Open a closed task' => 'Abgeschlossene Aufgabe wieder eröffnen',
'Closing a task' => 'Aufgabe abschließen', 'Closing a task' => 'Aufgabe abschließen',
'Assign a color to a specific user' => 'Einem Benutzer eine Farbe zuordnen', 'Assign a color to a specific user' => 'Einem Benutzer eine Farbe zuordnen',
'Column title' => 'Spaltentitel', 'Column title' => 'Spaltentitel',
@ -262,26 +254,26 @@ return array(
'link' => 'Link', 'link' => 'Link',
'Update this comment' => 'Kommentar aktualisieren', 'Update this comment' => 'Kommentar aktualisieren',
'Comment updated successfully.' => 'Kommentar erfolgreich aktualisiert.', 'Comment updated successfully.' => 'Kommentar erfolgreich aktualisiert.',
'Unable to update your comment.' => 'Kommentar konnte nicht aktualisiert werden.', 'Unable to update your comment.' => 'Aktualisierung des Kommentars nicht möglich.',
'Remove a comment' => 'Kommentar löschen', 'Remove a comment' => 'Kommentar löschen',
'Comment removed successfully.' => 'Kommentar erfolgreich gelöscht.', 'Comment removed successfully.' => 'Kommentar erfolgreich gelöscht.',
'Unable to remove this comment.' => 'Kommentar konnte nicht gelöscht werden.', 'Unable to remove this comment.' => 'Löschen des Kommentars nicht möglich.',
'Do you really want to remove this comment?' => 'Soll dieser Kommentar wirklich gelöscht werden?', 'Do you really want to remove this comment?' => 'Soll dieser Kommentar wirklich gelöscht werden?',
'Only administrators or the creator of the comment can access to this page.' => 'Nur Administratoren und der Ersteller des Kommentars könne diese Seite verwenden.', 'Only administrators or the creator of the comment can access to this page.' => 'Nur Administratoren und der Ersteller des Kommentars haben Zugriff auf diese Seite.',
'Details' => 'Details', 'Details' => 'Details',
'Current password for the user "%s"' => 'Aktuelles Passwort für den Benutzer «%s»', 'Current password for the user "%s"' => 'Aktuelles Passwort für den Benutzer "%s"',
'The current password is required' => 'Das aktuelle Passwort wird benötigt', 'The current password is required' => 'Das aktuelle Passwort wird benötigt',
'Wrong password' => 'Falsches Passwort', 'Wrong password' => 'Falsches Passwort',
'Reset all tokens' => 'Alle Tokens zurücksetzten', 'Reset all tokens' => 'Alle Tokens zurücksetzen',
'All tokens have been regenerated.' => 'Alle Tokens wurden zurückgesetzt.', 'All tokens have been regenerated.' => 'Alle Tokens wurden zurückgesetzt.',
'Unknown' => 'Unbekannt', 'Unknown' => 'Unbekannt',
'Last logins' => 'Letzte Anmeldungen', 'Last logins' => 'Letzte Anmeldungen',
'Login date' => 'Anmeldedatum', 'Login date' => 'Anmeldedatum',
'Authentication method' => 'Anmeldemethode', 'Authentication method' => 'Authentisierungsmethode',
'IP address' => 'IP Adresse', 'IP address' => 'IP Adresse',
'User agent' => 'User Agent', 'User agent' => 'User Agent',
'Persistent connections' => 'Bestehende Verbindungen', 'Persistent connections' => 'Bestehende Verbindungen',
'No session' => 'Keine Session', 'No session.' => 'Keine Sitzung.',
'Expiration date' => 'Ablaufdatum', 'Expiration date' => 'Ablaufdatum',
'Remember Me' => 'Angemeldet bleiben', 'Remember Me' => 'Angemeldet bleiben',
'Creation date' => 'Erstellungsdatum', 'Creation date' => 'Erstellungsdatum',
@ -292,31 +284,31 @@ return array(
'Closed' => 'Abgeschlossen', 'Closed' => 'Abgeschlossen',
'Search' => 'Suchen', 'Search' => 'Suchen',
'Nothing found.' => 'Nichts gefunden.', 'Nothing found.' => 'Nichts gefunden.',
'Search in the project "%s"' => 'Suche in Projekt «%s»', 'Search in the project "%s"' => 'Suche in Projekt "%s"',
'Due date' => 'Fälligkeitsdatum', 'Due date' => 'Fälligkeitsdatum',
'Others formats accepted: %s and %s' => 'Andere akzeptierte Formate: %s und %s', 'Others formats accepted: %s and %s' => 'Andere akzeptierte Formate: %s und %s',
'Description' => 'Beschreibung', 'Description' => 'Beschreibung',
'%d comments' => '%d Kommentare', '%d comments' => '%d Kommentare',
'%d comment' => '%d Kommentar', '%d comment' => '%d Kommentar',
'Email address invalid' => 'Ungültige Email-Adresse', 'Email address invalid' => 'Ungültige E-Mail-Adresse',
'Your Google Account is not linked anymore to your profile.' => 'Google Account nicht mehr mit dem Profil verbunden.', 'Your Google Account is not linked anymore to your profile.' => 'Google Account nicht mehr mit dem Profil verbunden.',
'Unable to unlink your Google Account.' => 'Trennung der Verbindung zum Google Account nicht möglich.', 'Unable to unlink your Google Account.' => 'Trennung der Verbindung zum Google Account nicht möglich.',
'Google authentication failed' => 'Zugang mit Google fehl geschlagen', 'Google authentication failed' => 'Zugang mit Google fehl geschlagen',
'Unable to link your Google Account.' => 'Verbindung mit diesem Google Account nicht möglich.', 'Unable to link your Google Account.' => 'Verbindung mit diesem Google Account nicht möglich.',
'Your Google Account is linked to your profile successfully.' => 'Der Google Account wurde erfolgreich verbunden.', 'Your Google Account is linked to your profile successfully.' => 'Der Google Account wurde erfolgreich verbunden.',
'Email' => 'Email', 'Email' => 'E-Mail',
'Link my Google Account' => 'Verbinde meinen Google Account', 'Link my Google Account' => 'Verbinde meinen Google Account',
'Unlink my Google Account' => 'Verbindung mit meinem Google Account trennen', 'Unlink my Google Account' => 'Verbindung mit meinem Google Account trennen',
'Login with my Google Account' => 'Anmelden mit meinem Google Account', 'Login with my Google Account' => 'Anmelden mit meinem Google Account',
'Project not found.' => 'Das Projekt wurde nicht gefunden.', 'Project not found.' => 'Das Projekt wurde nicht gefunden.',
'Task #%d' => 'Aufgabe #%d', 'Task #%d' => 'Aufgabe Nr. %d',
'Task removed successfully.' => 'Aufgabe erfolgreich gelöscht.', 'Task removed successfully.' => 'Aufgabe erfolgreich gelöscht.',
'Unable to remove this task.' => 'Löschen der Aufgabe nicht möglich.', 'Unable to remove this task.' => 'Löschen der Aufgabe nicht möglich.',
'Remove a task' => 'Aufgabe löschen', 'Remove a task' => 'Aufgabe löschen',
'Do you really want to remove this task: "%s"?' => 'Soll diese Aufgabe wirklich gelöscht werden: «%s»?', 'Do you really want to remove this task: "%s"?' => 'Soll diese Aufgabe wirklich gelöscht werden: "%s"?',
'Assign automatically a color based on a category' => 'Automatisch eine Farbe anhand der Kategorie vergeben', 'Assign automatically a color based on a category' => 'Automatisch eine Farbe anhand der Kategorie zuweisen',
'Assign automatically a category based on a color' => 'Automatisch eine Kategorie anhand der Farbe vergeben', 'Assign automatically a category based on a color' => 'Automatisch eine Kategorie anhand der Farbe zuweisen',
'Task creation or modification' => 'Erstellung oder Änderung einer Aufgabe', 'Task creation or modification' => 'Aufgabe erstellen oder ändern',
'Category' => 'Kategorie', 'Category' => 'Kategorie',
'Category:' => 'Kategorie:', 'Category:' => 'Kategorie:',
'Categories' => 'Kategorien', 'Categories' => 'Kategorien',
@ -328,20 +320,20 @@ return array(
'Remove a category' => 'Kategorie löschen', 'Remove a category' => 'Kategorie löschen',
'Category removed successfully.' => 'Kategorie erfolgreich gelöscht.', 'Category removed successfully.' => 'Kategorie erfolgreich gelöscht.',
'Unable to remove this category.' => 'Löschen der Kategorie nicht möglich.', 'Unable to remove this category.' => 'Löschen der Kategorie nicht möglich.',
'Category modification for the project "%s"' => 'Kategorie für das Projekt «%s» bearbeiten', 'Category modification for the project "%s"' => 'Kategorie für das Projekt "%s" bearbeiten',
'Category Name' => 'Kategoriename', 'Category Name' => 'Kategoriename',
'Categories for the project "%s"' => 'Kategorien des Projektes «%s»', 'Categories for the project "%s"' => 'Kategorien des Projektes "%s"',
'Add a new category' => 'Neue Kategorie', 'Add a new category' => 'Neue Kategorie',
'Do you really want to remove this category: "%s"?' => 'Soll diese Kategorie wirklich gelöscht werden: «%s»?', 'Do you really want to remove this category: "%s"?' => 'Soll diese Kategorie wirklich gelöscht werden: "%s"?',
'Filter by category' => 'Kategorie filtern', 'Filter by category' => 'Kategorie filtern',
'All categories' => 'Alle Kategorien', 'All categories' => 'Alle Kategorien',
'No category' => 'keine Kategorie', 'No category' => 'Keine Kategorie',
'The name is required' => 'Der Name ist erforderlich', 'The name is required' => 'Der Name ist erforderlich',
'Remove a file' => 'Datei löschen', 'Remove a file' => 'Datei löschen',
'Unable to remove this file.' => 'Löschen der Datei nicht möglich.', 'Unable to remove this file.' => 'Löschen der Datei nicht möglich.',
'File removed successfully.' => 'Datei erfolgreich gelöscht.', 'File removed successfully.' => 'Datei erfolgreich gelöscht.',
'Attach a document' => 'Datei anhängen', 'Attach a document' => 'Datei anhängen',
'Do you really want to remove this file: "%s"?' => 'Soll diese Datei wirklich gelöscht werden: «%s»?', 'Do you really want to remove this file: "%s"?' => 'Soll diese Datei wirklich gelöscht werden: "%s"?',
'open' => 'öffnen', 'open' => 'öffnen',
'Attachments' => 'Anhänge', 'Attachments' => 'Anhänge',
'Edit the task' => 'Aufgabe bearbeiten', 'Edit the task' => 'Aufgabe bearbeiten',
@ -352,7 +344,7 @@ return array(
'Time tracking' => 'Zeiterfassung', 'Time tracking' => 'Zeiterfassung',
'Estimate:' => 'Geschätzt:', 'Estimate:' => 'Geschätzt:',
'Spent:' => 'Aufgewendet:', 'Spent:' => 'Aufgewendet:',
'Do you really want to remove this sub-task?' => 'Soll diese Unteraufgabe wirklich gelöscht werden: «%s»?', 'Do you really want to remove this sub-task?' => 'Soll diese Unteraufgabe wirklich gelöscht werden: "%s"?',
'Remaining:' => 'Verbleibend:', 'Remaining:' => 'Verbleibend:',
'hours' => 'Stunden', 'hours' => 'Stunden',
'spent' => 'aufgewendet', 'spent' => 'aufgewendet',
@ -367,7 +359,6 @@ return array(
'The time must be a numeric value' => 'Zeit nur als nummerische Angabe', 'The time must be a numeric value' => 'Zeit nur als nummerische Angabe',
'Todo' => 'Nicht gestartet', 'Todo' => 'Nicht gestartet',
'In progress' => 'In Bearbeitung', 'In progress' => 'In Bearbeitung',
'Done' => 'Erledigt',
'Sub-task removed successfully.' => 'Unteraufgabe erfolgreich gelöscht.', 'Sub-task removed successfully.' => 'Unteraufgabe erfolgreich gelöscht.',
'Unable to remove this sub-task.' => 'Löschen der Unteraufgabe nicht möglich.', 'Unable to remove this sub-task.' => 'Löschen der Unteraufgabe nicht möglich.',
'Sub-task updated successfully.' => 'Unteraufgabe erfolgreich aktualisiert.', 'Sub-task updated successfully.' => 'Unteraufgabe erfolgreich aktualisiert.',
@ -384,7 +375,132 @@ return array(
'Unable to unlink your GitHub Account.' => 'Trennung der Verbindung zum GitHub Account nicht möglich.', 'Unable to unlink your GitHub Account.' => 'Trennung der Verbindung zum GitHub Account nicht möglich.',
'Login with my GitHub Account' => 'Anmelden mit meinem GitHub Account', 'Login with my GitHub Account' => 'Anmelden mit meinem GitHub Account',
'Link my GitHub Account' => 'Mit meinem GitHub Account verbinden', 'Link my GitHub Account' => 'Mit meinem GitHub Account verbinden',
'Unlink my GitHub Account' => 'Verbindung mit meinem GitHub Account trennen', 'Unlink my GitHub Account' => 'Verbindung mit meinem GitHub Account trennen',
'Created by %s' => 'Erstellt durch %s', 'Created by %s' => 'Erstellt durch %s',
'Last modified on %B %e, %G at %k:%M %p' => 'Letzte Änderung am %d.%m.%Y um %H:%M', 'Last modified on %B %e, %Y at %k:%M %p' => 'Letzte Änderung am %d.%m.%Y um %H:%M',
'Tasks Export' => 'Aufgaben exportieren',
'Tasks exportation for "%s"' => 'Aufgaben exportieren für "%s"',
'Start Date' => 'Anfangsdatum',
'End Date' => 'Enddatum',
'Execute' => 'Ausführen',
'Task Id' => 'Aufgaben ID',
'Creator' => 'Erstellt von',
'Modification date' => 'Änderungsdatum',
'Completion date' => 'Abschlussdatum',
'Webhook URL for task creation' => 'Webhook URL zur Aufgabenerstellung',
'Webhook URL for task modification' => 'Webhook URL zur Aufgabenänderung',
// 'Clone' => '',
// 'Clone Project' => '',
// 'Project cloned successfully.' => '',
// 'Unable to clone this project.' => '',
// 'Email notifications' => '',
// 'Enable email notifications' => '',
// 'Task position:' => '',
// 'The task #%d have been opened.' => '',
// 'The task #%d have been closed.' => '',
// 'Sub-task updated' => '',
// 'Title:' => '',
// 'Status:' => '',
// 'Assignee:' => '',
// 'Time tracking:' => '',
// 'New sub-task' => '',
// 'New attachment added "%s"' => '',
// 'Comment updated' => '',
// 'New comment posted by %s' => '',
// 'List of due tasks for the project "%s"' => '',
// '[%s][New attachment] %s (#%d)' => '',
// '[%s][New comment] %s (#%d)' => '',
// '[%s][Comment updated] %s (#%d)' => '',
// '[%s][New subtask] %s (#%d)' => '',
// '[%s][Subtask updated] %s (#%d)' => '',
// '[%s][New task] %s (#%d)' => '',
// '[%s][Task updated] %s (#%d)' => '',
// '[%s][Task closed] %s (#%d)' => '',
// '[%s][Task opened] %s (#%d)' => '',
// '[%s][Due tasks]' => '',
// '[Kanboard] Notification' => '',
// 'I want to receive notifications only for those projects:' => '',
// 'view the task on Kanboard' => '',
// 'Public access' => '',
// 'Categories management' => '',
// 'Users management' => '',
// 'Active tasks' => '',
// 'Disable public access' => '',
// 'Enable public access' => '',
// 'Active projects' => '',
// 'Inactive projects' => '',
// 'Public access disabled' => '',
// 'Do you really want to disable this project: "%s"?' => '',
// 'Do you really want to duplicate this project: "%s"?' => '',
// 'Do you really want to enable this project: "%s"?' => '',
// 'Project activation' => '',
'Move the task to another project' => 'Aufgabe in ein anderes Projekt verschieben',
'Move to another project' => 'In anderes Projekt verschieben',
// 'Do you really want to duplicate this task?' => '',
// 'Duplicate a task' => '',
// 'External accounts' => '',
// 'Account type' => '',
// 'Local' => '',
// 'Remote' => '',
// 'Enabled' => '',
// 'Disabled' => '',
// 'Google account linked' => '',
// 'Github account linked' => '',
// 'Username:' => '',
// 'Name:' => '',
// 'Email:' => '',
// 'Default project:' => '',
// 'Notifications:' => '',
// 'Group:' => '',
// 'Regular user' => '',
// 'Account type:' => '',
// 'Edit profile' => '',
// 'Change password' => '',
// 'Password modification' => '',
// 'External authentications' => '',
// 'Google Account' => '',
// 'Github Account' => '',
// 'Never connected.' => '',
// 'No account linked.' => '',
// 'Account linked.' => '',
// 'No external authentication enabled.' => '',
// 'Password modified successfully.' => '',
// 'Unable to change the password.' => '',
// 'Change category for the task "%s"' => '',
// 'Change category' => '',
// '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '',
// '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '',
// '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// 'Assigned to %s with an estimate of %s/%sh' => '',
// 'Not assigned, estimate of %sh' => '',
// '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s\'s activity' => '',
// 'No activity.' => '',
// 'RSS feed' => '',
// '%s updated a comment on the task #%d' => '',
// '%s commented on the task #%d' => '',
// '%s updated a subtask for the task #%d' => '',
// '%s created a subtask for the task #%d' => '',
// '%s updated the task #%d' => '',
// '%s created the task #%d' => '',
// '%s closed the task #%d' => '',
// '%s open the task #%d' => '',
// '%s moved the task #%d to the column "%s"' => '',
// '%s moved the task #%d to the position %d in the column "%s"' => '',
// 'Activity' => '',
// 'Default values are "%s"' => '',
// 'Default columns for new projects (Comma-separated)' => '',
// 'Task assignee change' => '',
// '%s change the assignee of the task #%d' => '',
// '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '[%s][Column Change] %s (#%d)' => '',
// '[%s][Position Change] %s (#%d)' => '',
// '[%s][Assignee Change] %s (#%d)' => '',
// 'New password for the user "%s"' => '',
); );

View file

@ -1,14 +1,6 @@
<?php <?php
return array( return array(
'English' => 'Inglés',
'French' => 'Francés',
'Polish' => 'Polaco',
'Portuguese (Brazilian)' => 'Portugués (Brasil)',
'Spanish' => 'Español',
// 'German' => '',
// 'Chinese (Simplified)' => '',
// 'Swedish' => 'Suèdois',
'None' => 'Ninguno', 'None' => 'Ninguno',
'edit' => 'modificar', 'edit' => 'modificar',
'Edit' => 'Modificar', 'Edit' => 'Modificar',
@ -37,7 +29,7 @@ return array(
'All users' => 'Todos los usuarios', 'All users' => 'Todos los usuarios',
'Username' => 'Nombre de usuario', 'Username' => 'Nombre de usuario',
'Password' => 'Contraseña', 'Password' => 'Contraseña',
'Default Project' => 'Proyecto por defecto', 'Default project' => 'Proyecto por defecto',
'Administrator' => 'Administrador', 'Administrator' => 'Administrador',
'Sign in' => 'Iniciar sesión', 'Sign in' => 'Iniciar sesión',
'Users' => 'Usuarios', 'Users' => 'Usuarios',
@ -59,6 +51,7 @@ return array(
'Status' => 'Estado', 'Status' => 'Estado',
'Tasks' => 'Tareas', 'Tasks' => 'Tareas',
'Board' => 'Tablero', 'Board' => 'Tablero',
'Actions' => 'Acciones',
'Inactive' => 'Inactivo', 'Inactive' => 'Inactivo',
'Active' => 'Activo', 'Active' => 'Activo',
'Column %d' => 'Columna %d', 'Column %d' => 'Columna %d',
@ -73,7 +66,7 @@ return array(
'Do you really want to remove this project: "%s"?' => '¿De verdad que deseas eliminar este proyecto: « %s » ?', 'Do you really want to remove this project: "%s"?' => '¿De verdad que deseas eliminar este proyecto: « %s » ?',
'Remove project' => 'Suprimir el proyecto', 'Remove project' => 'Suprimir el proyecto',
'Boards' => 'Tableros', 'Boards' => 'Tableros',
'Edit the board for "%s"' => 'Modificar el tablero por « %s »', 'Edit the board for "%s"' => 'Modificar el tablero para « %s »',
'All projects' => 'Todos los proyectos', 'All projects' => 'Todos los proyectos',
'Change columns' => 'Cambiar las columnas', 'Change columns' => 'Cambiar las columnas',
'Add a new column' => 'Añadir una nueva columna', 'Add a new column' => 'Añadir una nueva columna',
@ -90,7 +83,8 @@ return array(
'Settings' => 'Preferencias', 'Settings' => 'Preferencias',
'Application settings' => 'Parámetros de la aplicación', 'Application settings' => 'Parámetros de la aplicación',
'Language' => 'Idioma', 'Language' => 'Idioma',
'Webhooks token:' => 'Identificador (token) para los webhooks :', 'Webhooks token:' => 'Ficha de seguridad (token) para los webhooks :',
'API token:' => 'Ficha de seguridad (token) para API:',
'More information' => 'Más informaciones', 'More information' => 'Más informaciones',
'Database size:' => 'Tamaño de la base de datos:', 'Database size:' => 'Tamaño de la base de datos:',
'Download the database' => 'Descargar la base de datos', 'Download the database' => 'Descargar la base de datos',
@ -110,7 +104,7 @@ return array(
'Open a task' => 'Abrir una tarea', 'Open a task' => 'Abrir una tarea',
'Do you really want to open this task: "%s"?' => '¿Realmente desea abrir esta tarea: « %s » ?', 'Do you really want to open this task: "%s"?' => '¿Realmente desea abrir esta tarea: « %s » ?',
'Back to the board' => 'Volver al tablero', 'Back to the board' => 'Volver al tablero',
'Created on %B %e, %G at %k:%M %p' => 'Creado el %d/%m/%Y a las %H:%M', 'Created on %B %e, %Y at %k:%M %p' => 'Creado el %d/%m/%Y a las %H:%M',
'There is nobody assigned' => 'No hay nadie asignado a esta tarea', 'There is nobody assigned' => 'No hay nadie asignado a esta tarea',
'Column on the board:' => 'Columna en el tablero: ', 'Column on the board:' => 'Columna en el tablero: ',
'Status is open' => 'Estado abierto', 'Status is open' => 'Estado abierto',
@ -127,7 +121,7 @@ return array(
'The username must be unique' => 'El nombre de usuario debe ser único', 'The username must be unique' => 'El nombre de usuario debe ser único',
'The username must be alphanumeric' => 'El nombre de usuario debe ser alfanumérico', 'The username must be alphanumeric' => 'El nombre de usuario debe ser alfanumérico',
'The user id is required' => 'El identificador del usuario es obligatorio', 'The user id is required' => 'El identificador del usuario es obligatorio',
'Passwords doesn\'t matches' => 'Las contraseñas no corresponden', 'Passwords don\'t match' => 'Las contraseñas no coinciden',
'The confirmation is required' => 'La confirmación es obligatoria', 'The confirmation is required' => 'La confirmación es obligatoria',
'The column is required' => 'La columna es obligatoria', 'The column is required' => 'La columna es obligatoria',
'The project is required' => 'El proyecto es obligatorio', 'The project is required' => 'El proyecto es obligatorio',
@ -170,34 +164,33 @@ return array(
'Ready' => 'Listo', 'Ready' => 'Listo',
'Backlog' => 'En espera', 'Backlog' => 'En espera',
'Work in progress' => 'En curso', 'Work in progress' => 'En curso',
'Done' => 'Terminado', 'Done' => 'Hecho',
'Application version:' => 'Versión de la aplicación:', 'Application version:' => 'Versión de la aplicación:',
'Completed on %B %e, %G at %k:%M %p' => 'Completado el %d/%m/%Y a las %H:%M', 'Completed on %B %e, %Y at %k:%M %p' => 'Completado el %d/%m/%Y a las %H:%M',
'%B %e, %G at %k:%M %p' => '%d/%m/%Y a las %H:%M', '%B %e, %Y at %k:%M %p' => '%d/%m/%Y a las %H:%M',
'Date created' => 'Fecha de creación', 'Date created' => 'Fecha de creación',
'Date completed' => 'Fecha de terminación', 'Date completed' => 'Fecha de terminación',
'Id' => 'Identificador', 'Id' => 'Identificador',
'No task' => 'Ninguna tarea', 'No task' => 'Ninguna tarea',
'Completed tasks' => 'Tareas completadas', 'Completed tasks' => 'Tareas completadas',
'List of projects' => 'Lista de los proyectos', 'List of projects' => 'Lista de los proyectos',
'Completed tasks for "%s"' => 'Tarea completada por « %s »', 'Completed tasks for "%s"' => 'Tareas completadas por « %s »',
'%d closed tasks' => '%d tareas completadas', '%d closed tasks' => '%d tareas completadas',
'no task for this project' => 'ninguna tarea para este proyecto', 'No task for this project' => 'Ninguna tarea para este proyecto',
'Public link' => 'Enlace público', 'Public link' => 'Vinculación pública',
'There is no column in your project!' => '¡No hay ninguna columna para este proyecto!', 'There is no column in your project!' => '¡No hay ninguna columna para este proyecto!',
'Change assignee' => 'Cambiar la persona asignada', 'Change assignee' => 'Cambiar la persona asignada',
'Change assignee for the task "%s"' => 'Cambiar la persona asignada por la tarea « %s »', 'Change assignee for the task "%s"' => 'Cambiar la persona asignada por la tarea « %s »',
'Timezone' => 'Zona horaria', 'Timezone' => 'Zona horaria',
'Sorry, I didn\'t found this information in my database!' => 'Lo siento no he encontrado información en la base de datos!', 'Sorry, I didn\'t found this information in my database!' => 'Lo siento no he encontrado información en la base de datos!',
'Page not found' => 'Página no encontrada', 'Page not found' => 'Página no encontrada',
'Story Points' => 'Complejidad', 'Complexity' => 'Complejidad',
'limit' => 'límite', 'limit' => 'límite',
'Task limit' => 'Número máximo de tareas', 'Task limit' => 'Número máximo de tareas',
'This value must be greater than %d' => 'Este valor no debe de ser más grande que %d', 'This value must be greater than %d' => 'Este valor no debe de ser más grande que %d',
'Edit project access list' => 'Editar los permisos del proyecto', 'Edit project access list' => 'Editar los permisos del proyecto',
'Edit users access' => 'Editar los permisos de usuario', 'Edit users access' => 'Editar los permisos de usuario',
'Allow this user' => 'Autorizar este usuario', 'Allow this user' => 'Autorizar este usuario',
'Project access list for "%s"' => 'Permisos del proyecto « %s »',
'Only those users have access to this project:' => 'Solo estos usuarios tienen acceso a este proyecto:', 'Only those users have access to this project:' => 'Solo estos usuarios tienen acceso a este proyecto:',
'Don\'t forget that administrators have access to everything.' => 'No olvide que los administradores tienen acceso a todo.', 'Don\'t forget that administrators have access to everything.' => 'No olvide que los administradores tienen acceso a todo.',
'revoke' => 'revocar', 'revoke' => 'revocar',
@ -216,11 +209,11 @@ return array(
'The description is required' => 'La descripción es obligatoria', 'The description is required' => 'La descripción es obligatoria',
'Edit this task' => 'Editar esta tarea', 'Edit this task' => 'Editar esta tarea',
'Due Date' => 'Fecha límite', 'Due Date' => 'Fecha límite',
'm/d/Y' => 'd/m/Y', // Date format parsed with php 'm/d/Y' => 'd/m/Y',
'month/day/year' => 'día/mes/año', // Help shown to the user 'month/day/year' => 'día/mes/año',
'Invalid date' => 'Fecha no válida', 'Invalid date' => 'Fecha no válida',
'Must be done before %B %e, %G' => 'Debe de estar hecho antes del %d/%m/%Y', 'Must be done before %B %e, %Y' => 'Debe de estar hecho antes del %d/%m/%Y',
'%B %e, %G' => '%d/%m/%Y', '%B %e, %Y' => '%d/%m/%Y',
'Automatic actions' => 'Acciones automatizadas', 'Automatic actions' => 'Acciones automatizadas',
'Your automatic action have been created successfully.' => 'La acción automatizada ha sido creada correctamente.', 'Your automatic action have been created successfully.' => 'La acción automatizada ha sido creada correctamente.',
'Unable to create your automatic action.' => 'No se puede crear esta acción automatizada.', 'Unable to create your automatic action.' => 'No se puede crear esta acción automatizada.',
@ -229,6 +222,7 @@ return array(
'Action removed successfully.' => 'La acción ha sido borrada correctamente.', 'Action removed successfully.' => 'La acción ha sido borrada correctamente.',
'Automatic actions for the project "%s"' => 'Acciones automatizadas para este proyecto « %s »', 'Automatic actions for the project "%s"' => 'Acciones automatizadas para este proyecto « %s »',
'Defined actions' => 'Acciones definidas', 'Defined actions' => 'Acciones definidas',
'Add an action' => 'Agregar una acción',
'Event name' => 'Nombre del evento', 'Event name' => 'Nombre del evento',
'Action name' => 'Nombre de la acción', 'Action name' => 'Nombre de la acción',
'Action parameters' => 'Parámetros de la acción', 'Action parameters' => 'Parámetros de la acción',
@ -257,7 +251,7 @@ return array(
'Move Down' => 'Mover hacia abajo', 'Move Down' => 'Mover hacia abajo',
'Duplicate to another project' => 'Duplicar a otro proyecto', 'Duplicate to another project' => 'Duplicar a otro proyecto',
'Duplicate' => 'Duplicar', 'Duplicate' => 'Duplicar',
'link' => 'enlace', 'link' => 'vinculación',
'Update this comment' => 'Actualizar este comentario', 'Update this comment' => 'Actualizar este comentario',
'Comment updated successfully.' => 'El comentario ha sido actualizado correctamente.', 'Comment updated successfully.' => 'El comentario ha sido actualizado correctamente.',
'Unable to update your comment.' => 'No se puede actualizar este comentario.', 'Unable to update your comment.' => 'No se puede actualizar este comentario.',
@ -279,7 +273,7 @@ return array(
'IP address' => 'Dirección IP', 'IP address' => 'Dirección IP',
'User agent' => 'Agente de usuario', 'User agent' => 'Agente de usuario',
'Persistent connections' => 'Conexión persistente', 'Persistent connections' => 'Conexión persistente',
'No session' => 'No existe sesión', 'No session.' => 'No existe sesión.',
'Expiration date' => 'Fecha de expiración', 'Expiration date' => 'Fecha de expiración',
'Remember Me' => 'Recuérdame', 'Remember Me' => 'Recuérdame',
'Creation date' => 'Fecha de creación', 'Creation date' => 'Fecha de creación',
@ -297,15 +291,15 @@ return array(
'%d comments' => '%d comentarios', '%d comments' => '%d comentarios',
'%d comment' => '%d comentario', '%d comment' => '%d comentario',
'Email address invalid' => 'Dirección de correo inválida', 'Email address invalid' => 'Dirección de correo inválida',
'Your Google Account is not linked anymore to your profile.' => 'Tu Cuenta en Google ya no se encuentra enlazada con tu perfil', 'Your Google Account is not linked anymore to your profile.' => 'Tu Cuenta en Google ya no se encuentra vinculada con tu perfil',
'Unable to unlink your Google Account.' => 'No puedo desenlazar tu Cuenta en Google.', 'Unable to unlink your Google Account.' => 'No puedo desvincular tu Cuenta en Google.',
'Google authentication failed' => 'Ha fallado tu autenticación en Google', 'Google authentication failed' => 'Ha fallado tu autenticación en Google',
'Unable to link your Google Account.' => 'No puedo enlazar con tu Cuenta en Google.', 'Unable to link your Google Account.' => 'No puedo vincular con tu Cuenta en Google.',
'Your Google Account is linked to your profile successfully.' => 'Se ha enlazado correctamente tu Cuenta en Google con tu perfil.', 'Your Google Account is linked to your profile successfully.' => 'Se ha vinculado correctamente tu Cuenta en Google con tu perfil.',
'Email' => 'Correo', 'Email' => 'Correo',
'Link my Google Account' => 'Enlaza con mi Cuenta en Google', 'Link my Google Account' => 'Vincular con mi Cuenta en Google',
'Unlink my Google Account' => 'Desenlaza con mi Cuenta en Google', 'Unlink my Google Account' => 'Desvincular de mi Cuenta en Google',
'Login with my Google Account' => 'Ingresa con mi Cuenta en Google', 'Login with my Google Account' => 'Ingresar con mi Cuenta en Google',
'Project not found.' => 'Proyecto no hallado.', 'Project not found.' => 'Proyecto no hallado.',
'Task #%d' => 'Tarea número %d', 'Task #%d' => 'Tarea número %d',
'Task removed successfully.' => 'Tarea suprimida correctamente.', 'Task removed successfully.' => 'Tarea suprimida correctamente.',
@ -365,7 +359,6 @@ return array(
'The time must be a numeric value' => 'El tiempo debe de ser un valor numérico', 'The time must be a numeric value' => 'El tiempo debe de ser un valor numérico',
'Todo' => 'Por hacer', 'Todo' => 'Por hacer',
'In progress' => 'En progreso', 'In progress' => 'En progreso',
'Done' => 'Hecho',
'Sub-task removed successfully.' => 'Sub-tarea suprimida correctamente.', 'Sub-task removed successfully.' => 'Sub-tarea suprimida correctamente.',
'Unable to remove this sub-task.' => 'No pude suprimir esta sub-tarea.', 'Unable to remove this sub-task.' => 'No pude suprimir esta sub-tarea.',
'Sub-task updated successfully.' => 'Sub-tarea actualizada correctamente.', 'Sub-task updated successfully.' => 'Sub-tarea actualizada correctamente.',
@ -374,16 +367,140 @@ return array(
'Sub-task added successfully.' => 'Sub-tarea añadida correctamente.', 'Sub-task added successfully.' => 'Sub-tarea añadida correctamente.',
'Maximum size: ' => 'Tamaño máximo', 'Maximum size: ' => 'Tamaño máximo',
'Unable to upload the file.' => 'No pude cargar el fichero.', 'Unable to upload the file.' => 'No pude cargar el fichero.',
'Actions' => 'Acciones', 'Display another project' => 'Mostrar otro proyecto',
// 'Display another project' => '', 'Your GitHub account was successfully linked to your profile.' => 'Tu cuenta de GitHub ha sido correctamente vinculada con tu perfil',
// 'Your GitHub account was successfully linked to your profile.' => '', 'Unable to link your GitHub Account.' => 'Imposible vincular tu cuenta de GitHub',
// 'Unable to link your GitHub Account.' => '', 'GitHub authentication failed' => 'Falló la autenticación de GitHub',
// 'GitHub authentication failed' => '', 'Your GitHub account is no longer linked to your profile.' => 'Tu cuenta de GitHub ya no está vinculada a tu perfil',
// 'Your GitHub account is no longer linked to your profile.' => '', 'Unable to unlink your GitHub Account.' => 'Imposible desvincular tu cuenta de GitHub',
// 'Unable to unlink your GitHub Account.' => '', 'Login with my GitHub Account' => 'Ingresar con mi cuenta de GitHub',
// 'Login with my GitHub Account' => '', 'Link my GitHub Account' => 'Vincular mi cuenta de GitHub',
// 'Link my GitHub Account' => '', 'Unlink my GitHub Account' => 'Desvincular mi cuenta de GitHub',
// 'Unlink my GitHub Account' => '', 'Created by %s' => 'Creado por %s',
// 'Created by %s' => 'Créé par %s', 'Last modified on %B %e, %Y at %k:%M %p' => 'Última modificación %B %e, %Y a las %k:%M %p',
// 'Last modified on %B %e, %G at %k:%M %p' => '', 'Tasks Export' => 'Exportar tareas',
'Tasks exportation for "%s"' => 'Exportación de tareas para "%s"',
'Start Date' => 'Fecha de inicio',
'End Date' => 'Fecha final',
'Execute' => 'Ejecutar',
'Task Id' => 'ID de tarea',
'Creator' => 'Creador',
'Modification date' => 'Fecha de modificación',
'Completion date' => 'Fecha de terminación',
'Webhook URL for task creation' => 'Webhook para la creación de tareas',
'Webhook URL for task modification' => 'Webhook para la modificación de tareas',
'Clone' => 'Clonar',
'Clone Project' => 'Clonar proyecto',
'Project cloned successfully.' => 'Proyecto clonado correctamente',
'Unable to clone this project.' => 'Impsible clonar proyecto',
'Email notifications' => 'Notificaciones correo electrónico',
'Enable email notifications' => 'Habilitar notificaciones por correo electrónico',
'Task position:' => 'Posición de la tarea',
'The task #%d have been opened.' => 'La tarea #%d ha sido abierta',
'The task #%d have been closed.' => 'La tarea #%d ha sido cerrada',
'Sub-task updated' => 'Subtarea actualizada',
'Title:' => 'Título:',
'Status:' => 'Estado:',
'Assignee:' => 'Asignada a:',
'Time tracking:' => 'Control de tiempo:',
'New sub-task' => 'Nueva subtarea',
'New attachment added "%s"' => 'Nuevo adjunto agregado "%s"',
'Comment updated' => 'Comentario actualizado',
'New comment posted by %s' => 'Nuevo comentario agregado por %s',
'List of due tasks for the project "%s"' => 'Lista de tareas para el proyecto "%s"',
'[%s][New attachment] %s (#%d)' => '[%s][uevo adjunto] %s (#%d)',
'[%s][New comment] %s (#%d)' => '[%s][Nuevo comentario] %s (#%d)',
'[%s][Comment updated] %s (#%d)' => '[%s][Comentario actualizado] %s (#%d)',
'[%s][New subtask] %s (#%d)' => '[%s][Nueva subtarea] %s (#%d)',
'[%s][Subtask updated] %s (#%d)' => '[%s][Subtarea actualizada] %s (#%d)',
'[%s][New task] %s (#%d)' => '[%s][Nueva tarea] %s (#%d)',
'[%s][Task updated] %s (#%d)' => '[%s][Tarea actualizada] %s (#%d)',
'[%s][Task closed] %s (#%d)' => '[%s][Tarea cerrada] %s (#%d)',
'[%s][Task opened] %s (#%d)' => '[%s][Tarea abierta] %s (#%d)',
'[%s][Due tasks]' => '[%s][Tareas vencidas]',
'[Kanboard] Notification' => '[Kanboard] Notificación',
'I want to receive notifications only for those projects:' => 'Quiero recibir notificaciones sólo de estos proyectos:',
'view the task on Kanboard' => 'ver la tarea en Kanboard',
'Public access' => 'Acceso público',
'Categories management' => 'Gestión de Categorías',
'Users management' => 'Gestión de Usuarios',
'Active tasks' => 'Tareas activas',
'Disable public access' => 'Desactivar acceso público',
'Enable public access' => 'Activar acceso público',
'Active projects' => 'Proyectos activos',
'Inactive projects' => 'Proyectos inactivos',
'Public access disabled' => 'Acceso público desactivado',
'Do you really want to disable this project: "%s"?' => '¿Realmente deseas desactivar este proyecto: "%s"?',
'Do you really want to duplicate this project: "%s"?' => '¿Realmente deseas duplicar este proyecto: "%s"?',
'Do you really want to enable this project: "%s"?' => '¿Realmente deseas activar este proyecto: "%s"?',
'Project activation' => 'Activación de Proyecto',
'Move the task to another project' => 'Mover la tarea a otro proyecto',
'Move to another project' => 'Mover a otro proyecto',
'Do you really want to duplicate this task?' => '¿Realmente deseas duplicar esta tarea?',
'Duplicate a task' => 'Duplicar una tarea',
'External accounts' => 'Cuentas externas',
'Account type' => 'Tipo de Cuenta',
'Local' => 'Local',
'Remote' => 'Remota',
'Enabled' => 'Activada',
'Disabled' => 'Deactivada',
'Google account linked' => 'Vinculada con Cuenta de Google',
'Github account linked' => 'Vinculada con Cuenta de Gitgub',
'Username:' => 'Nombre de Usuario:',
'Name:' => 'Nombre:',
'Email:' => 'Correo electrónico:',
'Default project:' => 'Proyecto por defecto:',
'Notifications:' => 'Notificaciones:',
'Group:' => 'Grupo:',
'Regular user' => 'Usuario regular:',
'Account type:' => 'Tipo de Cuenta:',
'Edit profile' => 'Editar perfil',
'Change password' => 'Cambiar contraseña',
'Password modification' => 'Modificacion de contraseña',
'External authentications' => 'Autenticación externa',
'Google Account' => 'Cuenta de Google',
'Github Account' => 'Cuenta de Github',
'Never connected.' => 'Nunca se ha conectado.',
'No account linked.' => 'Sin vínculo con cuenta.',
'Account linked.' => 'Vinculada con Cuenta.',
'No external authentication enabled.' => 'Sin autenticación externa activa.',
'Password modified successfully.' => 'Contraseña cambiada correctamente.',
'Unable to change the password.' => 'No pude cambiar la contraseña.',
'Change category for the task "%s"' => 'Cambiar la categoría de la tarea "%s"',
'Change category' => 'Cambiar categoría',
'%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s actualizó la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s abrió la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s movió la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> a la posición #%d de la columna "%s"',
'%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '%s movió la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> a la columna "%s"',
'%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s creó la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s cerró la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s creó una subtarea para la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s actualizó una subtarea para la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'Assigned to %s with an estimate of %s/%sh' => 'Asignada a %s con una estimación de %s/%sh',
'Not assigned, estimate of %sh' => 'No asignada, se estima en %sh',
'%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s actualizó un comentario de la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s comentó la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'%s\'s activity' => 'Actividad de %s',
'No activity.' => 'Sin actividad',
'RSS feed' => 'Fichero RSS',
'%s updated a comment on the task #%d' => '%s actualizó un comentario de la tarea #%d',
'%s commented on the task #%d' => '%s comentó la tarea #%d',
'%s updated a subtask for the task #%d' => '%s actualizó una subtarea de la tarea #%d',
'%s created a subtask for the task #%d' => '%s creó una subtarea de la tarea #%d',
'%s updated the task #%d' => '%s actualizó la tarea #%d',
'%s created the task #%d' => '%s creó la tarea #%d',
'%s closed the task #%d' => '%s cerró la tarea #%d',
'%s open the task #%d' => '%s abrió la tarea #%d',
'%s moved the task #%d to the column "%s"' => '%s movió la tarea #%d a la columna "%s"',
'%s moved the task #%d to the position %d in the column "%s"' => '%s movió la tarea #%d a la posición %d de la columna "%s"',
'Activity' => 'Actividad',
'Default values are "%s"' => 'Los valores por defecto son "%s"',
'Default columns for new projects (Comma-separated)' => 'Columnas por defecto de los nuevos proyectos (Separadas mediante comas)',
// 'Task assignee change' => '',
// '%s change the assignee of the task #%d' => '',
// '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '[%s][Column Change] %s (#%d)' => '',
// '[%s][Position Change] %s (#%d)' => '',
// '[%s][Assignee Change] %s (#%d)' => '',
// 'New password for the user "%s"' => '',
); );

View file

@ -0,0 +1,506 @@
<?php
return array(
'None' => 'Ei mikään',
'edit' => 'muokkaa',
'Edit' => 'Muokkaa',
'remove' => 'poista',
'Remove' => 'Poista',
'Update' => 'Päivitä',
'Yes' => 'Kyllä',
'No' => 'Ei',
'cancel' => 'peruuta',
'or' => 'tai',
'Yellow' => 'Keltainen',
'Blue' => 'Sininen',
'Green' => 'Vihreä',
'Purple' => 'Violetti',
'Red' => 'Punainen',
'Orange' => 'Oranssi',
'Grey' => 'Harmaa',
'Save' => 'Tallenna',
'Login' => 'Sisäänkirjautuminen',
'Official website:' => 'Virallinen verkkosivu:',
'Unassigned' => 'Ei suorittajaa',
'View this task' => 'Näytä tämä tehtävä',
'Remove user' => 'Poista käyttäjä',
'Do you really want to remove this user: "%s"?' => 'Oletko varma että haluat poistaa käyttäjän "%s"?',
'New user' => 'Uusi käyttäjä',
'All users' => 'Kaikki käyttäjät',
'Username' => 'Käyttäjänimi',
'Password' => 'Salasana',
'Default project' => 'Oletusprojekti',
'Administrator' => 'Ylläpitäjä',
'Sign in' => 'Kirjaudu sisään',
'Users' => 'Käyttäjät',
'No user' => 'Ei käyttäjää',
'Forbidden' => 'Estetty',
'Access Forbidden' => 'Pääsy estetty',
'Only administrators can access to this page.' => 'Vain ylläpitäjillä on pääsy tälle sivulle.',
'Edit user' => 'Muokkaa käyttäjää',
'Logout' => 'Kirjaudu ulos',
'Bad username or password' => 'Väärä käyttäjätunnus tai salasana',
'users' => 'käyttäjät',
'projects' => 'projektit',
'Edit project' => 'Muokkaa projektia',
'Name' => 'Nimi',
'Activated' => 'Aktivoitu',
'Projects' => 'Projektit',
'No project' => 'Ei projektia',
'Project' => 'Projekti',
'Status' => 'Status',
'Tasks' => 'Tehtävät',
'Board' => 'Taulu',
'Actions' => 'Toiminnot',
'Inactive' => 'Ei aktiivinen',
'Active' => 'Aktiivinen',
'Column %d' => 'Sarake %d',
'Add this column' => 'Lisää tämä sarake',
'%d tasks on the board' => '%d tehtävää taululla',
'%d tasks in total' => '%d tehtävää yhteensä',
'Unable to update this board.' => 'Taulun muuttaminen ei onnistunut.',
'Edit board' => 'Muuta taulua',
'Disable' => 'Disabloi',
'Enable' => 'Aktivoi',
'New project' => 'Uusi projekti',
'Do you really want to remove this project: "%s"?' => 'Haluatko varmasti poistaa projektin: "%s"?',
'Remove project' => 'Poista projekti',
'Boards' => 'Taulut',
'Edit the board for "%s"' => 'Muokkaa taulua projektille "%s"',
'All projects' => 'Kaikki projektit',
'Change columns' => 'Muokkaa sarakkeita',
'Add a new column' => 'Lisää uusi sarake',
'Title' => 'Nimi',
'Add Column' => 'Lisää sarake',
'Project "%s"' => 'Projekti "%s"',
'Nobody assigned' => 'Ei suorittajaa',
'Assigned to %s' => 'Tekijä: %s',
'Remove a column' => 'Poista sarake',
'Remove a column from a board' => 'Poista sarake taulusta',
'Unable to remove this column.' => 'Sarakkeen poistaminen ei onnistunut.',
'Do you really want to remove this column: "%s"?' => 'Haluatko varmasti poistaa sarakkeen "%s"?',
'This action will REMOVE ALL TASKS associated to this column!' => 'Tämä toiminto POISTAA KAIKKI TEHTÄVÄT tästä sarakkeesta!',
'Settings' => 'Asetukset',
'Application settings' => 'Ohjelman asetukset',
'Language' => 'Kieli',
'Webhooks token:' => 'Webhooks avain:',
// 'API token:' => '',
'More information' => 'Lisätietoja',
'Database size:' => 'Tietokannan koko:',
'Download the database' => 'Lataa tietokanta',
'Optimize the database' => 'Optimoi tietokanta',
'(VACUUM command)' => '(VACUUM-komento)',
'(Gzip compressed Sqlite file)' => '(Gzip-pakattu Sqlite-tiedosto)',
'User settings' => 'Käyttäjän asetukset',
'My default project:' => 'Oletusprojektini: ',
'Close a task' => 'Sulje tehtävä',
'Do you really want to close this task: "%s"?' => 'Haluatko varmasti sulkea tehtävän: "%s"?',
'Edit a task' => 'Muokkaa tehtävää',
'Column' => 'Sarake',
'Color' => 'Väri',
'Assignee' => 'Suorittaja',
'Create another task' => 'Luo toinen tehtävä',
'New task' => 'Uusi tehtävä',
'Open a task' => 'Avaa tehtävä',
'Do you really want to open this task: "%s"?' => 'Haluatko varmasti avata tehtävän: "%s"?',
'Back to the board' => 'Takaisin tauluun',
'Created on %B %e, %Y at %k:%M %p' => 'Luotu %d.%m.%Y kello %H:%M',
'There is nobody assigned' => 'Ei suorittajaa',
'Column on the board:' => 'Sarake taululla: ',
'Status is open' => 'Status on avoin',
'Status is closed' => 'Status on suljettu',
'Close this task' => 'Sulje tämä tehtävä',
'Open this task' => 'Avaa tämä tehtävä',
'There is no description.' => 'Ei kuvausta.',
'Add a new task' => 'Lisää uusi tehtävä',
'The username is required' => 'Käyttäjätunnut vaaditaan',
'The maximum length is %d characters' => 'Maksimipituus on %d merkkiä',
'The minimum length is %d characters' => 'Vähimmäispituus on %d merkkiä',
'The password is required' => 'Salasana vaaditaan',
'This value must be an integer' => 'Tämän arvon täytyy olla numero',
'The username must be unique' => 'Käyttäjänimi täytyy olla uniikki',
'The username must be alphanumeric' => 'Käyttäjänimen täytyy olla alfanumeerinen',
'The user id is required' => 'Käyttäjän id on pakollinen',
// 'Passwords don\'t match' => '',
'The confirmation is required' => 'Varmistus vaaditaan',
'The column is required' => 'Sarake on pakollinen',
'The project is required' => 'Projekti on pakollinen',
'The color is required' => 'Väri on pakollinen',
'The id is required' => 'ID vaaditaan',
'The project id is required' => 'Projektin ID on pakollinen',
'The project name is required' => 'Projektin nimi on pakollinen',
'This project must be unique' => 'Projektin nimi täytyy olla uniikki',
'The title is required' => 'Otsikko vaaditaan',
'The language is required' => 'Kieli on pakollinen',
'There is no active project, the first step is to create a new project.' => 'Aktiivista projektia ei ole, ensimmäinen vaihe on luoda uusi projekti.',
'Settings saved successfully.' => 'Asetukset tallennettu onnistuneesti.',
'Unable to save your settings.' => 'Asetusten tallentaminen epäonnistui.',
'Database optimization done.' => 'Tietokannan optimointi suoritettu.',
'Your project have been created successfully.' => 'Projekti luotiin onnistuneesti.',
'Unable to create your project.' => 'Projektin luominen epäonnistui.',
'Project updated successfully.' => 'Projekti päivitettiin onnistuneesti.',
'Unable to update this project.' => 'Projektin muuttaminen epäonnistui.',
'Unable to remove this project.' => 'Projektin poistaminen epäonnistui.',
'Project removed successfully.' => 'Projekti poistettiin onnistuneesti.',
'Project activated successfully.' => 'Projekti aktivoitiin onnistuneesti.',
'Unable to activate this project.' => 'Projektin aktivoiminen epäonnistui.',
'Project disabled successfully.' => 'Projektin disabloiminen onnistui.',
'Unable to disable this project.' => 'Projektin disabloiminen epäonnistui.',
'Unable to open this task.' => 'Tehtävän avaus epäonnistui.',
'Task opened successfully.' => 'Tehtävä avattiin onnistuneesti.',
'Unable to close this task.' => 'Tehtävän sulkeminen epäonnistui.',
'Task closed successfully.' => 'Tehtävä suljettiin onnistuneesti.',
'Unable to update your task.' => 'Tehtävän muokkaaminen epäonnistui.',
'Task updated successfully.' => 'Tehtävä päivitettiin onnistuneesti.',
'Unable to create your task.' => 'Tehtävän luominen epäonnistui.',
'Task created successfully.' => 'Tehtävä luotiin onnistuneesti.',
'User created successfully.' => 'Käyttäjä lisättiin onnistuneesti.',
'Unable to create your user.' => 'Käyttäjän lisäys epäonnistui.',
'User updated successfully.' => 'Käyttäjätietojen päivitys onnistui.',
'Unable to update your user.' => 'Käyttäjätietojen päivitys epäonnistui.',
'User removed successfully.' => 'Käyttäjä poistettiin onnistuneesti.',
'Unable to remove this user.' => 'Käyttäjän poistaminen epäonnistui.',
'Board updated successfully.' => 'Taulu päivitettiin onnistuneesti.',
'Ready' => 'Valmis',
'Backlog' => 'Tehtäväjono',
'Work in progress' => 'Työnalla',
'Done' => 'Tehty',
'Application version:' => 'Ohjelman versio:',
'Completed on %B %e, %Y at %k:%M %p' => 'Valmistunut %d.%m.%Y kello %H:%M',
'%B %e, %Y at %k:%M %p' => '%d.%m.%Y kello %H:%M',
'Date created' => 'Luomispäivä',
'Date completed' => 'Valmistumispäivä',
'Id' => 'Id',
'No task' => 'Ei tehtävää',
'Completed tasks' => 'Valmiit tehtävät',
'List of projects' => 'Projektit',
'Completed tasks for "%s"' => 'Suoritetut tehtävät projektille %s',
'%d closed tasks' => '%d suljettua tehtävää',
'No task for this project' => 'Ei tehtävää tälle projektille',
'Public link' => 'Julkinen linkki',
'There is no column in your project!' => 'Projektilta puuttuu sarakkeet!',
'Change assignee' => 'Vaihda suorittajaa',
'Change assignee for the task "%s"' => 'Vaihda suorittajaa tehtävälle %s',
'Timezone' => 'Aikavyöhyke',
'Sorry, I didn\'t found this information in my database!' => 'Anteeksi, en löytänyt tätä tietoa tietokannastani',
'Page not found' => 'Sivua ei löydy',
'Complexity' => 'Monimutkaisuus',
'limit' => 'raja',
'Task limit' => 'Tehtävien maksimimäärä',
'This value must be greater than %d' => 'Arvon täytyy olla suurempi kuin %d',
'Edit project access list' => 'Muuta projektin käyttäjiä',
'Edit users access' => 'Muuta käyttäjien pääsyä',
'Allow this user' => 'Salli tämä projekti',
'Only those users have access to this project:' => 'Vain näillä käyttäjillä on pääsy projektiin:',
'Don\'t forget that administrators have access to everything.' => 'Muista että ylläpitäjät pääsevät kaikkialle.',
'revoke' => 'poista',
'List of authorized users' => 'Sallittujen käyttäjien lista',
'User' => 'Käyttäjät',
'Everybody have access to this project.' => 'Kaikilla on pääsy tähän projektiin.',
'You are not allowed to access to this project.' => 'Sinulla ei ole pääsyä tähän projektiin.',
'Comments' => 'Kommentit',
'Post comment' => 'Lisää kommentti',
'Write your text in Markdown' => 'Kirjoita kommenttisi Markdownilla',
'Leave a comment' => 'Lisää kommentti',
'Comment is required' => 'Kommentti vaaditaan',
'Leave a description' => 'Lisää kuvaus',
'Comment added successfully.' => 'Kommentti lisättiin onnistuneesti.',
'Unable to create your comment.' => 'Kommentin lisäys epäonnistui.',
'The description is required' => 'Kuvaus vaaditaan',
'Edit this task' => 'Muokkaa tehtävää',
'Due Date' => 'Deadline',
'm/d/Y' => 'd.m.Y',
'month/day/year' => 'päivä.kuukausi.vuosi',
'Invalid date' => 'Virheellinen päiväys',
'Must be done before %B %e, %Y' => 'Täytyy suorittaa ennen %d.%m.%Y',
'%B %e, %Y' => '%d.%m.%Y',
'Automatic actions' => 'Automaattiset toiminnot',
'Your automatic action have been created successfully.' => 'Toiminto suoritettiin onnistuneesti.',
'Unable to create your automatic action.' => 'Automaattisen toiminnon luominen epäonnistui.',
'Remove an action' => 'Poista toiminto',
'Unable to remove this action.' => 'Toiminnon poistaminen epäonnistui.',
'Action removed successfully.' => 'Toiminto poistettiin onnistuneesti.',
'Automatic actions for the project "%s"' => 'Automaattiset toiminnot projektille "%s"',
'Defined actions' => 'Määritellyt toiminnot',
// 'Add an action' => '',
'Event name' => 'Tapahtuman nimi',
'Action name' => 'Toiminnon nimi',
'Action parameters' => 'Toiminnon parametrit',
'Action' => 'Toiminto',
'Event' => 'Tapahtuma',
'When the selected event occurs execute the corresponding action.' => 'Kun valittu tapahtuma tapahtuu, suorita vastaava toiminto.',
'Next step' => 'Seuraava vaihe',
'Define action parameters' => 'Määrittele toiminnon parametrit',
'Save this action' => 'Tallenna toiminto',
'Do you really want to remove this action: "%s"?' => 'Oletko varma että haluat poistaa toiminnon "%s"?',
'Remove an automatic action' => 'Poista automaattintn toiminto',
'Close the task' => 'Sulje tehtävä',
'Assign the task to a specific user' => 'Osoita tehtävä käyttäjälle',
'Assign the task to the person who does the action' => 'Määritä suorittaja tehtävälle',
'Duplicate the task to another project' => 'Monista tehtävä toiselle projektille',
'Move a task to another column' => 'Siirrä tehtävä toiseen sarakkeeseen',
'Move a task to another position in the same column' => 'Siirrä tehtävä eri järjestykseen samassa sarakkeessa',
'Task modification' => 'Tehtävän muokkaus',
'Task creation' => 'Tehtävän luominen',
'Open a closed task' => 'Avaa jo suljettu tehtävä',
'Closing a task' => 'Tehtävää suljetaan',
'Assign a color to a specific user' => 'Valitse väri käyttäjälle',
'Column title' => 'Sarakkeen nimi',
'Position' => 'Positio',
'Move Up' => 'Siirrä ylös',
'Move Down' => 'Siirrä alas',
'Duplicate to another project' => 'Kopioi toiseen projektiin',
'Duplicate' => 'Monista',
'link' => 'linkki',
'Update this comment' => 'Muuta projektia',
'Comment updated successfully.' => 'Kommentti päivitettiin onnistuneesti.',
'Unable to update your comment.' => 'Kommentin päivitys epäonnistui.',
'Remove a comment' => 'Poista kommentti',
'Comment removed successfully.' => 'Kommentti poistettiin onnistuneesti.',
'Unable to remove this comment.' => 'Kommentin poistaminen epäonnistui.',
'Do you really want to remove this comment?' => 'Haluatko varmasti poistaa tämän kommentin?',
'Only administrators or the creator of the comment can access to this page.' => 'Vain ylläpitäjillä tai kommentin jättäjällä on pääsy tälle sivulle.',
'Details' => 'Tiedot',
'Current password for the user "%s"' => 'Käyttäjän "%s" salasana',
'The current password is required' => 'Salasana vaaditaan',
'Wrong password' => 'Väärä salasana',
'Reset all tokens' => 'Resetoi kaikki tokenit',
'All tokens have been regenerated.' => 'Kaikki tokenit luotiin uudelleen.',
'Unknown' => 'Tuntematon',
'Last logins' => 'Viimeisimmät kirjautumiset',
'Login date' => 'Kirjautumispäivä',
'Authentication method' => 'Autentikointimenetelmä',
'IP address' => 'IP-Osoite',
'User agent' => 'Selain',
'Persistent connections' => 'Voimassa olevat yhteydet',
'No session.' => 'Ei sessioita.',
'Expiration date' => 'Vanhentumispäivä',
'Remember Me' => 'Muista minut',
'Creation date' => 'Luomispäivä',
'Filter by user' => 'Rajaa käyttäjän mukaan',
'Filter by due date' => 'Rajaa deadlinen mukaan',
'Everybody' => 'Kaikki',
'Open' => 'Avoin',
'Closed' => 'Suljettu',
'Search' => 'Etsi',
'Nothing found.' => 'Ei löytynyt.',
'Search in the project "%s"' => 'Etsi projektista "%s"',
'Due date' => 'Deadline',
'Others formats accepted: %s and %s' => 'Muut hyväksytyt muodot: %s ja %s',
'Description' => 'Kuvaus',
'%d comments' => '%d kommenttia',
'%d comment' => '%d kommentti',
'Email address invalid' => 'Email ei kelpaa',
'Your Google Account is not linked anymore to your profile.' => 'Google tunnustasi ei ole enää linkattu profiiliisi',
'Unable to unlink your Google Account.' => 'Google tunnuksen linkkaamisen poistaminen epäonnistui.',
'Google authentication failed' => 'Google autentikointi epäonnistui',
'Unable to link your Google Account.' => 'Google tunnuksen linkkaaminen epäonnistui.',
'Your Google Account is linked to your profile successfully.' => 'Google tunnuksesi linkitettiin profiiliisi onnistuneesti.',
'Email' => 'Sähköposti',
'Link my Google Account' => 'Linkitä Google-tili',
'Unlink my Google Account' => 'Poista Google-tilin linkitys',
'Login with my Google Account' => 'Kirjaudu Google tunnuksella',
'Project not found.' => 'Projektia ei löytynyt.',
'Task #%d' => 'Tehtävä #%d',
'Task removed successfully.' => 'Tehtävä poistettiin onnistuneesti.',
'Unable to remove this task.' => 'Tehtävän poistaminen epäonnistui.',
'Remove a task' => 'Poista tehtävä',
'Do you really want to remove this task: "%s"?' => 'Haluatko varmasti poistaa tehtävän: "%s"?',
'Assign automatically a color based on a category' => 'Aseta väri automaattisesti kategorian mukaan',
'Assign automatically a category based on a color' => 'Aseta kategoria automaattisesti värin mukaan',
'Task creation or modification' => 'Tehtävän luonti tai muuttaminen',
'Category' => 'Kategoria',
'Category:' => 'Kategoria:',
'Categories' => 'Kategoriat',
'Category not found.' => 'Kategoriaa ei löytynyt.',
'Your category have been created successfully.' => 'Kategoria luotiin onnistuneesti.',
'Unable to create your category.' => 'Kategorian luonti epäonnistui.',
'Your category have been updated successfully.' => 'Kategoriaa muokattiin onnistuneesti.',
'Unable to update your category.' => 'Kategorian muokkaaminen epäonnistui.',
'Remove a category' => 'Poista kategoria',
'Category removed successfully.' => 'Kategoria poistettu onnistuneesti.',
'Unable to remove this category.' => 'Kategorian poisto epäonnistui.',
'Category modification for the project "%s"' => 'Kategorian muutos projektissa "%s"',
'Category Name' => 'Kategorian nimi',
'Categories for the project "%s"' => 'Kategoriat projektille "%s"',
'Add a new category' => 'Lisää uusi kategoria',
'Do you really want to remove this category: "%s"?' => 'Haluatko varmasti poistaa kategorian: "%s"?',
'Filter by category' => 'Rajaa kategorian mukaan',
'All categories' => 'Kaikki kategoriat',
'No category' => 'Kategoriaa ei löydy',
'The name is required' => 'Nimi vaaditaan',
'Remove a file' => 'Poista tiedosto',
'Unable to remove this file.' => 'Tiedoston poistaminen epäonnistui.',
'File removed successfully.' => 'Tiedosto poistettiin onnistuneesti.',
'Attach a document' => 'Liitä dokumentti',
'Do you really want to remove this file: "%s"?' => 'Haluatko varmasti poistaa tiedoston: "%s"?',
'open' => 'avaa',
'Attachments' => 'Liitteet',
'Edit the task' => 'Muokkaa tehtävää',
'Edit the description' => 'Muokkaa kuvausta',
'Add a comment' => 'Lisää kommentti',
'Edit a comment' => 'Muokkaa kommenttia',
'Summary' => 'Yhteenveto',
'Time tracking' => 'Ajan seuranta',
'Estimate:' => 'Arvio:',
'Spent:' => 'Käytetty:',
'Do you really want to remove this sub-task?' => 'Haluatko varmasti poistaa tämän alitehtävän?',
'Remaining:' => 'Jäljellä',
'hours' => 'tuntia',
'spent' => 'käytetty',
'estimated' => 'estimoitu',
'Sub-Tasks' => 'Alitehtävät',
'Add a sub-task' => 'Lisää alitehtävä',
'Original Estimate' => 'Alkuperäinen estimaatti',
'Create another sub-task' => 'Lisää toinen alitehtävä',
'Time Spent' => 'Käytetty aika',
'Edit a sub-task' => 'Muokkaa alitehtävää',
'Remove a sub-task' => 'Poista alitehtävä',
'The time must be a numeric value' => 'Ajan pitää olla numero',
'Todo' => 'Todo',
'In progress' => 'Työnalla',
'Sub-task removed successfully.' => 'Alitehtävä poistettu onnistuneesti.',
'Unable to remove this sub-task.' => 'Alitehtävän poistaminen epäonnistui.',
'Sub-task updated successfully.' => 'Alitehtävä päivitettiin onnistuneesti.',
'Unable to update your sub-task.' => 'Alitehtävän päivitys epäonnistui.',
'Unable to create your sub-task.' => 'Alitehtävän luonti epäonnistui.',
'Sub-task added successfully.' => 'Alitehtävä luotiin onnistuneesti.',
'Maximum size: ' => 'Maksimikoko: ',
'Unable to upload the file.' => 'Tiedoston lataus epäonnistui.',
'Display another project' => 'Näytä toinen projekti',
// 'Your GitHub account was successfully linked to your profile.' => '',
// 'Unable to link your GitHub Account.' => '',
// 'GitHub authentication failed' => '',
// 'Your GitHub account is no longer linked to your profile.' => '',
// 'Unable to unlink your GitHub Account.' => '',
// 'Login with my GitHub Account' => '',
// 'Link my GitHub Account' => '',
// 'Unlink my GitHub Account' => '',
'Created by %s' => 'Luonut: %s',
'Last modified on %B %e, %Y at %k:%M %p' => 'Viimeksi muokattu %B %e, %Y kello %H:%M',
'Tasks Export' => 'Tehtävien vienti',
'Tasks exportation for "%s"' => 'Tehtävien vienti projektilta "%s"',
'Start Date' => 'Aloituspäivä',
'End Date' => 'Lopetuspäivä',
'Execute' => 'Suorita',
'Task Id' => 'Tehtävän ID',
'Creator' => 'Luonut',
'Modification date' => 'Muokkauspäivä',
'Completion date' => 'Valmistumispäivä',
'Webhook URL for task creation' => 'Webhook URL tehtävän luomiselle',
'Webhook URL for task modification' => 'Webhook URL tehtävän muokkaamiselle',
// 'Clone' => '',
// 'Clone Project' => '',
// 'Project cloned successfully.' => '',
// 'Unable to clone this project.' => '',
// 'Email notifications' => '',
// 'Enable email notifications' => '',
// 'Task position:' => '',
// 'The task #%d have been opened.' => '',
// 'The task #%d have been closed.' => '',
// 'Sub-task updated' => '',
// 'Title:' => '',
// 'Status:' => '',
// 'Assignee:' => '',
// 'Time tracking:' => '',
// 'New sub-task' => '',
// 'New attachment added "%s"' => '',
// 'Comment updated' => '',
// 'New comment posted by %s' => '',
// 'List of due tasks for the project "%s"' => '',
// '[%s][New attachment] %s (#%d)' => '',
// '[%s][New comment] %s (#%d)' => '',
// '[%s][Comment updated] %s (#%d)' => '',
// '[%s][New subtask] %s (#%d)' => '',
// '[%s][Subtask updated] %s (#%d)' => '',
// '[%s][New task] %s (#%d)' => '',
// '[%s][Task updated] %s (#%d)' => '',
// '[%s][Task closed] %s (#%d)' => '',
// '[%s][Task opened] %s (#%d)' => '',
// '[%s][Due tasks]' => '',
// '[Kanboard] Notification' => '',
// 'I want to receive notifications only for those projects:' => '',
// 'view the task on Kanboard' => '',
// 'Public access' => '',
// 'Categories management' => '',
// 'Users management' => '',
// 'Active tasks' => '',
// 'Disable public access' => '',
// 'Enable public access' => '',
// 'Active projects' => '',
// 'Inactive projects' => '',
// 'Public access disabled' => '',
// 'Do you really want to disable this project: "%s"?' => '',
// 'Do you really want to duplicate this project: "%s"?' => '',
// 'Do you really want to enable this project: "%s"?' => '',
// 'Project activation' => '',
// 'Move the task to another project' => '',
// 'Move to another project' => '',
// 'Do you really want to duplicate this task?' => '',
// 'Duplicate a task' => '',
// 'External accounts' => '',
// 'Account type' => '',
// 'Local' => '',
// 'Remote' => '',
// 'Enabled' => '',
// 'Disabled' => '',
// 'Google account linked' => '',
// 'Github account linked' => '',
// 'Username:' => '',
// 'Name:' => '',
// 'Email:' => '',
// 'Default project:' => '',
// 'Notifications:' => '',
// 'Group:' => '',
// 'Regular user' => '',
// 'Account type:' => '',
// 'Edit profile' => '',
// 'Change password' => '',
// 'Password modification' => '',
// 'External authentications' => '',
// 'Google Account' => '',
// 'Github Account' => '',
// 'Never connected.' => '',
// 'No account linked.' => '',
// 'Account linked.' => '',
// 'No external authentication enabled.' => '',
// 'Password modified successfully.' => '',
// 'Unable to change the password.' => '',
// 'Change category for the task "%s"' => '',
// 'Change category' => '',
// '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '',
// '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '',
// '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// 'Assigned to %s with an estimate of %s/%sh' => '',
// 'Not assigned, estimate of %sh' => '',
// '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s\'s activity' => '',
// 'No activity.' => '',
// 'RSS feed' => '',
// '%s updated a comment on the task #%d' => '',
// '%s commented on the task #%d' => '',
// '%s updated a subtask for the task #%d' => '',
// '%s created a subtask for the task #%d' => '',
// '%s updated the task #%d' => '',
// '%s created the task #%d' => '',
// '%s closed the task #%d' => '',
// '%s open the task #%d' => '',
// '%s moved the task #%d to the column "%s"' => '',
// '%s moved the task #%d to the position %d in the column "%s"' => '',
// 'Activity' => '',
// 'Default values are "%s"' => '',
// 'Default columns for new projects (Comma-separated)' => '',
// 'Task assignee change' => '',
// '%s change the assignee of the task #%d' => '',
// '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '[%s][Column Change] %s (#%d)' => '',
// '[%s][Position Change] %s (#%d)' => '',
// '[%s][Assignee Change] %s (#%d)' => '',
// 'New password for the user "%s"' => '',
);

View file

@ -1,14 +1,6 @@
<?php <?php
return array( return array(
'English' => 'Anglais',
'French' => 'Français',
'Polish' => 'Polonais',
'Portuguese (Brazilian)' => 'Portugais (Brésil)',
'Spanish' => 'Espagnol',
'German' => 'Allemand',
'Chinese (Simplified)' => 'Chinois simplifié',
'Swedish' => 'Suèdois',
'None' => 'Aucun', 'None' => 'Aucun',
'edit' => 'modifier', 'edit' => 'modifier',
'Edit' => 'Modifier', 'Edit' => 'Modifier',
@ -35,9 +27,9 @@ return array(
'Do you really want to remove this user: "%s"?' => 'Voulez-vous vraiment supprimer cet utilisateur : « %s » ?', 'Do you really want to remove this user: "%s"?' => 'Voulez-vous vraiment supprimer cet utilisateur : « %s » ?',
'New user' => 'Ajouter un utilisateur', 'New user' => 'Ajouter un utilisateur',
'All users' => 'Tous les utilisateurs', 'All users' => 'Tous les utilisateurs',
'Username' => 'Identifiant', 'Username' => 'Nom d\'utilisateur',
'Password' => 'Mot de passe', 'Password' => 'Mot de passe',
'Default Project' => 'Projet par défaut', 'Default project' => 'Projet par défaut',
'Administrator' => 'Administrateur', 'Administrator' => 'Administrateur',
'Sign in' => 'Connexion', 'Sign in' => 'Connexion',
'Users' => 'Utilisateurs', 'Users' => 'Utilisateurs',
@ -59,6 +51,7 @@ return array(
'Status' => 'État', 'Status' => 'État',
'Tasks' => 'Tâches', 'Tasks' => 'Tâches',
'Board' => 'Tableau', 'Board' => 'Tableau',
'Actions' => 'Actions',
'Inactive' => 'Inactif', 'Inactive' => 'Inactif',
'Active' => 'Actif', 'Active' => 'Actif',
'Column %d' => 'Colonne %d', 'Column %d' => 'Colonne %d',
@ -91,6 +84,7 @@ return array(
'Application settings' => 'Paramètres de l\'application', 'Application settings' => 'Paramètres de l\'application',
'Language' => 'Langue', 'Language' => 'Langue',
'Webhooks token:' => 'Jeton de securité pour les webhooks :', 'Webhooks token:' => 'Jeton de securité pour les webhooks :',
'API token:' => 'Jeton de securité pour l\'API :',
'More information' => 'Plus d\'informations', 'More information' => 'Plus d\'informations',
'Database size:' => 'Taille de la base de données :', 'Database size:' => 'Taille de la base de données :',
'Download the database' => 'Télécharger la base de données', 'Download the database' => 'Télécharger la base de données',
@ -110,7 +104,7 @@ return array(
'Open a task' => 'Ouvrir une tâche', 'Open a task' => 'Ouvrir une tâche',
'Do you really want to open this task: "%s"?' => 'Voulez-vous vraiment ouvrir cette tâche : « %s » ?', 'Do you really want to open this task: "%s"?' => 'Voulez-vous vraiment ouvrir cette tâche : « %s » ?',
'Back to the board' => 'Retour au tableau', 'Back to the board' => 'Retour au tableau',
'Created on %B %e, %G at %k:%M %p' => 'Créé le %d/%m/%Y à %H:%M', 'Created on %B %e, %Y at %k:%M %p' => 'Créé le %d/%m/%Y à %H:%M',
'There is nobody assigned' => 'Il n\'y a personne d\'assigné à cette tâche', 'There is nobody assigned' => 'Il n\'y a personne d\'assigné à cette tâche',
'Column on the board:' => 'Colonne sur le tableau : ', 'Column on the board:' => 'Colonne sur le tableau : ',
'Status is open' => 'État ouvert', 'Status is open' => 'État ouvert',
@ -172,8 +166,8 @@ return array(
'Work in progress' => 'En cours', 'Work in progress' => 'En cours',
'Done' => 'Terminé', 'Done' => 'Terminé',
'Application version:' => 'Version de l\'application :', 'Application version:' => 'Version de l\'application :',
'Completed on %B %e, %G at %k:%M %p' => 'Terminé le %d/%m/%Y à %H:%M', 'Completed on %B %e, %Y at %k:%M %p' => 'Terminé le %d/%m/%Y à %H:%M',
'%B %e, %G at %k:%M %p' => '%d/%m/%Y à %H:%M', '%B %e, %Y at %k:%M %p' => '%d/%m/%Y à %H:%M',
'Date created' => 'Date de création', 'Date created' => 'Date de création',
'Date completed' => 'Date de clôture', 'Date completed' => 'Date de clôture',
'Id' => 'Identifiant', 'Id' => 'Identifiant',
@ -182,22 +176,21 @@ return array(
'List of projects' => 'Liste des projets', 'List of projects' => 'Liste des projets',
'Completed tasks for "%s"' => 'Tâches terminées pour « %s »', 'Completed tasks for "%s"' => 'Tâches terminées pour « %s »',
'%d closed tasks' => '%d tâches terminées', '%d closed tasks' => '%d tâches terminées',
'no task for this project' => 'aucune tâche pour ce projet', 'No task for this project' => 'Aucune tâche pour ce projet',
'Public link' => 'Accès public', 'Public link' => 'Lien public',
'There is no column in your project!' => 'Il n\'y a aucune colonne dans votre projet !', 'There is no column in your project!' => 'Il n\'y a aucune colonne dans votre projet !',
'Change assignee' => 'Changer la personne assignée', 'Change assignee' => 'Changer la personne assignée',
'Change assignee for the task "%s"' => 'Changer la personne assignée pour la tâche « %s »', 'Change assignee for the task "%s"' => 'Changer la personne assignée pour la tâche « %s »',
'Timezone' => 'Fuseau horaire', 'Timezone' => 'Fuseau horaire',
'Sorry, I didn\'t found this information in my database!' => 'Désolé, je n\'ai pas trouvé cette information dans ma base de données !', 'Sorry, I didn\'t found this information in my database!' => 'Désolé, je n\'ai pas trouvé cette information dans ma base de données !',
'Page not found' => 'Page introuvable', 'Page not found' => 'Page introuvable',
'Story Points' => 'Complexité', 'Complexity' => 'Complexité',
'limit' => 'limite', 'limit' => 'limite',
'Task limit' => 'Nombre maximum de tâches', 'Task limit' => 'Nombre maximum de tâches',
'This value must be greater than %d' => 'Cette valeur doit être plus grande que %d', 'This value must be greater than %d' => 'Cette valeur doit être plus grande que %d',
'Edit project access list' => 'Modifier l\'accès au projet', 'Edit project access list' => 'Modifier l\'accès au projet',
'Edit users access' => 'Modifier les utilisateurs autorisés', 'Edit users access' => 'Modifier les utilisateurs autorisés',
'Allow this user' => 'Autoriser cet utilisateur', 'Allow this user' => 'Autoriser cet utilisateur',
'Project access list for "%s"' => 'Liste des accès au projet « %s »',
'Only those users have access to this project:' => 'Seulement ces utilisateurs ont accès à ce projet :', 'Only those users have access to this project:' => 'Seulement ces utilisateurs ont accès à ce projet :',
'Don\'t forget that administrators have access to everything.' => 'N\'oubliez pas que les administrateurs ont accès à tout.', 'Don\'t forget that administrators have access to everything.' => 'N\'oubliez pas que les administrateurs ont accès à tout.',
'revoke' => 'révoquer', 'revoke' => 'révoquer',
@ -219,8 +212,8 @@ return array(
'm/d/Y' => 'd/m/Y', // Date format parsed with php 'm/d/Y' => 'd/m/Y', // Date format parsed with php
'month/day/year' => 'jour/mois/année', // Help shown to the user 'month/day/year' => 'jour/mois/année', // Help shown to the user
'Invalid date' => 'Date invalide', 'Invalid date' => 'Date invalide',
'Must be done before %B %e, %G' => 'Doit être fait avant le %d/%m/%Y', 'Must be done before %B %e, %Y' => 'Doit être fait avant le %d/%m/%Y',
'%B %e, %G' => '%d/%m/%Y', '%B %e, %Y' => '%d/%m/%Y',
'Automatic actions' => 'Actions automatisées', 'Automatic actions' => 'Actions automatisées',
'Your automatic action have been created successfully.' => 'Votre action automatisée a été ajouté avec succès.', 'Your automatic action have been created successfully.' => 'Votre action automatisée a été ajouté avec succès.',
'Unable to create your automatic action.' => 'Impossible de créer votre action automatisée.', 'Unable to create your automatic action.' => 'Impossible de créer votre action automatisée.',
@ -229,6 +222,7 @@ return array(
'Action removed successfully.' => 'Action supprimée avec succès.', 'Action removed successfully.' => 'Action supprimée avec succès.',
'Automatic actions for the project "%s"' => 'Actions automatisées pour le projet « %s »', 'Automatic actions for the project "%s"' => 'Actions automatisées pour le projet « %s »',
'Defined actions' => 'Actions définies', 'Defined actions' => 'Actions définies',
'Add an action' => 'Ajouter une action',
'Event name' => 'Nom de l\'événement', 'Event name' => 'Nom de l\'événement',
'Action name' => 'Nom de l\'action', 'Action name' => 'Nom de l\'action',
'Action parameters' => 'Paramètres de l\'action', 'Action parameters' => 'Paramètres de l\'action',
@ -279,7 +273,7 @@ return array(
'IP address' => 'Adresse IP', 'IP address' => 'Adresse IP',
'User agent' => 'Agent utilisateur', 'User agent' => 'Agent utilisateur',
'Persistent connections' => 'Connexions persistantes', 'Persistent connections' => 'Connexions persistantes',
'No session' => 'Aucune session', 'No session.' => 'Aucune session.',
'Expiration date' => 'Date d\'expiration', 'Expiration date' => 'Date d\'expiration',
'Remember Me' => 'Connexion automatique', 'Remember Me' => 'Connexion automatique',
'Creation date' => 'Date de création', 'Creation date' => 'Date de création',
@ -383,5 +377,130 @@ return array(
'Link my GitHub Account' => 'Lier mon compte Github', 'Link my GitHub Account' => 'Lier mon compte Github',
'Unlink my GitHub Account' => 'Ne plus utiliser mon compte Github', 'Unlink my GitHub Account' => 'Ne plus utiliser mon compte Github',
'Created by %s' => 'Créé par %s', 'Created by %s' => 'Créé par %s',
'Last modified on %B %e, %G at %k:%M %p' => 'Modifié le %d/%m/%Y à %H:%M', 'Last modified on %B %e, %Y at %k:%M %p' => 'Modifié le %d/%m/%Y à %H:%M',
'Tasks Export' => 'Exportation des tâches',
'Tasks exportation for "%s"' => 'Exportation des tâches pour « %s »',
'Start Date' => 'Date de début',
'End Date' => 'Date de fin',
'Execute' => 'Exécuter',
'Task Id' => 'Identifiant de la tâche',
'Creator' => 'Créateur',
'Modification date' => 'Date de modification',
'Completion date' => 'Date de complétion',
'Webhook URL for task creation' => 'URL du webhook pour la création de tâche',
'Webhook URL for task modification' => 'URL du webhook pour la modification de tâche',
'Clone' => 'Clone',
'Clone Project' => 'Cloner le projet',
'Project cloned successfully.' => 'Projet cloné avec succès.',
'Unable to clone this project.' => 'Impossible de cloner ce projet.',
'Email notifications' => 'Notifications par email',
'Enable email notifications' => 'Activer les notifications par emails',
'Task position:' => 'Position de la tâche :',
'The task #%d have been opened.' => 'La tâche #%d a été ouverte.',
'The task #%d have been closed.' => 'La tâche #%d a été fermée.',
'Sub-task updated' => 'Sous-tâche mise à jour',
'Title:' => 'Titre :',
'Status:' => 'État :',
'Assignee:' => 'Assigné :',
'Time tracking:' => 'Gestion du temps :',
'New sub-task' => 'Nouvelle sous-tâche',
'New attachment added "%s"' => 'Nouvelle pièce-jointe ajoutée « %s »',
'Comment updated' => 'Commentaire ajouté',
'New comment posted by %s' => 'Nouveau commentaire ajouté par « %s »',
'List of due tasks for the project "%s"' => 'Liste des tâches expirées pour le projet « %s »',
'[%s][New attachment] %s (#%d)' => '[%s][Pièce-jointe] %s (#%d)',
'[%s][New comment] %s (#%d)' => '[%s][Nouveau commentaire] %s (#%d)',
'[%s][Comment updated] %s (#%d)' => '[%s][Commentaire mis à jour] %s (#%d)',
'[%s][New subtask] %s (#%d)' => '[%s][Nouvelle sous-tâche] %s (#%d)',
'[%s][Subtask updated] %s (#%d)' => '[%s][Sous-tâche mise à jour] %s (#%d)',
'[%s][New task] %s (#%d)' => '[%s][Nouvelle tâche] %s (#%d)',
'[%s][Task updated] %s (#%d)' => '[%s][Tâche mise à jour] %s (#%d)',
'[%s][Task closed] %s (#%d)' => '[%s][Tâche fermée] %s (#%d)',
'[%s][Task opened] %s (#%d)' => '[%s][Tâche ouverte] %s (#%d)',
'[%s][Due tasks]' => '[%s][Tâches expirées]',
'[Kanboard] Notification' => '[Kanboard] Notification',
'I want to receive notifications only for those projects:' => 'Je souhaite reçevoir les notifications uniquement pour les projets sélectionnés :',
'view the task on Kanboard' => 'voir la tâche sur Kanboard',
'Public access' => 'Accès public',
'Categories management' => 'Gestion des catégories',
'Users management' => 'Gestion des utilisateurs',
'Active tasks' => 'Tâches actives',
'Disable public access' => 'Désactiver l\'accès public',
'Enable public access' => 'Activer l\'accès public',
'Active projects' => 'Projets activés',
'Inactive projects' => 'Projets désactivés',
'Public access disabled' => 'Accès public désactivé',
'Do you really want to disable this project: "%s"?' => 'Voulez-vous vraiment désactiver ce projet : « %s » ?',
'Do you really want to duplicate this project: "%s"?' => 'Voulez-vous vraiment dupliquer ce projet : « %s » ?',
'Do you really want to enable this project: "%s"?' => 'Voulez-vous vraiment activer ce projet : « %s » ?',
'Project activation' => 'Activation du projet',
'Move the task to another project' => 'Déplacer la tâche vers un autre projet',
'Move to another project' => 'Déplacer vers un autre projet',
'Do you really want to duplicate this task?' => 'Voulez-vous vraiment dupliquer cette tâche ?',
'Duplicate a task' => 'Dupliquer une tâche',
'External accounts' => 'Comptes externes',
'Account type' => 'Type de compte',
'Local' => 'Local',
'Remote' => 'Distant',
'Enabled' => 'Activé',
'Disabled' => 'Désactivé',
'Google account linked' => 'Compte Google attaché',
'Github account linked' => 'Compte Github attaché',
'Username:' => 'Nom d\'utilisateur :',
'Name:' => 'Nom :',
'Email:' => 'Email :',
'Default project:' => 'Projet par défaut :',
'Notifications:' => 'Notifications :',
'Group:' => 'Groupe :',
'Regular user' => 'Utilisateur normal',
'Account type:' => 'Type de compte :',
'Edit profile' => 'Modifier le profile',
'Change password' => 'Changer le mot de passe',
'Password modification' => 'Changement de mot de passe',
'External authentications' => 'Authentifications externe',
'Google Account' => 'Compte Google',
'Github Account' => 'Compte Github',
'Never connected.' => 'Jamais connecté.',
'No account linked.' => 'Aucun compte attaché.',
'Account linked.' => 'Compte attaché.',
'No external authentication enabled.' => 'Aucune authentication externe activée.',
'Password modified successfully.' => 'Mot de passe changé avec succès.',
'Unable to change the password.' => 'Impossible de changer le mot de passe.',
'Change category for the task "%s"' => 'Changer la catégorie pour la tâche « %s »',
'Change category' => 'Changer de catégorie',
'%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a mis à jour la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
'%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a ouvert la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
'%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s a déplacé la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a> à la position n°%d dans la colonne « %s »',
'%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '%s a déplacé la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a> dans la colonne « %s »',
'%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a créé la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
'%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a fermé la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
'%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a créé une sous-tâche pour la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
'%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a mis à jour une sous-tâche appartenant à la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
'Assigned to %s with an estimate of %s/%sh' => 'Assigné à %s avec un estimé de %s/%sh',
'Not assigned, estimate of %sh' => 'Personne assigné, estimé de %sh',
'%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a mis à jour un commentaire appartenant à la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
'%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a ajouté un commentaire sur la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
'%s\'s activity' => 'Activité du projet %s',
'No activity.' => 'Aucune activité.',
'RSS feed' => 'Flux RSS',
'%s updated a comment on the task #%d' => '%s a mis à jour un commentaire sur la tâche n°%d',
'%s commented on the task #%d' => '%s a ajouté un commentaire sur la tâche n°%d',
'%s updated a subtask for the task #%d' => '%s a mis à jour une sous-tâche appartenant à la tâche n°%d',
'%s created a subtask for the task #%d' => '%s a créé une sous-tâche pour la tâche n°%d',
'%s updated the task #%d' => '%s a mis à jour la tâche n°%d',
'%s created the task #%d' => '%s a créé la tâche n°%d',
'%s closed the task #%d' => '%s a fermé la tâche n°%d',
'%s open the task #%d' => '%s a ouvert la tâche n°%d',
'%s moved the task #%d to the column "%s"' => '%s a déplacé la tâche n°%d dans la colonne « %s »',
'%s moved the task #%d to the position %d in the column "%s"' => '%s a déplacé la tâche n°%d à la position n°%d dans la colonne « %s »',
'Activity' => 'Activité',
'Default values are "%s"' => 'Les valeurs par défaut sont « %s »',
'Default columns for new projects (Comma-separated)' => 'Colonnes par défaut pour les nouveaux projets (séparé par des virgules)',
'Task assignee change' => 'Modification de la personne assignée sur une tâche',
'%s change the assignee of the task #%d' => '%s a changé la personne assignée sur la tâche #%d',
'%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a changé la personne assignée sur la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
'[%s][Column Change] %s (#%d)' => '[%s][Changement de colonne] %s (#%d)',
'[%s][Position Change] %s (#%d)' => '[%s][Changement de position] %s (#%d)',
'[%s][Assignee Change] %s (#%d)' => '[%s][Changement d\'assigné] %s (#%d)',
'New password for the user "%s"' => 'Nouveau mot de passe pour l\'utilisateur « %s »',
); );

View file

@ -0,0 +1,506 @@
<?php
return array(
'None' => 'Nessuno',
'edit' => 'modificare',
'Edit' => 'Modificare',
'remove' => 'cancellare',
'Remove' => 'Cancellare',
'Update' => 'Aggiornare',
'Yes' => 'Si',
'No' => 'No',
'cancel' => 'annullare',
'or' => 'o',
'Yellow' => 'Giallo',
'Blue' => 'Blu',
'Green' => 'Verde',
'Purple' => 'Porpora',
'Red' => 'Rosso',
'Orange' => 'Arancione',
'Grey' => 'Grigio',
'Save' => 'Salvare',
'Login' => 'Entra',
'Official website:' => 'Sito web ufficiale :',
'Unassigned' => 'Non assegnato',
'View this task' => 'Vedere questo compito',
'Remove user' => 'Cancellare un utente',
'Do you really want to remove this user: "%s"?' => 'Veramente vuoi cancellare questo utente: « %s » ?',
'New user' => 'Aggiungere un utente',
'All users' => 'Tutti gli utenti',
'Username' => 'Nome utente',
'Password' => 'Password',
'Default project' => 'Progetto predefinito',
'Administrator' => 'Amministratore',
'Sign in' => 'Iscriversi',
'Users' => 'Utenti',
'No user' => 'Nessun utente',
'Forbidden' => 'Vietato',
'Access Forbidden' => 'Accesso vietato',
'Only administrators can access to this page.' => 'Solo gli amministratori possono accedere a questa pagina.',
'Edit user' => 'Modificare un utente',
'Logout' => 'Uscire',
'Bad username or password' => 'Utente o password sbagliato',
'users' => 'utenti',
'projects' => 'progetti',
'Edit project' => 'Modificare progetto',
'Name' => 'Nome',
'Activated' => 'Attivo',
'Projects' => 'Progetti',
'No project' => 'Nessun progetto',
'Project' => 'Progetto',
'Status' => 'Stato',
'Tasks' => 'Compiti',
'Board' => 'Bacheca',
// 'Actions' => '',
'Inactive' => 'Inattivo',
'Active' => 'Attivo',
'Column %d' => 'Colonna %d',
'Add this column' => 'Aggiungere questa colonna',
'%d tasks on the board' => '%d compiti sulla bacheca',
'%d tasks in total' => '%d compiti in totale',
'Unable to update this board.' => 'Non si può aggiornare questa bacheca.',
'Edit board' => 'Modificare questa bacheca',
'Disable' => 'Disattivare',
'Enable' => 'Attivare',
'New project' => 'Nuovo progetto',
'Do you really want to remove this project: "%s"?' => 'Vuoi veramente eliminare questo progetto: « %s » ?',
'Remove project' => 'Cancellare il progetto',
'Boards' => 'Bacheche',
'Edit the board for "%s"' => 'Modificare la bacheca per « %s »',
'All projects' => 'Tutti i progetti',
'Change columns' => 'Cambiare le colonne',
'Add a new column' => 'Aggiungere una nuova colonna',
'Title' => 'Titolo',
'Add Column' => 'Aggiungere colonna',
'Project "%s"' => 'progetto « %s »',
'Nobody assigned' => 'Nessuno assegnato',
'Assigned to %s' => 'Assegnato a %s',
'Remove a column' => 'Cancellare questa colonna',
'Remove a column from a board' => 'Cancellare una colonna di una bacheca',
'Unable to remove this column.' => 'Non si può cancellare questa colonna.',
'Do you really want to remove this column: "%s"?' => 'Veramente desideri cancellare questa colonna : « %s » ?',
'This action will REMOVE ALL TASKS associated to this column!' => 'Questa azione cancellerà TUTTI I COMPITI legati a questa colonna!',
'Settings' => 'Impostazioni',
'Application settings' => 'Impostazioni dell\'applicazione',
'Language' => 'Lingua',
'Webhooks token:' => 'Identificatore (token) per i webhooks :',
// 'API token:' => '',
'More information' => 'Più informazione',
'Database size:' => 'Dimensioni della base dati:',
'Download the database' => 'Scaricare la base dati',
'Optimize the database' => 'Ottimizare la base dati',
'(VACUUM command)' => '(Comando VACUUM)',
'(Gzip compressed Sqlite file)' => '(File Sqlite compresso in Gzip)',
'User settings' => 'Impostazioni di utente',
'My default project:' => 'Il mio progetto predefinito: ',
'Close a task' => 'Chiudere un compito',
'Do you really want to close this task: "%s"?' => 'Veramente desidera chiudere questo compito: « %s » ?',
'Edit a task' => 'Modificare un compito',
'Column' => 'colonna',
// 'Color' => '',
'Assignee' => 'Persona assegnata',
'Create another task' => 'Creare un nuovo compito',
'New task' => 'Nuovo compito',
'Open a task' => 'Aprire un compito',
'Do you really want to open this task: "%s"?' => 'Veramente desidera aprire questo compito: « %s » ?',
'Back to the board' => 'Tornare alla bacheca',
// 'Created on %B %e, %Y at %k:%M %p' => '',
'There is nobody assigned' => 'Non c\'è nessuno assegnato a questo compito',
'Column on the board:' => 'Colonna sulla bacheca: ',
'Status is open' => 'Stato aperto',
'Status is closed' => 'stato chiuso',
'Close this task' => 'Chiudere questo compito',
'Open this task' => 'Aprire questo compito',
'There is no description.' => 'Non c\'è descrizione.',
'Add a new task' => 'Aggiungere un nuovo compito',
'The username is required' => 'Si richiede un nome di utente',
'The maximum length is %d characters' => 'La lunghezza massima è di %d caratteri',
'The minimum length is %d characters' => 'La lunghezza minima è di %d caratteri',
'The password is required' => 'Si richiede una password',
'This value must be an integer' => 'questo valore deve essere un intero',
'The username must be unique' => 'Il nome di utente deve essere unico',
'The username must be alphanumeric' => 'Il nome di utente deve essere alfanumerico',
'The user id is required' => 'Si richiede l\'identificatore dell\'utente',
// 'Passwords don\'t match' => '',
'The confirmation is required' => 'Si richiede una conferma',
'The column is required' => 'Si richiede una colonna',
'The project is required' => 'Si richiede il progetto',
'The color is required' => 'Si richiede il colore',
'The id is required' => 'Si richiede l\'identificatore',
'The project id is required' => 'Si richiede l\'identificatore del progetto',
'The project name is required' => 'Si richiede il nome del progetto',
'This project must be unique' => 'Il nome del progetto deve essere unico',
'The title is required' => 'Si richiede un titolo',
'The language is required' => 'Si richiede una lingua',
'There is no active project, the first step is to create a new project.' => 'Non ci sono progetti attivi, il primo passo consiste in creare un nuovo progetto.',
'Settings saved successfully.' => 'Impostazioni salvate correttamente.',
'Unable to save your settings.' => 'Non si possono salvare gli impostazioni.',
'Database optimization done.' => 'Ottimizzazione della base dati conclusa.',
'Your project have been created successfully.' => 'Il suo progetto è stato creato correttamente.',
'Unable to create your project.' => 'Non si può creare il progetto.',
'Project updated successfully.' => 'Progetto aggiornato correttamente.',
'Unable to update this project.' => 'Non si può aggiornare il progetto.',
'Unable to remove this project.' => 'Non si può cancellare questo progetto.',
'Project removed successfully.' => 'Progetto cancellato correttamente.',
'Project activated successfully.' => 'Progetto attivato correttamente.',
'Unable to activate this project.' => 'Non si può attivare il progetto.',
'Project disabled successfully.' => 'Progetto disattivato correttamente.',
'Unable to disable this project.' => 'Non si può disattivare il progetto.',
'Unable to open this task.' => 'Non si può aprire questo compito.',
'Task opened successfully.' => 'Il compito è stato aperto correttamente.',
'Unable to close this task.' => 'Non si può chiudere questo compito.',
'Task closed successfully.' => 'Compito chiuso correttamente.',
'Unable to update your task.' => 'Non si può modificare questo compito.',
'Task updated successfully.' => 'Compito modificato correttamente.',
'Unable to create your task.' => 'Non si può creare questo compito.',
'Task created successfully.' => 'Compito creato correttamente.',
'User created successfully.' => 'Utente creato correttamente.',
'Unable to create your user.' => 'Non si può creare l\'utente.',
'User updated successfully.' => 'Utente aggiornato correttamente.',
'Unable to update your user.' => 'Non si può aggiornare questo utente.',
'User removed successfully.' => 'Utente cancellato correttamente.',
'Unable to remove this user.' => 'Non si può cancellare questo utente.',
'Board updated successfully.' => 'Bacheca aggiornata correttamente.',
'Ready' => 'Pronto',
'Backlog' => 'In attesa',
'Work in progress' => 'In corso',
'Done' => 'Fatto',
'Application version:' => 'Versione dell\'applicazione:',
// 'Completed on %B %e, %Y at %k:%M %p' => '',
// '%B %e, %Y at %k:%M %p' => '',
'Date created' => 'Data di creazione',
'Date completed' => 'Data di termine',
'Id' => 'Identificatore',
'No task' => 'Nessun compito',
'Completed tasks' => 'Compiti fatti',
'List of projects' => 'Lista di progetti',
'Completed tasks for "%s"' => 'Compiti fatti da « %s »',
'%d closed tasks' => '%d compiti chiusi',
'No task for this project' => 'Nessun compito per questo progetto',
'Public link' => 'Link pubblico',
'There is no column in your project!' => 'Non c\'è nessuna colonna per questo progetto!',
'Change assignee' => 'Cambiare la persona assegnata',
'Change assignee for the task "%s"' => 'Cambiare la persona assegnata per il compito « %s »',
'Timezone' => 'Fuso orario',
'Sorry, I didn\'t found this information in my database!' => 'Mi dispiace, non ho trovato questa informazione sulla base dati!',
'Page not found' => 'Página non trovata',
// 'Complexity' => '',
'limit' => 'limite',
'Task limit' => 'Numero massimo di compiti',
'This value must be greater than %d' => 'questo valore deve essere maggiore di %d',
'Edit project access list' => 'Modificare i permessi del progetto',
'Edit users access' => 'Modificare i permessi degli utenti',
'Allow this user' => 'Permettere a questo utente',
'Only those users have access to this project:' => 'Solo questi utenti hanno accesso a questo progetto:',
'Don\'t forget that administrators have access to everything.' => 'Non dimenticare che gli amministratori hanno accesso a tutto.',
'revoke' => 'revocare',
'List of authorized users' => 'Lista di utenti autorizzati',
'User' => 'Utente',
'Everybody have access to this project.' => 'Tutti hanno accesso a questo progetto.',
'You are not allowed to access to this project.' => 'Non hai l\'accesso a questo progetto.',
'Comments' => 'Commenti',
'Post comment' => 'Mandare commento',
'Write your text in Markdown' => 'Scrivi il testo in Markdown',
'Leave a comment' => 'Lasciare un commento',
'Comment is required' => 'Si richiede un commento',
'Leave a description' => 'Lasciare una descrizione',
'Comment added successfully.' => 'Commenti aggiunti correttamente.',
'Unable to create your comment.' => 'Non si può creare questo commento.',
'The description is required' => 'Si richiede una descrizione',
'Edit this task' => 'Modificare questo compito',
'Due Date' => 'Data di scadenza',
'm/d/Y' => 'd/m/Y',
'month/day/year' => 'giorno/mese/anno',
'Invalid date' => 'Data sbagliata',
// 'Must be done before %B %e, %Y' => '',
// '%B %e, %Y' => '',
'Automatic actions' => 'Azioni automatiche',
'Your automatic action have been created successfully.' => 'l\'azione automatica è stata creata correttamente.',
'Unable to create your automatic action.' => 'Non si può creare quest\'azione automatica.',
'Remove an action' => 'Cancellare un\'azione',
'Unable to remove this action.' => 'Non si può cancellare questa azione.',
'Action removed successfully.' => 'Azione cancellata correttamente.',
'Automatic actions for the project "%s"' => 'Azioni automatiche per questo progetto « %s »',
'Defined actions' => 'Azioni definite',
// 'Add an action' => '',
'Event name' => 'Nome dell\'evento',
'Action name' => 'Nome dell\'azione',
'Action parameters' => 'Parametri d\'azione',
'Action' => 'Azione',
'Event' => 'Evento',
'When the selected event occurs execute the corresponding action.' => 'Quando accade l\'evento selezionato, eseguire l\'azione corrispondente.',
'Next step' => 'Passo seguente',
'Define action parameters' => 'Definire i parametri dell\'azione',
'Save this action' => 'Salvare questa azione',
'Do you really want to remove this action: "%s"?' => 'Veramente vuole cancellare questa azione « %s » ?',
'Remove an automatic action' => 'Cancellare un\'azione automatica',
'Close the task' => 'Chiudere questo compito',
'Assign the task to a specific user' => 'Assegnare questo compito a un utente specifico',
'Assign the task to the person who does the action' => 'Assegnare il compito all\'utente che svolge l\'azione',
'Duplicate the task to another project' => 'Duplicare il compito in altro progetto',
'Move a task to another column' => 'Muovere un compito ad un altra colonna',
'Move a task to another position in the same column' => 'Muovere un compito ad altra posizione sulla stessa colonna',
'Task modification' => 'Modifica di un compito',
'Task creation' => 'Creazione di un compito',
'Open a closed task' => 'Riaprire un compito',
'Closing a task' => 'Chiudere un compito',
// 'Assign a color to a specific user' => '',
'Column title' => 'Titolo della colonna',
'Position' => 'Posizione',
'Move Up' => 'Alzare',
'Move Down' => 'Abassare',
'Duplicate to another project' => 'Duplicare in un altro progetto',
'Duplicate' => 'Duplicare',
'link' => 'link',
'Update this comment' => 'Aggiornare questo commento',
'Comment updated successfully.' => 'Commento aggiornato correttamente.',
'Unable to update your comment.' => 'Non si può aggiornare questo commento.',
'Remove a comment' => 'Cancellare un commento',
'Comment removed successfully.' => 'Commento cancellato correttamente.',
'Unable to remove this comment.' => 'Non si può cancellare questo commento.',
'Do you really want to remove this comment?' => 'Desidera cancellare questo commento?',
'Only administrators or the creator of the comment can access to this page.' => 'Solo gli amministratori o l\'autore del commento hanno accesso a questa pagina.',
'Details' => 'Dettagli',
'Current password for the user "%s"' => 'Password attuale per l\'utente: « %s »',
'The current password is required' => 'Si richiede la password attuale',
'Wrong password' => 'password sbagliata',
'Reset all tokens' => 'Azzerare gli identificatori (tokens) di sicurezza ',
'All tokens have been regenerated.' => 'Tutti gli identificatori (tokens) sono stati rigenerati.',
'Unknown' => 'Sconociuto',
'Last logins' => 'Ultimi ingressi',
'Login date' => 'Data di ingresso',
'Authentication method' => 'Metodo di autenticazzione',
'IP address' => 'Indirizzo IP',
'User agent' => 'Navigatore',
'Persistent connections' => 'Conessioni persistenti',
'No session.' => 'Non essiste sessione.',
'Expiration date' => 'Data di scadenza',
'Remember Me' => 'Riccordami',
'Creation date' => 'Data di creazione',
'Filter by user' => 'Filtrado mediante utente',
'Filter by due date' => 'Filtrare attraverso data di scadenza',
'Everybody' => 'Tutti',
'Open' => 'Aperto',
'Closed' => 'Chiuso',
'Search' => 'Cercare',
'Nothing found.' => 'Non si è trovato nulla.',
'Search in the project "%s"' => 'Cercare sul progetto "%s"',
'Due date' => 'Data di scadenza',
'Others formats accepted: %s and %s' => 'Altri formati accettati: %s y %s',
'Description' => 'Descrizione',
'%d comments' => '%d commenti',
'%d comment' => '%d commento',
'Email address invalid' => 'Indirizzo e-mail sbagliato',
'Your Google Account is not linked anymore to your profile.' => 'Il suo account Google non i più collegato col suo profilo',
'Unable to unlink your Google Account.' => 'Non si può svincolare l\'account di Google.',
'Google authentication failed' => 'Non si è riuscito ad ingressare su Google',
'Unable to link your Google Account.' => 'Non si può collegare con il suo account di Google.',
'Your Google Account is linked to your profile successfully.' => 'Il suo account di Google è stato collegato correttamente al suo profilo.',
'Email' => 'E-mail',
'Link my Google Account' => 'Collegare con il mio Account di Google',
'Unlink my Google Account' => 'Svincolare con il mio account di Google',
'Login with my Google Account' => 'Ingressa con il mio Account di Google',
'Project not found.' => 'progetto non trovato.',
'Task #%d' => 'Compito numero %d',
'Task removed successfully.' => 'Compito cancellato correttamente.',
'Unable to remove this task.' => 'Non si può cancellare questo compito.',
'Remove a task' => 'Cancellare un compito',
'Do you really want to remove this task: "%s"?' => 'Veramente vuoi cancellare questo compito: "%s"?',
'Assign automatically a color based on a category' => 'Assegnare un colore in modo automatico basandosi sulla categoria',
'Assign automatically a category based on a color' => 'Assegnare una categoria in modo automatico basandosi sul colore',
'Task creation or modification' => 'Creazione o Modifica di compito',
'Category' => 'Categoria',
'Category:' => 'Categoria:',
'Categories' => 'Categorie',
'Category not found.' => 'Categoria non trovata.',
'Your category have been created successfully.' => 'La sua categoria è stata creata correttamente.',
'Unable to create your category.' => 'Non si può creare la sua categoria.',
'Your category have been updated successfully.' => 'La sua categoria è stata aggiornata correttamente.',
'Unable to update your category.' => 'Non si può aggiornare la sua categoria.',
'Remove a category' => 'Cancellare una categoria',
'Category removed successfully.' => 'Categoria cancellata correttamente.',
'Unable to remove this category.' => 'Non si può cancellare questa categoria.',
'Category modification for the project "%s"' => 'Modifica di categoria per il progetto "%s"',
'Category Name' => 'Nome di categoria',
'Categories for the project "%s"' => 'Categorie per il progetto',
'Add a new category' => 'Aggiungere una nuova categoria',
'Do you really want to remove this category: "%s"?' => 'Vuoi veramente cancellare questa categoria: "%s"?',
'Filter by category' => 'Filtrare attraverso categoria',
'All categories' => 'Tutte le categorie',
'No category' => 'Senza categoria',
'The name is required' => 'Si richiede un nome',
'Remove a file' => 'Cancellare un file',
'Unable to remove this file.' => 'Non si può cancellare questo file.',
'File removed successfully.' => 'File cancellato correttamente.',
'Attach a document' => 'Allegare un documento',
'Do you really want to remove this file: "%s"?' => 'Vuoi veramente cancellare questo file: "%s"?',
'open' => 'aprire',
'Attachments' => 'Allegati',
'Edit the task' => 'Modificare il compito',
'Edit the description' => 'Modificare la descrizione',
'Add a comment' => 'Aggiungere un commento',
'Edit a comment' => 'Modificare un commento',
'Summary' => 'Sommario',
'Time tracking' => 'Time tracking',
'Estimate:' => 'Stimato:',
'Spent:' => 'Trascorso:',
'Do you really want to remove this sub-task?' => 'Vuoi veramente cancellare questo sub-compito?',
'Remaining:' => 'Rimangono',
'hours' => 'ore',
'spent' => 'trascorse',
'estimated' => 'stimate',
'Sub-Tasks' => 'Sub-Compiti',
'Add a sub-task' => 'Aggiungere un sub-compito',
'Original Estimate' => 'Stima originale',
'Create another sub-task' => 'Crear un altro sub-compito',
'Time Spent' => 'Tempo Trascorso',
'Edit a sub-task' => 'Modificare un sub-compito',
'Remove a sub-task' => 'Cancellare un sub-compito',
'The time must be a numeric value' => 'Il tempo deve essere un valore numerico',
'Todo' => 'Da fare',
'In progress' => 'In corso',
'Sub-task removed successfully.' => 'Sub-compito cancellato correttamente.',
'Unable to remove this sub-task.' => 'Non si può cancellare questo sub-compito.',
'Sub-task updated successfully.' => 'Sub-compito aggiornato correttamente.',
'Unable to update your sub-task.' => 'Non si può aggiornare il suo sub-compito.',
'Unable to create your sub-task.' => 'Non si può creare il suo sub-compito.',
'Sub-task added successfully.' => 'Sub-compito aggiunto correttamente.',
'Maximum size: ' => 'Dimensioni massime',
'Unable to upload the file.' => 'Non si può caricare il file.',
'Display another project' => 'Mostrare un altro progetto',
'Your GitHub account was successfully linked to your profile.' => 'Il suo account di Github è stato collegato correttamente col suo profilo.',
'Unable to link your GitHub Account.' => 'Non si può collegarre col suo account di Github.',
'GitHub authentication failed' => 'L\'autenticazione non è stata possibile',
'Your GitHub account is no longer linked to your profile.' => 'Il suo account di Github non è più vincolato al suo profilo.',
'Unable to unlink your GitHub Account.' => 'Non si può svincolare il suo account di Github.',
'Login with my GitHub Account' => 'Ingressare col suo account di Github',
'Link my GitHub Account' => 'Lier mon compte Github',
'Unlink my GitHub Account' => 'Non impiegare più l\'account di Github',
'Created by %s' => 'Creato da %s',
'Last modified on %B %e, %Y at %k:%M %p' => 'Ultima modifica il %d/%m/%Y alle %H:%M',
'Tasks Export' => 'Esportazione di compiti',
'Tasks exportation for "%s"' => 'Esportazione di compiti per « %s »',
'Start Date' => 'Data d\'inizio',
'End Date' => 'Data di fine',
'Execute' => 'Essecutare',
'Task Id' => 'Identificatore del compito',
'Creator' => 'Creatore',
'Modification date' => 'Data di modifica',
'Completion date' => 'Data di termine',
// 'Webhook URL for task creation' => '',
// 'Webhook URL for task modification' => '',
// 'Clone' => '',
// 'Clone Project' => '',
// 'Project cloned successfully.' => '',
// 'Unable to clone this project.' => '',
// 'Email notifications' => '',
// 'Enable email notifications' => '',
// 'Task position:' => '',
// 'The task #%d have been opened.' => '',
// 'The task #%d have been closed.' => '',
// 'Sub-task updated' => '',
// 'Title:' => '',
// 'Status:' => '',
// 'Assignee:' => '',
// 'Time tracking:' => '',
// 'New sub-task' => '',
'New attachment added "%s"' => 'Nuovo allegato aggiunto « %s »',
'Comment updated' => 'Commento aggiornato',
'New comment posted by %s' => 'Nuovo commento aggiunto da « %s »',
'List of due tasks for the project "%s"' => 'Lista dei compiti scaduti per il progetto « %s »',
'[%s][New attachment] %s (#%d)' => '[%s][Nuovo allegato] %s (#%d)',
'[%s][New comment] %s (#%d)' => '[%s][Nuovo commento] %s (#%d)',
'[%s][Comment updated] %s (#%d)' => '[%s][Commento aggiornato] %s (#%d)',
'[%s][New subtask] %s (#%d)' => '[%s][Nuovo sub-compito] %s (#%d)',
'[%s][Subtask updated] %s (#%d)' => '[%s][Sub-compito aggiornato] %s (#%d)',
'[%s][New task] %s (#%d)' => '[%s][Nuovo compito] %s (#%d)',
'[%s][Task updated] %s (#%d)' => '[%s][Compito aggiornato] %s (#%d)',
'[%s][Task closed] %s (#%d)' => '[%s][Compito chiuso] %s (#%d)',
'[%s][Task opened] %s (#%d)' => '[%s][Compito aperto] %s (#%d)',
'[%s][Due tasks]' => '[%s][Compiti scaduti]',
'[Kanboard] Notification' => '[Kanboard] Notification',
'I want to receive notifications only for those projects:' => 'Vorrei ricevere le notifiche solo da questi progetti:',
'view the task on Kanboard' => 'vedi il compito su Kanboard',
// 'Public access' => '',
// 'Categories management' => '',
// 'Users management' => '',
// 'Active tasks' => '',
// 'Disable public access' => '',
// 'Enable public access' => '',
// 'Active projects' => '',
// 'Inactive projects' => '',
// 'Public access disabled' => '',
// 'Do you really want to disable this project: "%s"?' => '',
// 'Do you really want to duplicate this project: "%s"?' => '',
// 'Do you really want to enable this project: "%s"?' => '',
// 'Project activation' => '',
// 'Move the task to another project' => '',
// 'Move to another project' => '',
// 'Do you really want to duplicate this task?' => '',
// 'Duplicate a task' => '',
// 'External accounts' => '',
// 'Account type' => '',
// 'Local' => '',
// 'Remote' => '',
// 'Enabled' => '',
// 'Disabled' => '',
// 'Google account linked' => '',
// 'Github account linked' => '',
// 'Username:' => '',
// 'Name:' => '',
// 'Email:' => '',
// 'Default project:' => '',
// 'Notifications:' => '',
// 'Group:' => '',
// 'Regular user' => '',
// 'Account type:' => '',
// 'Edit profile' => '',
// 'Change password' => '',
// 'Password modification' => '',
// 'External authentications' => '',
// 'Google Account' => '',
// 'Github Account' => '',
// 'Never connected.' => '',
// 'No account linked.' => '',
// 'Account linked.' => '',
// 'No external authentication enabled.' => '',
// 'Password modified successfully.' => '',
// 'Unable to change the password.' => '',
// 'Change category for the task "%s"' => '',
// 'Change category' => '',
// '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '',
// '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '',
// '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// 'Assigned to %s with an estimate of %s/%sh' => '',
// 'Not assigned, estimate of %sh' => '',
// '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s\'s activity' => '',
// 'No activity.' => '',
// 'RSS feed' => '',
// '%s updated a comment on the task #%d' => '',
// '%s commented on the task #%d' => '',
// '%s updated a subtask for the task #%d' => '',
// '%s created a subtask for the task #%d' => '',
// '%s updated the task #%d' => '',
// '%s created the task #%d' => '',
// '%s closed the task #%d' => '',
// '%s open the task #%d' => '',
// '%s moved the task #%d to the column "%s"' => '',
// '%s moved the task #%d to the position %d in the column "%s"' => '',
// 'Activity' => '',
// 'Default values are "%s"' => '',
// 'Default columns for new projects (Comma-separated)' => '',
// 'Task assignee change' => '',
// '%s change the assignee of the task #%d' => '',
// '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '[%s][Column Change] %s (#%d)' => '',
// '[%s][Position Change] %s (#%d)' => '',
// '[%s][Assignee Change] %s (#%d)' => '',
// 'New password for the user "%s"' => '',
);

View file

@ -1,13 +1,6 @@
<?php <?php
return array( return array(
'English' => 'angielski',
'French' => 'francuski',
'Polish' => 'polski',
'Portuguese (Brazilian)' => 'Portugalski (brazylijski)',
'Spanish' => 'Hiszpański',
// 'German' => '',
// 'Chinese (Simplified)' => '',
'None' => 'Brak', 'None' => 'Brak',
'edit' => 'edytuj', 'edit' => 'edytuj',
'Edit' => 'Edytuj', 'Edit' => 'Edytuj',
@ -36,7 +29,7 @@ return array(
'All users' => 'Wszyscy użytkownicy', 'All users' => 'Wszyscy użytkownicy',
'Username' => 'Nazwa użytkownika', 'Username' => 'Nazwa użytkownika',
'Password' => 'Hasło', 'Password' => 'Hasło',
'Default Project' => 'Domyślny projekt', 'Default project' => 'Domyślny projekt',
'Administrator' => 'Administrator', 'Administrator' => 'Administrator',
'Sign in' => 'Zaloguj', 'Sign in' => 'Zaloguj',
'Users' => 'Użytkownicy', 'Users' => 'Użytkownicy',
@ -58,6 +51,7 @@ return array(
'Status' => 'Status', 'Status' => 'Status',
'Tasks' => 'Zadania', 'Tasks' => 'Zadania',
'Board' => 'Tablica', 'Board' => 'Tablica',
'Actions' => 'Akcje',
'Inactive' => 'Nieaktywny', 'Inactive' => 'Nieaktywny',
'Active' => 'Aktywny', 'Active' => 'Aktywny',
'Column %d' => 'Kolumna %d', 'Column %d' => 'Kolumna %d',
@ -90,6 +84,7 @@ return array(
'Application settings' => 'Ustawienia aplikacji', 'Application settings' => 'Ustawienia aplikacji',
'Language' => 'Język', 'Language' => 'Język',
'Webhooks token:' => 'Token :', 'Webhooks token:' => 'Token :',
// 'API token:' => '',
'More information' => 'Więcej informacji', 'More information' => 'Więcej informacji',
'Database size:' => 'Rozmiar bazy danych :', 'Database size:' => 'Rozmiar bazy danych :',
'Download the database' => 'Pobierz bazę danych', 'Download the database' => 'Pobierz bazę danych',
@ -109,7 +104,7 @@ return array(
'Open a task' => 'Otwórz zadanie', 'Open a task' => 'Otwórz zadanie',
'Do you really want to open this task: "%s"?' => 'Na pewno chcesz otworzyć zadanie: "%s"?', 'Do you really want to open this task: "%s"?' => 'Na pewno chcesz otworzyć zadanie: "%s"?',
'Back to the board' => 'Powrót do tablicy', 'Back to the board' => 'Powrót do tablicy',
'Created on %B %e, %G at %k:%M %p' => 'Utworzono dnia %e %B %G o %k:%M', 'Created on %B %e, %Y at %k:%M %p' => 'Utworzono dnia %e %B %Y o %k:%M',
'There is nobody assigned' => 'Nikt nie jest przypisany', 'There is nobody assigned' => 'Nikt nie jest przypisany',
'Column on the board:' => 'Kolumna na tablicy:', 'Column on the board:' => 'Kolumna na tablicy:',
'Status is open' => 'Status otwarty', 'Status is open' => 'Status otwarty',
@ -171,8 +166,8 @@ return array(
'Work in progress' => 'W trakcie', 'Work in progress' => 'W trakcie',
'Done' => 'Zakończone', 'Done' => 'Zakończone',
'Application version:' => 'Wersja aplikacji:', 'Application version:' => 'Wersja aplikacji:',
'Completed on %B %e, %G at %k:%M %p' => 'Zakończono dnia %e %B %G o %k:%M', 'Completed on %B %e, %Y at %k:%M %p' => 'Zakończono dnia %e %B %Y o %k:%M',
'%B %e, %G at %k:%M %p' => '%e %B %G o %k:%M', '%B %e, %Y at %k:%M %p' => '%e %B %Y o %k:%M',
'Date created' => 'Data utworzenia', 'Date created' => 'Data utworzenia',
'Date completed' => 'Data zakończenia', 'Date completed' => 'Data zakończenia',
'Id' => 'Ident', 'Id' => 'Ident',
@ -181,25 +176,21 @@ return array(
'List of projects' => 'Lista projektów', 'List of projects' => 'Lista projektów',
'Completed tasks for "%s"' => 'Zadania zakończone dla "%s"', 'Completed tasks for "%s"' => 'Zadania zakończone dla "%s"',
'%d closed tasks' => '%d zamkniętych zadań', '%d closed tasks' => '%d zamkniętych zadań',
'no task for this project' => 'brak zadań dla tego projektu', 'No task for this project' => 'Brak zadań dla tego projektu',
'Public link' => 'Link publiczny', 'Public link' => 'Link publiczny',
'There is no column in your project!' => 'Brak kolumny w Twoim projekcie', 'There is no column in your project!' => 'Brak kolumny w Twoim projekcie',
'Change assignee' => 'Zmień odpowiedzialną osobę', 'Change assignee' => 'Zmień odpowiedzialną osobę',
'Change assignee for the task "%s"' => 'Zmień odpowiedzialną osobę dla zadania "%s"', 'Change assignee for the task "%s"' => 'Zmień odpowiedzialną osobę dla zadania "%s"',
'Timezone' => 'Strefa czasowa', 'Timezone' => 'Strefa czasowa',
'Actions' => 'Akcje',
'Confirmation' => 'Powtórzenie hasła',
'Description' => 'Opis',
'Sorry, I didn\'t found this information in my database!' => 'Niestety nie znaleziono tej informacji w bazie danych', 'Sorry, I didn\'t found this information in my database!' => 'Niestety nie znaleziono tej informacji w bazie danych',
'Page not found' => 'Strona nie istnieje', 'Page not found' => 'Strona nie istnieje',
'Story Points' => 'Poziom trudności', 'Complexity' => 'Poziom trudności',
'limit' => 'limit', 'limit' => 'limit',
'Task limit' => 'Limit zadań', 'Task limit' => 'Limit zadań',
'This value must be greater than %d' => 'Wartość musi być większa niż %d', 'This value must be greater than %d' => 'Wartość musi być większa niż %d',
'Edit project access list' => 'Edycja list dostępu dla projektu', 'Edit project access list' => 'Edycja list dostępu dla projektu',
'Edit users access' => 'Edytuj dostęp', 'Edit users access' => 'Edytuj dostęp',
'Allow this user' => 'Dodaj użytkownika', 'Allow this user' => 'Dodaj użytkownika',
'Project access list for "%s"' => 'Lista uprawnionych dla projektu "%s"',
'Only those users have access to this project:' => 'Użytkownicy mający dostęp:', 'Only those users have access to this project:' => 'Użytkownicy mający dostęp:',
'Don\'t forget that administrators have access to everything.' => 'Pamiętaj: Administratorzy mają zawsze dostęp do wszystkiego!', 'Don\'t forget that administrators have access to everything.' => 'Pamiętaj: Administratorzy mają zawsze dostęp do wszystkiego!',
'revoke' => 'odbierz dostęp', 'revoke' => 'odbierz dostęp',
@ -212,16 +203,17 @@ return array(
'Write your text in Markdown' => 'Możesz użyć Markdown', 'Write your text in Markdown' => 'Możesz użyć Markdown',
'Leave a comment' => 'Zostaw komentarz', 'Leave a comment' => 'Zostaw komentarz',
'Comment is required' => 'Komentarz jest wymagany', 'Comment is required' => 'Komentarz jest wymagany',
// 'Leave a description' => '',
'Comment added successfully.' => 'Komentarz dodany', 'Comment added successfully.' => 'Komentarz dodany',
'Unable to create your comment.' => 'Nie udało się dodać komentarza', 'Unable to create your comment.' => 'Nie udało się dodać komentarza',
'The description is required' => 'Opis jest wymagany', 'The description is required' => 'Opis jest wymagany',
'Edit this task' => 'Edytuj zadanie', 'Edit this task' => 'Edytuj zadanie',
'Due Date' => 'Termin', 'Due Date' => 'Termin',
'm/d/Y' => 'd/m/Y', // Date format parsed with php 'm/d/Y' => 'd/m/Y',
'month/day/year' => 'dzień/miesiąc/rok', // Help shown to the user 'month/day/year' => 'dzień/miesiąc/rok',
'Invalid date' => 'Błędna data', 'Invalid date' => 'Błędna data',
'Must be done before %B %e, %G' => 'Termin do %e %B %G', 'Must be done before %B %e, %Y' => 'Termin do %e %B %Y',
'%B %e, %G' => '%e %B %G', '%B %e, %Y' => '%e %B %Y',
'Automatic actions' => 'Akcje automatyczne', 'Automatic actions' => 'Akcje automatyczne',
'Your automatic action have been created successfully.' => 'Twoja akcja została dodana', 'Your automatic action have been created successfully.' => 'Twoja akcja została dodana',
'Unable to create your automatic action.' => 'Nie udało się utworzyć akcji', 'Unable to create your automatic action.' => 'Nie udało się utworzyć akcji',
@ -230,6 +222,7 @@ return array(
'Action removed successfully.' => 'Akcja usunięta', 'Action removed successfully.' => 'Akcja usunięta',
'Automatic actions for the project "%s"' => 'Akcje automatyczne dla projektu "%s"', 'Automatic actions for the project "%s"' => 'Akcje automatyczne dla projektu "%s"',
'Defined actions' => 'Zdefiniowane akcje', 'Defined actions' => 'Zdefiniowane akcje',
'Add an action' => 'Nowa akcja',
'Event name' => 'Nazwa zdarzenia', 'Event name' => 'Nazwa zdarzenia',
'Action name' => 'Nazwa akcji', 'Action name' => 'Nazwa akcji',
'Action parameters' => 'Parametry akcji', 'Action parameters' => 'Parametry akcji',
@ -252,7 +245,6 @@ return array(
'Open a closed task' => 'Otwarcie zamkniętego zadania', 'Open a closed task' => 'Otwarcie zamkniętego zadania',
'Closing a task' => 'Zamknięcie zadania', 'Closing a task' => 'Zamknięcie zadania',
'Assign a color to a specific user' => 'Przypisz kolor do wybranego użytkownika', 'Assign a color to a specific user' => 'Przypisz kolor do wybranego użytkownika',
'Add an action' => 'Nowa akcja',
'Column title' => 'Tytuł kolumny', 'Column title' => 'Tytuł kolumny',
'Position' => 'Pozycja', 'Position' => 'Pozycja',
'Move Up' => 'Przenieś wyżej', 'Move Up' => 'Przenieś wyżej',
@ -281,12 +273,12 @@ return array(
'IP address' => 'Adres IP', 'IP address' => 'Adres IP',
'User agent' => 'Przeglądarka', 'User agent' => 'Przeglądarka',
'Persistent connections' => 'Stałe połączenia', 'Persistent connections' => 'Stałe połączenia',
'No session' => 'Brak sesji', 'No session.' => 'Brak sesji.',
'Expiration date' => 'Data zakończenia', 'Expiration date' => 'Data zakończenia',
'Remember Me' => 'Pamiętaj mnie', 'Remember Me' => 'Pamiętaj mnie',
'Creation date' => 'Data utworzenia', 'Creation date' => 'Data utworzenia',
// 'Filter by user' => '', // 'Filter by user' => '',
// 'Filter by due date' => ', // 'Filter by due date' => '',
// 'Everybody' => '', // 'Everybody' => '',
// 'Open' => '', // 'Open' => '',
// 'Closed' => '', // 'Closed' => '',
@ -295,7 +287,7 @@ return array(
// 'Search in the project "%s"' => '', // 'Search in the project "%s"' => '',
// 'Due date' => '', // 'Due date' => '',
// 'Others formats accepted: %s and %s' => '', // 'Others formats accepted: %s and %s' => '',
// 'Description' => '', 'Description' => 'Opis',
// '%d comments' => '', // '%d comments' => '',
// '%d comment' => '', // '%d comment' => '',
// 'Email address invalid' => '', // 'Email address invalid' => '',
@ -367,7 +359,6 @@ return array(
// 'The time must be a numeric value' => '', // 'The time must be a numeric value' => '',
// 'Todo' => '', // 'Todo' => '',
// 'In progress' => '', // 'In progress' => '',
// 'Done' => '',
// 'Sub-task removed successfully.' => '', // 'Sub-task removed successfully.' => '',
// 'Unable to remove this sub-task.' => '', // 'Unable to remove this sub-task.' => '',
// 'Sub-task updated successfully.' => '', // 'Sub-task updated successfully.' => '',
@ -384,7 +375,132 @@ return array(
// 'Unable to unlink your GitHub Account.' => '', // 'Unable to unlink your GitHub Account.' => '',
// 'Login with my GitHub Account' => '', // 'Login with my GitHub Account' => '',
// 'Link my GitHub Account' => '', // 'Link my GitHub Account' => '',
// 'Unlink my GitHub Account' => '', // 'Unlink my GitHub Account' => '',
// 'Created by %s' => 'Créé par %s', // 'Created by %s' => '',
// 'Last modified on %B %e, %G at %k:%M %p' => '', // 'Last modified on %B %e, %Y at %k:%M %p' => '',
// 'Tasks Export' => '',
// 'Tasks exportation for "%s"' => '',
// 'Start Date' => '',
// 'End Date' => '',
// 'Execute' => '',
// 'Task Id' => '',
// 'Creator' => '',
// 'Modification date' => '',
// 'Completion date' => '',
// 'Webhook URL for task creation' => '',
// 'Webhook URL for task modification' => '',
// 'Clone' => '',
// 'Clone Project' => '',
// 'Project cloned successfully.' => '',
// 'Unable to clone this project.' => '',
// 'Email notifications' => '',
// 'Enable email notifications' => '',
// 'Task position:' => '',
// 'The task #%d have been opened.' => '',
// 'The task #%d have been closed.' => '',
// 'Sub-task updated' => '',
// 'Title:' => '',
// 'Status:' => '',
// 'Assignee:' => '',
// 'Time tracking:' => '',
// 'New sub-task' => '',
// 'New attachment added "%s"' => '',
// 'Comment updated' => '',
// 'New comment posted by %s' => '',
// 'List of due tasks for the project "%s"' => '',
// '[%s][New attachment] %s (#%d)' => '',
// '[%s][New comment] %s (#%d)' => '',
// '[%s][Comment updated] %s (#%d)' => '',
// '[%s][New subtask] %s (#%d)' => '',
// '[%s][Subtask updated] %s (#%d)' => '',
// '[%s][New task] %s (#%d)' => '',
// '[%s][Task updated] %s (#%d)' => '',
// '[%s][Task closed] %s (#%d)' => '',
// '[%s][Task opened] %s (#%d)' => '',
// '[%s][Due tasks]' => '',
// '[Kanboard] Notification' => '',
// 'I want to receive notifications only for those projects:' => '',
// 'view the task on Kanboard' => '',
// 'Public access' => '',
// 'Categories management' => '',
// 'Users management' => '',
// 'Active tasks' => '',
// 'Disable public access' => '',
// 'Enable public access' => '',
// 'Active projects' => '',
// 'Inactive projects' => '',
// 'Public access disabled' => '',
// 'Do you really want to disable this project: "%s"?' => '',
// 'Do you really want to duplicate this project: "%s"?' => '',
// 'Do you really want to enable this project: "%s"?' => '',
// 'Project activation' => '',
// 'Move the task to another project' => '',
// 'Move to another project' => '',
// 'Do you really want to duplicate this task?' => '',
// 'Duplicate a task' => '',
// 'External accounts' => '',
// 'Account type' => '',
// 'Local' => '',
// 'Remote' => '',
// 'Enabled' => '',
// 'Disabled' => '',
// 'Google account linked' => '',
// 'Github account linked' => '',
// 'Username:' => '',
// 'Name:' => '',
// 'Email:' => '',
// 'Default project:' => '',
// 'Notifications:' => '',
// 'Group:' => '',
// 'Regular user' => '',
// 'Account type:' => '',
// 'Edit profile' => '',
// 'Change password' => '',
// 'Password modification' => '',
// 'External authentications' => '',
// 'Google Account' => '',
// 'Github Account' => '',
// 'Never connected.' => '',
// 'No account linked.' => '',
// 'Account linked.' => '',
// 'No external authentication enabled.' => '',
// 'Password modified successfully.' => '',
// 'Unable to change the password.' => '',
// 'Change category for the task "%s"' => '',
// 'Change category' => '',
// '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '',
// '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '',
// '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// 'Assigned to %s with an estimate of %s/%sh' => '',
// 'Not assigned, estimate of %sh' => '',
// '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s\'s activity' => '',
// 'No activity.' => '',
// 'RSS feed' => '',
// '%s updated a comment on the task #%d' => '',
// '%s commented on the task #%d' => '',
// '%s updated a subtask for the task #%d' => '',
// '%s created a subtask for the task #%d' => '',
// '%s updated the task #%d' => '',
// '%s created the task #%d' => '',
// '%s closed the task #%d' => '',
// '%s open the task #%d' => '',
// '%s moved the task #%d to the column "%s"' => '',
// '%s moved the task #%d to the position %d in the column "%s"' => '',
// 'Activity' => '',
// 'Default values are "%s"' => '',
// 'Default columns for new projects (Comma-separated)' => '',
// 'Task assignee change' => '',
// '%s change the assignee of the task #%d' => '',
// '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '[%s][Column Change] %s (#%d)' => '',
// '[%s][Position Change] %s (#%d)' => '',
// '[%s][Assignee Change] %s (#%d)' => '',
// 'New password for the user "%s"' => '',
); );

View file

@ -1,13 +1,6 @@
<?php <?php
return array( return array(
'English' => 'Inglês',
'French' => 'Francês',
'Polish' => 'Polonês',
'Portuguese (Brazilian)' => 'Português (Brasil)',
'Spanish' => 'Espanhol',
// 'German' => '',
// 'Chinese (Simplified)' => '',
'None' => 'Nenhum', 'None' => 'Nenhum',
'edit' => 'editar', 'edit' => 'editar',
'Edit' => 'Editar', 'Edit' => 'Editar',
@ -36,7 +29,7 @@ return array(
'All users' => 'Todos os usuários', 'All users' => 'Todos os usuários',
'Username' => 'Nome do usuário', 'Username' => 'Nome do usuário',
'Password' => 'Senha', 'Password' => 'Senha',
'Default Project' => 'Projeto default', 'Default project' => 'Projeto default',
'Administrator' => 'Administrador', 'Administrator' => 'Administrador',
'Sign in' => 'Logar', 'Sign in' => 'Logar',
'Users' => 'Usuários', 'Users' => 'Usuários',
@ -58,6 +51,7 @@ return array(
'Status' => 'Status', 'Status' => 'Status',
'Tasks' => 'Tarefas', 'Tasks' => 'Tarefas',
'Board' => 'Quadro', 'Board' => 'Quadro',
'Actions' => 'Ações',
'Inactive' => 'Inativo', 'Inactive' => 'Inativo',
'Active' => 'Ativo', 'Active' => 'Ativo',
'Column %d' => 'Coluna %d', 'Column %d' => 'Coluna %d',
@ -90,6 +84,7 @@ return array(
'Application settings' => 'Preferências da aplicação', 'Application settings' => 'Preferências da aplicação',
'Language' => 'Idioma', 'Language' => 'Idioma',
'Webhooks token:' => 'Token de webhooks:', 'Webhooks token:' => 'Token de webhooks:',
'API token:' => 'API Token:',
'More information' => 'Mais informação', 'More information' => 'Mais informação',
'Database size:' => 'Tamanho do banco de dados:', 'Database size:' => 'Tamanho do banco de dados:',
'Download the database' => 'Download do banco de dados', 'Download the database' => 'Download do banco de dados',
@ -109,7 +104,7 @@ return array(
'Open a task' => 'Abrir uma tarefa', 'Open a task' => 'Abrir uma tarefa',
'Do you really want to open this task: "%s"?' => 'Quer realmente abrir esta tarefa: "%s"?', 'Do you really want to open this task: "%s"?' => 'Quer realmente abrir esta tarefa: "%s"?',
'Back to the board' => 'Voltar ao quadro', 'Back to the board' => 'Voltar ao quadro',
'Created on %B %e, %G at %k:%M %p' => 'Criado em %d %B %G às %H:%M', 'Created on %B %e, %Y at %k:%M %p' => 'Criado em %d %B %Y às %H:%M',
'There is nobody assigned' => 'Não há ninguém designado', 'There is nobody assigned' => 'Não há ninguém designado',
'Column on the board:' => 'Coluna no quadro:', 'Column on the board:' => 'Coluna no quadro:',
'Status is open' => 'Status está aberto', 'Status is open' => 'Status está aberto',
@ -171,8 +166,8 @@ return array(
'Work in progress' => 'Em andamento', 'Work in progress' => 'Em andamento',
'Done' => 'Encerrado', 'Done' => 'Encerrado',
'Application version:' => 'Versão da aplicação:', 'Application version:' => 'Versão da aplicação:',
'Completed on %B %e, %G at %k:%M %p' => 'Encerrado em %d %B %G às %H:%M', 'Completed on %B %e, %Y at %k:%M %p' => 'Encerrado em %d %B %Y às %H:%M',
'%B %e, %G at %k:%M %p' => '%d %B %G às %H:%M', '%B %e, %Y at %k:%M %p' => '%d %B %Y às %H:%M',
'Date created' => 'Data de criação', 'Date created' => 'Data de criação',
'Date completed' => 'Data de encerramento', 'Date completed' => 'Data de encerramento',
'Id' => 'Id', 'Id' => 'Id',
@ -181,7 +176,7 @@ return array(
'List of projects' => 'Lista de projetos', 'List of projects' => 'Lista de projetos',
'Completed tasks for "%s"' => 'Tarefas completadas por "%s"', 'Completed tasks for "%s"' => 'Tarefas completadas por "%s"',
'%d closed tasks' => '%d tarefas encerradas', '%d closed tasks' => '%d tarefas encerradas',
'no task for this project' => 'nenhuma tarefa para este projeto', 'No task for this project' => 'Nenhuma tarefa para este projeto',
'Public link' => 'Link público', 'Public link' => 'Link público',
'There is no column in your project!' => 'Não há colunas no seu projeto!', 'There is no column in your project!' => 'Não há colunas no seu projeto!',
'Change assignee' => 'Mudar a designação', 'Change assignee' => 'Mudar a designação',
@ -189,14 +184,13 @@ return array(
'Timezone' => 'Fuso horário', 'Timezone' => 'Fuso horário',
'Sorry, I didn\'t found this information in my database!' => 'Desculpe, não encontrei esta informação no meu banco de dados!', 'Sorry, I didn\'t found this information in my database!' => 'Desculpe, não encontrei esta informação no meu banco de dados!',
'Page not found' => 'Página não encontrada', 'Page not found' => 'Página não encontrada',
'Story Points' => 'Complexidade', 'Complexity' => 'Complexidade',
'limit' => 'limite', 'limit' => 'limite',
'Task limit' => 'Limite da tarefa', 'Task limit' => 'Limite da tarefa',
'This value must be greater than %d' => 'Este valor deve ser maior que %d', 'This value must be greater than %d' => 'Este valor deve ser maior que %d',
'Edit project access list' => 'Editar lista de acesso ao projeto', // new translations to brazilian portuguese starts here 'Edit project access list' => 'Editar lista de acesso ao projeto',
'Edit users access' => 'Editar acesso de usuários', 'Edit users access' => 'Editar acesso de usuários',
'Allow this user' => 'Permitir esse usuário', 'Allow this user' => 'Permitir esse usuário',
'Project access list for "%s"' => 'Lista de acesso ao projeto para "%s"',
'Only those users have access to this project:' => 'Somente estes usuários têm acesso a este projeto:', 'Only those users have access to this project:' => 'Somente estes usuários têm acesso a este projeto:',
'Don\'t forget that administrators have access to everything.' => 'Não esqueça que administradores têm acesso a tudo.', 'Don\'t forget that administrators have access to everything.' => 'Não esqueça que administradores têm acesso a tudo.',
'revoke' => 'revogar', 'revoke' => 'revogar',
@ -215,11 +209,11 @@ return array(
'The description is required' => 'A descrição é obrigatória', 'The description is required' => 'A descrição é obrigatória',
'Edit this task' => 'Editar esta tarefa', 'Edit this task' => 'Editar esta tarefa',
'Due Date' => 'Data de vencimento', 'Due Date' => 'Data de vencimento',
'm/d/Y' => 'd/m/Y', // Date format parsed with php 'm/d/Y' => 'd/m/Y',
'month/day/year' => 'dia/mês/ano', // Help shown to the user 'month/day/year' => 'dia/mês/ano',
'Invalid date' => 'Data inválida', 'Invalid date' => 'Data inválida',
'Must be done before %B %e, %G' => 'Deve ser feito antes de %d %B %G', 'Must be done before %B %e, %Y' => 'Deve ser feito antes de %d %B %Y',
'%B %e, %G' => '%d %B %G', '%B %e, %Y' => '%d %B %Y',
'Automatic actions' => 'Ações automáticas', 'Automatic actions' => 'Ações automáticas',
'Your automatic action have been created successfully.' => 'Sua ação automética foi criada com sucesso.', 'Your automatic action have been created successfully.' => 'Sua ação automética foi criada com sucesso.',
'Unable to create your automatic action.' => 'Impossível criar sua ação automática.', 'Unable to create your automatic action.' => 'Impossível criar sua ação automática.',
@ -228,6 +222,7 @@ return array(
'Action removed successfully.' => 'Ação removida com sucesso.', 'Action removed successfully.' => 'Ação removida com sucesso.',
'Automatic actions for the project "%s"' => 'Ações automáticas para o projeto "%s"', 'Automatic actions for the project "%s"' => 'Ações automáticas para o projeto "%s"',
'Defined actions' => 'Ações definidas', 'Defined actions' => 'Ações definidas',
'Add an action' => 'Adicionar Ação',
'Event name' => 'Nome do evento', 'Event name' => 'Nome do evento',
'Action name' => 'Nome da ação', 'Action name' => 'Nome da ação',
'Action parameters' => 'Parâmetros da ação', 'Action parameters' => 'Parâmetros da ação',
@ -250,138 +245,262 @@ return array(
'Open a closed task' => 'Reabrir uma tarefa fechada', 'Open a closed task' => 'Reabrir uma tarefa fechada',
'Closing a task' => 'Fechando uma tarefa', 'Closing a task' => 'Fechando uma tarefa',
'Assign a color to a specific user' => 'Designar uma cor para um usuário específico', 'Assign a color to a specific user' => 'Designar uma cor para um usuário específico',
// 'Column title' => '', 'Column title' => 'Título da coluna',
// 'Position' => '', 'Position' => 'Posição',
// 'Move Up' => '', 'Move Up' => 'Mover para cima',
// 'Move Down' => '', 'Move Down' => 'Mover para baixo',
// 'Duplicate to another project' => '', 'Duplicate to another project' => 'Duplicar para outro projeto',
// 'Duplicate' => '', 'Duplicate' => 'Duplicar',
// 'link' => '', 'link' => 'link',
// 'Update this comment' => '', 'Update this comment' => 'Atualizar este comentário',
// 'Comment updated successfully.' => '', 'Comment updated successfully.' => 'Comentário atualizado com sucesso.',
// 'Unable to update your comment.' => '', 'Unable to update your comment.' => 'Impossível atualizar seu comentário.',
// 'Remove a comment' => '', 'Remove a comment' => 'Remover um comentário.',
// 'Comment removed successfully.' => '', 'Comment removed successfully.' => 'Comentário removido com sucesso.',
// 'Unable to remove this comment.' => '', 'Unable to remove this comment.' => 'Impossível remover este comentário',
// 'Do you really want to remove this comment?' => '', 'Do you really want to remove this comment?' => 'Você tem certeza de que quer remover este comentário?',
// 'Only administrators or the creator of the comment can access to this page.' => '', 'Only administrators or the creator of the comment can access to this page.' => 'Somente administradores ou o criator deste comentário tem acesso a esta página.',
// 'Details' => '', 'Details' => 'Detalhes',
// 'Current password for the user "%s"' => '', 'Current password for the user "%s"' => 'Senha atual para o usuário "%s"',
// 'The current password is required' => '', 'The current password is required' => 'A senha atual é obrigatória',
// 'Wrong password' => '', 'Wrong password' => 'Senha errada',
// 'Reset all tokens' => '', 'Reset all tokens' => 'Reiniciar todos os tokens',
// 'All tokens have been regenerated.' => '', 'All tokens have been regenerated.' => 'Todos os tokens foram gerados novamente',
// 'Unknown' => '', 'Unknown' => 'Desconhecido',
// 'Last logins' => '', 'Last logins' => 'Últimos logins',
// 'Login date' => '', 'Login date' => 'Data de login',
// 'Authentication method' => '', 'Authentication method' => 'Método de autenticação',
// 'IP address' => '', 'IP address' => 'Endereço IP',
// 'User agent' => '', 'User agent' => 'Agente usuário',
// 'Persistent connections' => '', 'Persistent connections' => 'Conexões persistentes',
// 'No session' => '', 'No session.' => 'Sem sessão.',
// 'Expiration date' => '', 'Expiration date' => 'Data de expiração',
// 'Remember Me' => '', 'Remember Me' => 'Lembre-se de mim',
// 'Creation date' => '', 'Creation date' => 'Data de criação',
// 'Filter by user' => '', 'Filter by user' => 'Filtrar por usuário',
// 'Filter by due date' => ', 'Filter by due date' => 'Filtrar por data de vencimento',
// 'Everybody' => '', 'Everybody' => 'Todos',
// 'Open' => '', 'Open' => 'Abrir',
// 'Closed' => '', 'Closed' => 'Fechado',
// 'Search' => '', 'Search' => 'Pesquisar',
// 'Nothing found.' => '', 'Nothing found.' => 'Não encontrado.',
// 'Search in the project "%s"' => '', 'Search in the project "%s"' => 'Procure no projeto "%s"',
// 'Due date' => '', 'Due date' => 'Data de vencimento',
// 'Others formats accepted: %s and %s' => '', 'Others formats accepted: %s and %s' => 'Outros formatos permitidos: %s e %s',
// 'Description' => '', 'Description' => 'Descrição',
// '%d comments' => '', '%d comments' => '%d comentários',
// '%d comment' => '', '%d comment' => '%d comentário',
// 'Email address invalid' => '', 'Email address invalid' => 'Endereço de e-mail inválido',
// 'Your Google Account is not linked anymore to your profile.' => '', 'Your Google Account is not linked anymore to your profile.' => 'Sua conta Google não está mais associada ao seu perfil.',
// 'Unable to unlink your Google Account.' => '', 'Unable to unlink your Google Account.' => 'Impossível desassociar sua conta Google.',
// 'Google authentication failed' => '', 'Google authentication failed' => 'Autenticação do Google falhou.',
// 'Unable to link your Google Account.' => '', 'Unable to link your Google Account.' => 'Impossível associar a sua conta do Google.',
// 'Your Google Account is linked to your profile successfully.' => '', 'Your Google Account is linked to your profile successfully.' => 'Sua Conta do Google está ligada ao seu perfil com sucesso.',
// 'Email' => '', 'Email' => 'E-mail',
// 'Link my Google Account' => '', 'Link my Google Account' => 'Vincular minha conta Google',
// 'Unlink my Google Account' => '', 'Unlink my Google Account' => 'Desvincular minha conta do Google',
// 'Login with my Google Account' => '', 'Login with my Google Account' => 'Entrar com minha conta do Google',
// 'Project not found.' => '', 'Project not found.' => 'Projeto não encontrado.',
// 'Task #%d' => '', 'Task #%d' => 'Tarefa #%d',
// 'Task removed successfully.' => '', 'Task removed successfully.' => 'Tarefa removida com sucesso.',
// 'Unable to remove this task.' => '', 'Unable to remove this task.' => 'Não foi possível remover esta tarefa.',
// 'Remove a task' => '', 'Remove a task' => 'Remover uma tarefa',
// 'Do you really want to remove this task: "%s"?' => '', 'Do you really want to remove this task: "%s"?' => 'Você realmente deseja remover esta tarefa: "%s"',
// 'Assign automatically a color based on a category' => '', 'Assign automatically a color based on a category' => 'Atribuir automaticamente uma cor com base em uma categoria',
// 'Assign automatically a category based on a color' => '', 'Assign automatically a category based on a color' => 'Atribuir automaticamente uma categoria com base em uma cor',
// 'Task creation or modification' => '', 'Task creation or modification' => 'Criação ou modificação de tarefa',
// 'Category' => '', 'Category' => 'Categoria',
// 'Category:' => '', 'Category:' => 'Categoria:',
// 'Categories' => '', 'Categories' => 'Categorias',
// 'Category not found.' => '', 'Category not found.' => 'Categoria não encontrada.',
// 'Your category have been created successfully.' => '', 'Your category have been created successfully.' => 'Seu categoria foi criada com sucesso.',
// 'Unable to create your category.' => '', 'Unable to create your category.' => 'Não é possível criar sua categoria.',
// 'Your category have been updated successfully.' => '', 'Your category have been updated successfully.' => 'A sua categoria foi atualizada com sucesso.',
// 'Unable to update your category.' => '', 'Unable to update your category.' => 'Não foi possível atualizar a sua categoria.',
// 'Remove a category' => '', 'Remove a category' => 'Remover uma categoria',
// 'Category removed successfully.' => '', 'Category removed successfully.' => 'Categoria removido com sucesso.',
// 'Unable to remove this category.' => '', 'Unable to remove this category.' => 'Não foi possível remover esta categoria.',
// 'Category modification for the project "%s"' => '', 'Category modification for the project "%s"' => 'Modificação de categoria para o projeto "%s"',
// 'Category Name' => '', 'Category Name' => 'Nome da Categoria',
// 'Categories for the project "%s"' => '', 'Categories for the project "%s"' => 'Categorias para o projeto "%s"',
// 'Add a new category' => '', 'Add a new category' => 'Adicionar uma nova categoria',
// 'Do you really want to remove this category: "%s"?' => '', 'Do you really want to remove this category: "%s"?' => 'Você realmente deseja remover esta categoria: "%s"',
// 'Filter by category' => '', 'Filter by category' => 'Filtrar por categoria',
// 'All categories' => '', 'All categories' => 'Todas as categorias',
// 'No category' => '', 'No category' => 'Sem categoria',
// 'The name is required' => '', 'The name is required' => 'O nome é obrigatório',
// 'Remove a file' => '', 'Remove a file' => 'Remover um arquivo',
// 'Unable to remove this file.' => '', 'Unable to remove this file.' => 'Não foi possível remover este arquivo.',
// 'File removed successfully.' => '', 'File removed successfully.' => 'Arquivo removido com sucesso.',
// 'Attach a document' => '', 'Attach a document' => 'Anexar um documento',
// 'Do you really want to remove this file: "%s"?' => '', 'Do you really want to remove this file: "%s"?' => 'Você realmente deseja remover este arquivo: "%s"',
// 'open' => '', 'open' => 'Aberto',
// 'Attachments' => '', 'Attachments' => 'Anexos',
// 'Edit the task' => '', 'Edit the task' => 'Editar a tarefa',
// 'Edit the description' => '', 'Edit the description' => 'Editar a descrição',
// 'Add a comment' => '', 'Add a comment' => 'Adicionar um comentário',
// 'Edit a comment' => '', 'Edit a comment' => 'Editar um comentário',
// 'Summary' => '', 'Summary' => 'Resumo',
// 'Time tracking' => '', 'Time tracking' => 'Rastreamento de tempo',
// 'Estimate:' => '', 'Estimate:' => 'Estimado:',
// 'Spent:' => '', 'Spent:' => 'Gasto:',
// 'Do you really want to remove this sub-task?' => '', 'Do you really want to remove this sub-task?' => 'Você realmente deseja remover esta sub-tarefa?',
// 'Remaining:' => '', 'Remaining:' => 'Restante:',
// 'hours' => '', 'hours' => 'horas',
// 'spent' => '', 'spent' => 'gasto',
// 'estimated' => '', 'estimated' => 'estimada',
// 'Sub-Tasks' => '', 'Sub-Tasks' => 'Sub-tarefas',
// 'Add a sub-task' => '', 'Add a sub-task' => 'Adicionar uma sub-tarefa',
// 'Original Estimate' => '', 'Original Estimate' => 'Estimativa original',
// 'Create another sub-task' => '', 'Create another sub-task' => 'Criar uma outra sub-tarefa',
// 'Time Spent' => '', 'Time Spent' => 'Tempo gasto',
// 'Edit a sub-task' => '', 'Edit a sub-task' => 'Editar uma sub-tarefa',
// 'Remove a sub-task' => '', 'Remove a sub-task' => 'Remover uma sub-tarefa',
// 'The time must be a numeric value' => '', 'The time must be a numeric value' => 'O tempo deve ser um valor numérico',
// 'Todo' => '', 'Todo' => 'A fazer',
// 'In progress' => '', 'In progress' => 'Em andamento',
// 'Done' => '', 'Sub-task removed successfully.' => 'Sub-tarefa removido com sucesso.',
// 'Sub-task removed successfully.' => '', 'Unable to remove this sub-task.' => 'Não foi possível remover esta sub-tarefa.',
// 'Unable to remove this sub-task.' => '', 'Sub-task updated successfully.' => 'Sub-tarefa atualizada com sucesso.',
// 'Sub-task updated successfully.' => '', 'Unable to update your sub-task.' => 'Não foi possível atualizar sua sub-tarefa.',
// 'Unable to update your sub-task.' => '', 'Unable to create your sub-task.' => 'Não é possível criar sua sub-tarefa.',
// 'Unable to create your sub-task.' => '', 'Sub-task added successfully.' => 'Sub-tarefa adicionada com sucesso.',
// 'Sub-task added successfully.' => '', 'Maximum size: ' => 'O tamanho máximo:',
// 'Maximum size: ' => '', 'Unable to upload the file.' => 'Não foi possível carregar o arquivo.',
// 'Unable to upload the file.' => '', 'Display another project' => 'Mostrar um outro projeto',
// 'Display another project' => '', 'Your GitHub account was successfully linked to your profile.' => 'A sua conta GitHub foi ligada com sucesso ao seu perfil.',
// 'Your GitHub account was successfully linked to your profile.' => '', 'Unable to link your GitHub Account.' => 'Não foi possível vincular sua conta GitHub.',
// 'Unable to link your GitHub Account.' => '', 'GitHub authentication failed' => 'Falhou autenticação GitHub',
// 'GitHub authentication failed' => '', 'Your GitHub account is no longer linked to your profile.' => 'A sua conta GitHub já não está ligada ao seu perfil.',
// 'Your GitHub account is no longer linked to your profile.' => '', 'Unable to unlink your GitHub Account.' => 'Não foi possível desvincular sua conta GitHub.',
// 'Unable to unlink your GitHub Account.' => '', 'Login with my GitHub Account' => 'Entrar com minha conta do GitHub',
// 'Login with my GitHub Account' => '', 'Link my GitHub Account' => 'Vincular minha conta GitHub',
// 'Link my GitHub Account' => '', 'Unlink my GitHub Account' => 'Desvincular minha conta do GitHub',
// 'Unlink my GitHub Account' => '', 'Created by %s' => 'Criado por %s',
// 'Created by %s' => 'Créé par %s', 'Last modified on %B %e, %Y at %k:%M %p' => 'Última modificação em %B %e, %Y às %k: %M %p',
// 'Last modified on %B %e, %G at %k:%M %p' => '', 'Tasks Export' => 'Tarefas Export',
'Tasks exportation for "%s"' => 'Tarefas exportação para "%s"',
'Start Date' => 'Data inicial',
'End Date' => 'Data final',
'Execute' => 'Executar',
'Task Id' => 'Id da Tarefa',
'Creator' => 'Criador',
'Modification date' => 'Data de modificação',
'Completion date' => 'Data de conclusão',
'Webhook URL for task creation' => 'Webhook URL para criação de tarefas',
'Webhook URL for task modification' => 'Webhook URL para modificação tarefa',
'Clone' => 'Clone',
'Clone Project' => 'Clonar Projeto',
'Project cloned successfully.' => 'Projeto clonado com sucesso.',
'Unable to clone this project.' => 'Impossível clonar este projeto.',
// 'Email notifications' => '',
// 'Enable email notifications' => '',
// 'Task position:' => '',
// 'The task #%d have been opened.' => '',
// 'The task #%d have been closed.' => '',
// 'Sub-task updated' => '',
// 'Title:' => '',
// 'Status:' => '',
// 'Assignee:' => '',
// 'Time tracking:' => '',
// 'New sub-task' => '',
// 'New attachment added "%s"' => '',
// 'Comment updated' => '',
// 'New comment posted by %s' => '',
// 'List of due tasks for the project "%s"' => '',
// '[%s][New attachment] %s (#%d)' => '',
// '[%s][New comment] %s (#%d)' => '',
// '[%s][Comment updated] %s (#%d)' => '',
// '[%s][New subtask] %s (#%d)' => '',
// '[%s][Subtask updated] %s (#%d)' => '',
// '[%s][New task] %s (#%d)' => '',
// '[%s][Task updated] %s (#%d)' => '',
// '[%s][Task closed] %s (#%d)' => '',
// '[%s][Task opened] %s (#%d)' => '',
// '[%s][Due tasks]' => '',
// '[Kanboard] Notification' => '',
// 'I want to receive notifications only for those projects:' => '',
// 'view the task on Kanboard' => '',
// 'Public access' => '',
// 'Categories management' => '',
// 'Users management' => '',
// 'Active tasks' => '',
// 'Disable public access' => '',
// 'Enable public access' => '',
// 'Active projects' => '',
// 'Inactive projects' => '',
// 'Public access disabled' => '',
// 'Do you really want to disable this project: "%s"?' => '',
// 'Do you really want to duplicate this project: "%s"?' => '',
// 'Do you really want to enable this project: "%s"?' => '',
// 'Project activation' => '',
// 'Move the task to another project' => '',
// 'Move to another project' => '',
// 'Do you really want to duplicate this task?' => '',
// 'Duplicate a task' => '',
// 'External accounts' => '',
// 'Account type' => '',
// 'Local' => '',
// 'Remote' => '',
// 'Enabled' => '',
// 'Disabled' => '',
// 'Google account linked' => '',
// 'Github account linked' => '',
// 'Username:' => '',
// 'Name:' => '',
// 'Email:' => '',
// 'Default project:' => '',
// 'Notifications:' => '',
// 'Group:' => '',
// 'Regular user' => '',
// 'Account type:' => '',
// 'Edit profile' => '',
// 'Change password' => '',
// 'Password modification' => '',
// 'External authentications' => '',
// 'Google Account' => '',
// 'Github Account' => '',
// 'Never connected.' => '',
// 'No account linked.' => '',
// 'Account linked.' => '',
// 'No external authentication enabled.' => '',
// 'Password modified successfully.' => '',
// 'Unable to change the password.' => '',
// 'Change category for the task "%s"' => '',
// 'Change category' => '',
// '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '',
// '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '',
// '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// 'Assigned to %s with an estimate of %s/%sh' => '',
// 'Not assigned, estimate of %sh' => '',
// '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s\'s activity' => '',
// 'No activity.' => '',
// 'RSS feed' => '',
// '%s updated a comment on the task #%d' => '',
// '%s commented on the task #%d' => '',
// '%s updated a subtask for the task #%d' => '',
// '%s created a subtask for the task #%d' => '',
// '%s updated the task #%d' => '',
// '%s created the task #%d' => '',
// '%s closed the task #%d' => '',
// '%s open the task #%d' => '',
// '%s moved the task #%d to the column "%s"' => '',
// '%s moved the task #%d to the position %d in the column "%s"' => '',
// 'Activity' => '',
// 'Default values are "%s"' => '',
// 'Default columns for new projects (Comma-separated)' => '',
// 'Task assignee change' => '',
// '%s change the assignee of the task #%d' => '',
// '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '[%s][Column Change] %s (#%d)' => '',
// '[%s][Position Change] %s (#%d)' => '',
// '[%s][Assignee Change] %s (#%d)' => '',
// 'New password for the user "%s"' => '',
); );

View file

@ -0,0 +1,506 @@
<?php
return array(
'None' => 'Отсутствует',
'edit' => 'изменить',
'Edit' => 'Изменить',
'remove' => 'удалить',
'Remove' => 'Удалить',
'Update' => 'Обновить',
'Yes' => 'Да',
'No' => 'Нет',
'cancel' => 'Отменить',
'or' => 'или',
'Yellow' => 'Желтый',
'Blue' => 'Синий',
'Green' => 'Зеленый',
'Purple' => 'Фиолетовый',
'Red' => 'Красный',
'Orange' => 'Оранжевый',
'Grey' => 'Серый',
'Save' => 'Сохранить',
'Login' => 'Вход',
'Official website:' => 'Официальный сайт :',
'Unassigned' => 'Не назначена',
'View this task' => 'Посмотреть задачу',
'Remove user' => 'Удалить пользователя',
'Do you really want to remove this user: "%s"?' => 'Вы точно хотите удалить пользователя: « %s » ?',
'New user' => 'Новый пользователь',
'All users' => 'Все пользователи',
'Username' => 'Имя пользователя',
'Password' => 'Пароль',
'Default project' => 'Проект по умолчанию',
'Administrator' => 'Администратор',
'Sign in' => 'Войти',
'Users' => 'Пользователи',
'No user' => 'Нет пользователя',
'Forbidden' => 'Запрещено',
'Access Forbidden' => 'Доступ запрещен',
'Only administrators can access to this page.' => 'Только администраторы могут войти на эту страницу.',
'Edit user' => 'Изменить пользователя',
'Logout' => 'Выйти',
'Bad username or password' => 'Неверное имя пользователя или пароль',
'users' => 'пользователи',
'projects' => 'проекты',
'Edit project' => 'Изменить проект',
'Name' => 'Имя',
'Activated' => 'Активен',
'Projects' => 'Проекты',
'No project' => 'Нет проекта',
'Project' => 'Проект',
'Status' => 'Статус',
'Tasks' => 'Задачи',
'Board' => 'Доска',
'Actions' => 'Действия',
'Inactive' => 'Неактивен',
'Active' => 'Активен',
'Column %d' => 'Колонка %d',
'Add this column' => 'Добавить колонку',
'%d tasks on the board' => 'Задач на доске - %d',
'%d tasks in total' => 'Задач всего - %d',
'Unable to update this board.' => 'Не удалось обновить доску.',
'Edit board' => 'Изменить доски',
'Disable' => 'Деактивировать',
'Enable' => 'Активировать',
'New project' => 'Новый проект',
'Do you really want to remove this project: "%s"?' => 'Вы точно хотите удалить этот проект? : « %s » ?',
'Remove project' => 'Удалить проект',
'Boards' => 'Доски',
'Edit the board for "%s"' => 'Изменить доску для « %s »',
'All projects' => 'Все проекты',
'Change columns' => 'Изменить колонки',
'Add a new column' => 'Добавить новую колонку',
'Title' => 'Название',
'Add Column' => 'Добавить колонку',
'Project "%s"' => 'Проект « %s »',
'Nobody assigned' => 'Никто не назначен',
'Assigned to %s' => 'Исполнитель: %s',
'Remove a column' => 'Удалить колонку',
'Remove a column from a board' => 'Удалить колонку с доски',
'Unable to remove this column.' => 'Не удалось удалить колонку.',
'Do you really want to remove this column: "%s"?' => 'Вы точно хотите удалить эту колонку : « %s » ?',
'This action will REMOVE ALL TASKS associated to this column!' => 'Вы УДАЛИТЕ ВСЕ ЗАДАЧИ находящиеся в этой колонке !',
'Settings' => 'Настройки',
'Application settings' => 'Настройки приложения',
'Language' => 'Язык',
'Webhooks token:' => 'Webhooks токен :',
'API token:' => 'API токен :',
'More information' => 'Подробнее',
'Database size:' => 'Размер базы данных :',
'Download the database' => 'Скачать базу данных',
'Optimize the database' => 'Оптимизировать базу данных',
'(VACUUM command)' => '(Команда VACUUM)',
'(Gzip compressed Sqlite file)' => '(Сжать GZip файл SQLite)',
'User settings' => 'Настройки пользователя',
'My default project:' => 'Мой проект по умолчанию : ',
'Close a task' => 'Закрыть задачу',
'Do you really want to close this task: "%s"?' => 'Вы точно хотите закрыть задачу : « %s » ?',
'Edit a task' => 'Изменить задачу',
'Column' => 'Колонка',
'Color' => 'Цвет',
'Assignee' => 'Назначена',
'Create another task' => 'Создать другую задачу',
'New task' => 'Новая задача',
'Open a task' => 'Открыть задачу',
'Do you really want to open this task: "%s"?' => 'Вы уверены что хотите открыть задачу : « %s » ?',
'Back to the board' => 'Вернуться на доску',
'Created on %B %e, %Y at %k:%M %p' => 'Создано %d/%m/%Y в %H:%M',
'There is nobody assigned' => 'Никто не назначен',
'Column on the board:' => 'Колонка на доске : ',
'Status is open' => 'Статус - открыт',
'Status is closed' => 'Статус - закрыт',
'Close this task' => 'Закрыть эту задачу',
'Open this task' => 'Открыть эту задачу',
'There is no description.' => 'Нет описания.',
'Add a new task' => 'Добавить новую задачу',
'The username is required' => 'Требуется имя пользователя',
'The maximum length is %d characters' => 'Максимальная длина - %d знаков',
'The minimum length is %d characters' => 'Минимальная длина - %d знаков',
'The password is required' => 'Требуется пароль',
'This value must be an integer' => 'Это значение должно быть целым',
'The username must be unique' => 'Требуется уникальное имя пользователя',
'The username must be alphanumeric' => 'Имя пользователя должно быть букво-цифровым',
'The user id is required' => 'Требуется ID пользователя',
'Passwords don\'t match' => 'Пароли не совпадают',
'The confirmation is required' => 'Требуется подтверждение',
'The column is required' => 'Требуется колонка',
'The project is required' => 'Требуется проект',
'The color is required' => 'Требуется цвет',
'The id is required' => 'Требуется ID',
'The project id is required' => 'Требуется ID проекта',
'The project name is required' => 'Требуется имя проекта',
'This project must be unique' => 'Проект должен быть уникальным',
'The title is required' => 'Требуется заголовок',
'The language is required' => 'Требуется язык',
'There is no active project, the first step is to create a new project.' => 'Нет активного проекта, сначала создайте новый проект.',
'Settings saved successfully.' => 'Параметры успешно сохранены.',
'Unable to save your settings.' => 'Невозможно сохранить параметры.',
'Database optimization done.' => 'База данных оптимизирована.',
'Your project have been created successfully.' => 'Ваш проект успешно создан.',
'Unable to create your project.' => 'Не удалось создать проект.',
'Project updated successfully.' => 'Проект успешно обновлен.',
'Unable to update this project.' => 'Не удалось обновить проект.',
'Unable to remove this project.' => 'Не удалось удалить проект.',
'Project removed successfully.' => 'Проект удален.',
'Project activated successfully.' => 'Проект активирован.',
'Unable to activate this project.' => 'Невозможно активировать проект.',
'Project disabled successfully.' => 'Проект успешно выключен.',
'Unable to disable this project.' => 'Не удалось выключить проект.',
'Unable to open this task.' => 'Не удалось открыть задачу.',
'Task opened successfully.' => 'Задача открыта.',
'Unable to close this task.' => 'Не удалось закрыть задачу.',
'Task closed successfully.' => 'Задача закрыта.',
'Unable to update your task.' => 'Не удалось обновить вашу задачу.',
'Task updated successfully.' => 'Задача обновлена.',
'Unable to create your task.' => 'Не удалось создать задачу.',
'Task created successfully.' => 'Задача создана.',
'User created successfully.' => 'Пользователь создан.',
'Unable to create your user.' => 'Не удалось создать пользователя.',
'User updated successfully.' => 'Пользователь обновлен.',
'Unable to update your user.' => 'Не удалось обновить пользователя.',
'User removed successfully.' => 'Пользователь удален.',
'Unable to remove this user.' => 'Не удалось удалить пользователя.',
'Board updated successfully.' => 'Доска обновлена.',
'Ready' => 'Готовые',
'Backlog' => 'Ожидающие',
'Work in progress' => 'В процессе',
'Done' => 'Завершенные',
'Application version:' => 'Версия приложения :',
'Completed on %B %e, %Y at %k:%M %p' => 'Завершен %d/%m/%Y в %H:%M',
'%B %e, %Y at %k:%M %p' => '%d/%m/%Y в %H:%M',
'Date created' => 'Дата создания',
'Date completed' => 'Дата завершения',
'Id' => 'ID',
'No task' => 'Нет задачи',
'Completed tasks' => 'Завершенные задачи',
'List of projects' => 'Список проектов',
'Completed tasks for "%s"' => 'Задачи завершенные для « %s »',
'%d closed tasks' => '%d завершенных задач',
'No task for this project' => 'нет задач для этого проекта',
'Public link' => 'Ссылка для просмотра',
'There is no column in your project!' => 'Нет колонки в вашем проекте !',
'Change assignee' => 'Сменить назначенного',
'Change assignee for the task "%s"' => 'Сменить назначенного для задачи « %s »',
'Timezone' => 'Часовой пояс',
'Sorry, I didn\'t found this information in my database!' => 'К сожалению, информация в базе данных не найдена !',
'Page not found' => 'Страница не найдена',
'Complexity' => 'Сложность',
'limit' => 'лимит',
'Task limit' => 'Лимит задач',
'This value must be greater than %d' => 'Это значение должно быть больше %d',
'Edit project access list' => 'Изменить доступ к проекту',
'Edit users access' => 'Изменить доступ пользователей',
'Allow this user' => 'Разрешить этого пользователя',
'Only those users have access to this project:' => 'Только эти пользователи имеют доступ к проекту :',
'Don\'t forget that administrators have access to everything.' => 'Помните, администратор имеет доступ всюду.',
'revoke' => 'отозвать',
'List of authorized users' => 'Список авторизованных пользователей',
'User' => 'Пользователь',
'Everybody have access to this project.' => 'Кто угодно имеет доступ к этому проекту.',
'You are not allowed to access to this project.' => 'Вам запрешен доступ к этому проекту.',
'Comments' => 'Комментарии',
'Post comment' => 'Оставить комментарий',
'Write your text in Markdown' => 'Справка по синтаксису Markdown',
'Leave a comment' => 'Оставить комментарий 2',
'Comment is required' => 'Нужен комментарий',
'Leave a description' => 'Оставьте описание',
'Comment added successfully.' => 'Комментарий успешно добавлен.',
'Unable to create your comment.' => 'Невозможно создать комментарий.',
'The description is required' => 'Требуется описание',
'Edit this task' => 'Изменить задачу',
'Due Date' => 'Срок',
'm/d/Y' => 'м/д/Г',
'month/day/year' => 'месяц/день/год',
'Invalid date' => 'Неверная дата',
'Must be done before %B %e, %Y' => 'Должно быть сделано до %d/%m/%Y',
'%B %e, %Y' => '%d/%m/%Y',
'Automatic actions' => 'Автоматические действия',
'Your automatic action have been created successfully.' => 'Автоматика настроена.',
'Unable to create your automatic action.' => 'Не удалось создать автоматизированное действие.',
'Remove an action' => 'Удалить действие',
'Unable to remove this action.' => 'Не удалось удалить действие',
'Action removed successfully.' => 'Действие удалено.',
'Automatic actions for the project "%s"' => 'Автоматические действия для проекта « %s »',
'Defined actions' => 'Заданные действия',
'Add an action' => 'Добавить действие',
'Event name' => 'Имя события',
'Action name' => 'Имя действия',
'Action parameters' => 'Параметры действия',
'Action' => 'Действие',
'Event' => 'Событие',
'When the selected event occurs execute the corresponding action.' => 'Когда случится ВЫБРАННОЕ событие выполняется СООТВЕТСТВУЮЩЕЕ действие.',
'Next step' => 'Следующий шаг',
'Define action parameters' => 'Задать параметры действия',
'Save this action' => 'Сохранить это действие',
'Do you really want to remove this action: "%s"?' => 'Вы точно хотите удалить это действие: « %s » ?',
'Remove an automatic action' => 'Удалить автоматическое действие',
'Close the task' => 'Закрыть задачу',
'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 position in the same column' => 'Переместить задачу в другое место этой же колонки',
'Task modification' => 'Изменение задачи',
'Task creation' => 'Создание задачи',
'Open a closed task' => 'Открыть завершенную задачу',
'Closing a task' => 'Завершение задачи',
'Assign a color to a specific user' => 'Назначить определенный цвет пользователю',
'Column title' => 'Название колонки',
'Position' => 'Расположение',
'Move Up' => 'Сдвинуть вверх',
'Move Down' => 'Сдвинуть вниз',
'Duplicate to another project' => 'Клонировать в другой проект',
'Duplicate' => 'Клонировать',
'link' => 'связь',
'Update this comment' => 'Обновить комментарий',
'Comment updated successfully.' => 'Комментарий обновлен.',
'Unable to update your comment.' => 'Не удалось обновить ваш комментарий.',
'Remove a comment' => 'Удалить комментарий',
'Comment removed successfully.' => 'Комментарий удален.',
'Unable to remove this comment.' => 'Не удалось удалить этот комментарий.',
'Do you really want to remove this comment?' => 'Вы точно хотите удалить этот комментарий ?',
'Only administrators or the creator of the comment can access to this page.' => 'Только администратор или автор комментарий могут получить доступ.',
'Details' => 'Подробности',
'Current password for the user "%s"' => 'Текущий пароль для пользователя « %s »',
'The current password is required' => 'Требуется текущий пароль',
'Wrong password' => 'Неверный пароль',
'Reset all tokens' => 'Сброс всех токенов',
'All tokens have been regenerated.' => 'Все токены пересозданы.',
'Unknown' => 'Неизвестно',
'Last logins' => 'Последние посещения',
'Login date' => 'Дата входа',
'Authentication method' => 'Способ аутентификации',
'IP address' => 'IP адрес',
'User agent' => 'User agent',
'Persistent connections' => 'Постоянные соединения',
'No session.' => 'Нет сеанса',
'Expiration date' => 'Дата окончания',
'Remember Me' => 'Запомнить меня',
'Creation date' => 'Дата создания',
'Filter by user' => 'Фильтр по пользователям',
'Filter by due date' => 'Фильтр по сроку',
'Everybody' => 'Все',
'Open' => 'Открытый',
'Closed' => 'Закрытый',
'Search' => 'Поиск',
'Nothing found.' => 'Ничего не найдено.',
'Search in the project "%s"' => 'Искать в проекте « %s »',
'Due date' => 'Срок',
'Others formats accepted: %s and %s' => 'Другой формат приемлем : %s и %s',
'Description' => 'Описание',
'%d comments' => '%d комментариев',
'%d comment' => '%d комментарий',
'Email address invalid' => 'Adresse email invalide',
'Your Google Account is not linked anymore to your profile.' => 'Ваш аккаунт в Google больше не привязан к вашему профилю.',
'Unable to unlink your Google Account.' => 'Не удалось отвязать ваш профиль от Google.',
'Google authentication failed' => 'Аутентификация Google не удалась',
'Unable to link your Google Account.' => 'Не удалось привязать ваш профиль к Google.',
'Your Google Account is linked to your profile successfully.' => 'Ваш профиль успешно привязан к Google.',
'Email' => 'Email',
'Link my Google Account' => 'Привязать мой профиль к Google',
'Unlink my Google Account' => 'Отвязать мой профиль от Google',
'Login with my Google Account' => 'Аутентификация через Google',
'Project not found.' => 'Проект не найден.',
'Task #%d' => 'Задача n°%d',
'Task removed successfully.' => 'Задача удалена.',
'Unable to remove this 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' => 'Категории',
'Category not found.' => 'Категория не найдена',
'Your category have been created successfully.' => 'Категория создана.',
'Unable to create your category.' => 'Не удалось создать категорию.',
'Your category have been updated successfully.' => 'Категория обновлена.',
'Unable to update your category.' => 'Не удалось обновить категорию.',
'Remove a category' => 'Удалить категорию',
'Category removed successfully.' => 'Категория удалена.',
'Unable to remove this category.' => 'Не удалось удалить категорию.',
'Category modification for the project "%s"' => 'Изменение категории для проекта « %s »',
'Category Name' => 'Название категории',
'Categories for the project "%s"' => 'Категории для проекта « %s »',
'Add a new category' => 'Добавить новую категорию',
'Do you really want to remove this category: "%s"?' => 'Вы точно хотите удалить категорию « %s » ?',
'Filter by category' => 'Фильтр по категориям',
'All categories' => 'Все категории',
'No category' => 'Нет категории',
'The name is required' => 'Требуется название',
'Remove a file' => 'Удалить файл',
'Unable to remove this file.' => 'Не удалось удалить файл.',
'File removed successfully.' => 'Файл удален.',
'Attach a document' => 'Приложить документ',
'Do you really want to remove this file: "%s"?' => 'Вы точно хотите удалить этот файл « %s » ?',
'open' => 'открыть',
'Attachments' => 'Приложение',
'Edit the task' => 'Изменить задачу',
'Edit the description' => 'Изменить описание',
'Add a comment' => 'Добавить комментарий',
'Edit a comment' => 'Изменить комментарий',
'Summary' => 'Сводка',
'Time tracking' => 'Отслеживание времени',
'Estimate:' => 'Приблизительно :',
'Spent:' => 'Затрачено :',
'Do you really want to remove this sub-task?' => 'Вы точно хотите удалить подзадачу ?',
'Remaining:' => 'Осталось :',
'hours' => 'часов',
'spent' => 'затрачено',
'estimated' => 'расчетное',
'Sub-Tasks' => 'Подзадачи',
'Add a sub-task' => 'Добавить подзадачу',
'Original Estimate' => 'Начальная оценка',
'Create another sub-task' => 'Создать другую подзадачу',
'Time Spent' => 'Времени затрачено',
'Edit a sub-task' => 'Изменить подзадачу',
'Remove a sub-task' => 'Удалить подзадачу',
'The time must be a numeric value' => 'Время должно быть числом!',
'Todo' => 'TODO',
'In progress' => 'В процессе',
'Sub-task removed successfully.' => 'Подзадача удалена.',
'Unable to remove this sub-task.' => 'Не удалось удалить подзадачу.',
'Sub-task updated successfully.' => 'Подзадача обновлена.',
'Unable to update your sub-task.' => 'Не удалось обновить подзадачу.',
'Unable to create your sub-task.' => 'Не удалось создать подзадачу.',
'Sub-task added successfully.' => 'Подзадача добавлена.',
'Maximum size: ' => 'Максимальный размер : ',
'Unable to upload the file.' => 'Не удалось загрузить файл.',
'Display another project' => 'Показать другой проект',
'Your GitHub account was successfully linked to your profile.' => 'Ваш GitHub привязан к вашему профилю.',
'Unable to link your GitHub Account.' => 'Не удалось привязать ваш профиль к Github.',
'GitHub authentication failed' => 'Аутентификация в GitHub не удалась',
'Your GitHub account is no longer linked to your profile.' => 'Ваш GitHub отвязан от вашего профиля.',
'Unable to unlink your GitHub Account.' => 'Не удалось отвязать ваш профиль от GitHub.',
'Login with my GitHub Account' => 'Аутентификация через GitHub',
'Link my GitHub Account' => 'Привязать мой профиль к GitHub',
'Unlink my GitHub Account' => 'Отвязать мой профиль от GitHub',
'Created by %s' => 'Создано %s',
'Last modified on %B %e, %Y at %k:%M %p' => 'Последнее изменение %d/%m/%Y в %H:%M',
'Tasks Export' => 'Экспорт задач',
'Tasks exportation for "%s"' => 'Задача экспортирована для « %s »',
'Start Date' => 'Дата начала',
'End Date' => 'Дата завершения',
'Execute' => 'Выполнить',
'Task Id' => 'ID задачи',
'Creator' => 'Автор',
'Modification date' => 'Дата изменения',
'Completion date' => 'Дата завершения',
'Webhook URL for task creation' => 'Webhook URL для создания задачи',
'Webhook URL for task modification' => 'Webhook URL для изменения задачи',
'Clone' => 'Клонировать',
'Clone Project' => 'Клонировать проект',
'Project cloned successfully.' => 'Проект клонирован.',
'Unable to clone this project.' => 'Не удалось клонировать проект.',
'Email notifications' => 'Уведомления по email',
'Enable email notifications' => 'Включить уведомления по email',
'Task position:' => 'Позиция задачи :',
'The task #%d have been opened.' => 'Задача #%d была открыта.',
'The task #%d have been closed.' => 'Задача #%d была закрыта.',
'Sub-task updated' => 'Подзадача обновлена',
'Title:' => 'Название :',
'Status:' => 'Статус :',
'Assignee:' => 'Назначена :',
'Time tracking:' => 'Отслеживание времени :',
'New sub-task' => 'Новая подзадача',
'New attachment added "%s"' => 'Добавлено вложение « %s »',
'Comment updated' => 'Комментарий обновлен',
'New comment posted by %s' => 'Новый комментарий написан « %s »',
'List of due tasks for the project "%s"' => 'Список сроков к проекту « %s »',
'[%s][New attachment] %s (#%d)' => '[%s][Новых вложений] %s (#%d)',
'[%s][New comment] %s (#%d)' => '[%s][Новых комментариев] %s (#%d)',
'[%s][Comment updated] %s (#%d)' => '[%s][Обновленых коментариев] %s (#%d)',
'[%s][New subtask] %s (#%d)' => '[%s][Новых подзадач] %s (#%d)',
'[%s][Subtask updated] %s (#%d)' => '[%s][Обновленных подзадач] %s (#%d)',
'[%s][New task] %s (#%d)' => '[%s][Новых задач] %s (#%d)',
'[%s][Task updated] %s (#%d)' => '[%s][Обновленных задач] %s (#%d)',
'[%s][Task closed] %s (#%d)' => '[%s][Закрытых задач] %s (#%d)',
'[%s][Task opened] %s (#%d)' => '[%s][Открытых задач] %s (#%d)',
'[%s][Due tasks]' => '[%s][Текущие задачи]',
'[Kanboard] Notification' => '[Kanboard] Оповещение',
'I want to receive notifications only for those projects:' => 'Я хочу получать уведомления только по этим проектам :',
'view the task on Kanboard' => 'посмотреть задачу на Kanboard',
'Public access' => 'Общий доступ',
'Categories management' => 'Управление категориями',
'Users management' => 'Управление пользователями',
'Active tasks' => 'Активные задачи',
'Disable public access' => 'Отключить общий доступ',
'Enable public access' => 'Включить общий доступ',
'Active projects' => 'Активные проекты',
'Inactive projects' => 'Неактивные проекты',
'Public access disabled' => 'Общий доступ отключен',
'Do you really want to disable this project: "%s"?' => 'Вы точно хотите отключить проект: "%s"?',
'Do you really want to duplicate this project: "%s"?' => 'Вы точно хотите клонировать проект: "%s"?',
'Do you really want to enable this project: "%s"?' => 'Вы точно хотите включить проект: "%s"?',
'Project activation' => 'Активация проекта',
'Move the task to another project' => 'Переместить задачу в другой проект',
'Move to another project' => 'Переместить в другой проект',
'Do you really want to duplicate this task?' => 'Вы точно хотите клонировать задачу?',
'Duplicate a task' => 'Клонировать задачу',
'External accounts' => 'Внешняя аутентификация',
'Account type' => 'Тип профиля',
'Local' => 'Локальный',
'Remote' => 'Удаленный',
'Enabled' => 'Включен',
'Disabled' => 'Выключены',
'Google account linked' => 'Профиль Google связан',
'Github account linked' => 'Профиль GitHub связан',
'Username:' => 'Имя пользователя:',
'Name:' => 'Имя:',
'Email:' => 'Email:',
'Default project:' => 'Проект по умолчанию:',
'Notifications:' => 'Уведомления:',
'Group:' => 'Группа:',
'Regular user' => 'Обычный пользователь',
'Account type:' => 'Тип профиля:',
'Edit profile' => 'Редактировать профиль:',
'Change password' => 'Сменить пароль',
'Password modification' => 'Изменение пароля',
'External authentications' => 'Внешняя аутентификация',
'Google Account' => 'Профиль Google',
'Github Account' => 'Профиль GitHub',
'Never connected.' => 'Ранее не соединялось.',
'No account linked.' => 'Нет связанных профилей.',
'Account linked.' => 'Профиль связан.',
'No external authentication enabled.' => 'Нет активной внешней аутентификации.',
'Password modified successfully.' => 'Пароль изменен.',
'Unable to change the password.' => 'Не удалось сменить пароль.',
'Change category for the task "%s"' => 'Сменить категорию для задачи "%s"',
'Change category' => 'Смена категории',
'%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s обновил задачу <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s открыл задачу <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s перместил задачу <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> на позицию #%d в колонке "%s"',
'%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '%s переместил задачу <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> в колонку "%s"',
'%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s создал задачу <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s закрыл задачу <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s создал подзадачу для задачи <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s обновил подзадачу для задачи <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'Assigned to %s with an estimate of %s/%sh' => 'Назначено %s с окончанием %s/%sh',
'Not assigned, estimate of %sh' => 'Не назначено, окончание %sh',
'%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s обновил комментарий к задаче <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s прокомментировал задачу <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'%s\'s activity' => '%s активность',
'No activity.' => 'Нет активности',
'RSS feed' => 'RSS лента',
'%s updated a comment on the task #%d' => '%s обновил комментарий задачи #%d',
'%s commented on the task #%d' => '%s откомментировал задачу #%d',
'%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 open the task #%d' => '%s открыл задачу #%d',
'%s moved the task #%d to the column "%s"' => '%s переместил задачу #%d в колонку "%s"',
'%s moved the task #%d to the position %d in the column "%s"' => '%s переместил задачу #%d на позицию %d в колонке "%s"',
'Activity' => 'Активность',
'Default values are "%s"' => 'Колонки по умолчанию: "%s"',
'Default columns for new projects (Comma-separated)' => 'Колонки по умолчанию для новых проектов (разделять запятой)',
// 'Task assignee change' => '',
// '%s change the assignee of the task #%d' => '',
// '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '[%s][Column Change] %s (#%d)' => '',
// '[%s][Position Change] %s (#%d)' => '',
// '[%s][Assignee Change] %s (#%d)' => '',
// 'New password for the user "%s"' => '',
);

View file

@ -1,13 +1,6 @@
<?php <?php
return array( return array(
'English' => 'Engelska',
'French' => 'Franska',
'Polish' => 'Polska',
'Portuguese (Brazilian)' => 'Portugisiska (Brasilien)',
'Spanish' => 'Spanska',
'German' => 'Tyska',
'Swedish' => 'Svenska',
'None' => 'Ingen', 'None' => 'Ingen',
'edit' => 'redigera', 'edit' => 'redigera',
'Edit' => 'Redigera', 'Edit' => 'Redigera',
@ -36,7 +29,7 @@ return array(
'All users' => 'Alla användare', 'All users' => 'Alla användare',
'Username' => 'Användarnamn', 'Username' => 'Användarnamn',
'Password' => 'Lösenord', 'Password' => 'Lösenord',
'Default Project' => 'Standardprojekt', 'Default project' => 'Standardprojekt',
'Administrator' => 'Administratör', 'Administrator' => 'Administratör',
'Sign in' => 'Logga in', 'Sign in' => 'Logga in',
'Users' => 'Användare', 'Users' => 'Användare',
@ -58,7 +51,7 @@ return array(
'Status' => 'Status', 'Status' => 'Status',
'Tasks' => 'Uppgifter', 'Tasks' => 'Uppgifter',
'Board' => 'Tavla', 'Board' => 'Tavla',
'Actions' => 'Åtgärder', 'Actions' => 'Åtgärder',
'Inactive' => 'Inaktiv', 'Inactive' => 'Inaktiv',
'Active' => 'Aktiv', 'Active' => 'Aktiv',
'Column %d' => 'Kolumn %d', 'Column %d' => 'Kolumn %d',
@ -91,6 +84,7 @@ return array(
'Application settings' => 'Applikationsinställningar', 'Application settings' => 'Applikationsinställningar',
'Language' => 'Språk', 'Language' => 'Språk',
'Webhooks token:' => 'Token för webhooks:', 'Webhooks token:' => 'Token för webhooks:',
'API token:' => 'API token:',
'More information' => 'Mer information', 'More information' => 'Mer information',
'Database size:' => 'Databasstorlek:', 'Database size:' => 'Databasstorlek:',
'Download the database' => 'Ladda ner databasen', 'Download the database' => 'Ladda ner databasen',
@ -110,7 +104,7 @@ return array(
'Open a task' => 'Öppna en uppgift', 'Open a task' => 'Öppna en uppgift',
'Do you really want to open this task: "%s"?' => 'Vill du verkligen öppna denna uppgift: "%s"?', 'Do you really want to open this task: "%s"?' => 'Vill du verkligen öppna denna uppgift: "%s"?',
'Back to the board' => 'Tillbaka till tavlan', 'Back to the board' => 'Tillbaka till tavlan',
'Created on %B %e, %G at %k:%M %p' => 'Skapad %d %B %G kl %H:%M', 'Created on %B %e, %Y at %k:%M %p' => 'Skapad %d %B %Y kl %H:%M',
'There is nobody assigned' => 'Det finns ingen tilldelad', 'There is nobody assigned' => 'Det finns ingen tilldelad',
'Column on the board:' => 'Kolumn på tavlan:', 'Column on the board:' => 'Kolumn på tavlan:',
'Status is open' => 'Statusen är öppen', 'Status is open' => 'Statusen är öppen',
@ -127,7 +121,7 @@ return array(
'The username must be unique' => 'Användarnamnet måste vara unikt', 'The username must be unique' => 'Användarnamnet måste vara unikt',
'The username must be alphanumeric' => 'Användarnamnet måste vara alfanumeriskt', 'The username must be alphanumeric' => 'Användarnamnet måste vara alfanumeriskt',
'The user id is required' => 'Användar-ID måste anges', 'The user id is required' => 'Användar-ID måste anges',
'Passwords doesn\'t matches' => 'Fel lösenord', 'Passwords don\'t match' => 'Lösenorden matchar inte',
'The confirmation is required' => 'Bekräftelse behövs.', 'The confirmation is required' => 'Bekräftelse behövs.',
'The column is required' => 'Kolumnen måste anges', 'The column is required' => 'Kolumnen måste anges',
'The project is required' => 'Projektet måste anges', 'The project is required' => 'Projektet måste anges',
@ -170,10 +164,10 @@ return array(
'Ready' => 'Denna månad', 'Ready' => 'Denna månad',
'Backlog' => 'Att göra', 'Backlog' => 'Att göra',
'Work in progress' => 'Pågående', 'Work in progress' => 'Pågående',
'Done' => 'Klart', 'Done' => 'Slutfört',
'Application version:' => 'Version:', 'Application version:' => 'Version:',
'Completed on %B %e, %G at %k:%M %p' => 'Slutfört %d %B %G kl %H:%M', 'Completed on %B %e, %Y at %k:%M %p' => 'Slutfört %d %B %Y kl %H:%M',
'%B %e, %G at %k:%M %p' => '%d %B %G kl %H:%M', '%B %e, %Y at %k:%M %p' => '%d %B %Y kl %H:%M',
'Date created' => 'Skapat datum', 'Date created' => 'Skapat datum',
'Date completed' => 'Slutfört datum', 'Date completed' => 'Slutfört datum',
'Id' => 'ID', 'Id' => 'ID',
@ -182,7 +176,7 @@ return array(
'List of projects' => 'Lista med projekt', 'List of projects' => 'Lista med projekt',
'Completed tasks for "%s"' => 'Slutföra uppgifter för "%s"', 'Completed tasks for "%s"' => 'Slutföra uppgifter för "%s"',
'%d closed tasks' => '%d stängda uppgifter', '%d closed tasks' => '%d stängda uppgifter',
'no task for this project' => 'inga uppgifter i detta projekt', 'No task for this project' => 'Inga uppgifter i detta projekt',
'Public link' => 'Publik länk', 'Public link' => 'Publik länk',
'There is no column in your project!' => 'Det saknas kolumner i ditt projekt!', 'There is no column in your project!' => 'Det saknas kolumner i ditt projekt!',
'Change assignee' => 'Ändra uppdragsinnehavare', 'Change assignee' => 'Ändra uppdragsinnehavare',
@ -190,14 +184,13 @@ return array(
'Timezone' => 'Tidszon', 'Timezone' => 'Tidszon',
'Sorry, I didn\'t found this information in my database!' => 'Informationen kunde inte hittas i databasen.', 'Sorry, I didn\'t found this information in my database!' => 'Informationen kunde inte hittas i databasen.',
'Page not found' => 'Sidan hittas inte', 'Page not found' => 'Sidan hittas inte',
'Story Points' => 'Ungefärligt antal timmar', 'Complexity' => 'Ungefärligt antal timmar',
'limit' => 'max', 'limit' => 'max',
'Task limit' => 'Uppgiftsbegränsning', 'Task limit' => 'Uppgiftsbegränsning',
'This value must be greater than %d' => 'Värdet måste vara större än %d', 'This value must be greater than %d' => 'Värdet måste vara större än %d',
'Edit project access list' => 'Ändra projektåtkomst lista', 'Edit project access list' => 'Ändra projektåtkomst lista',
'Edit users access' => 'Användaråtkomst', 'Edit users access' => 'Användaråtkomst',
'Allow this user' => 'Tillåt användare', 'Allow this user' => 'Tillåt användare',
'Project access list for "%s"' => 'Behörighetslista för "%s"',
'Only those users have access to this project:' => 'Bara de användarna har tillgång till detta projekt.', 'Only those users have access to this project:' => 'Bara de användarna har tillgång till detta projekt.',
'Don\'t forget that administrators have access to everything.' => 'Glöm inte att administratörerna har rätt att göra allt.', 'Don\'t forget that administrators have access to everything.' => 'Glöm inte att administratörerna har rätt att göra allt.',
'revoke' => 'Dra tillbaka behörighet', 'revoke' => 'Dra tillbaka behörighet',
@ -205,7 +198,6 @@ return array(
'User' => 'Användare', 'User' => 'Användare',
'Everybody have access to this project.' => 'Alla har tillgång till detta projekt.', 'Everybody have access to this project.' => 'Alla har tillgång till detta projekt.',
'You are not allowed to access to this project.' => 'Du har inte tillgång till detta projekt.', 'You are not allowed to access to this project.' => 'Du har inte tillgång till detta projekt.',
'%B %e, %G at %k:%M %p' => '%d %B %G kl %H:%M',
'Comments' => 'Kommentarer', 'Comments' => 'Kommentarer',
'Post comment' => 'Ladda upp kommentar', 'Post comment' => 'Ladda upp kommentar',
'Write your text in Markdown' => 'Exempelsyntax för text', 'Write your text in Markdown' => 'Exempelsyntax för text',
@ -217,11 +209,11 @@ return array(
'The description is required' => 'En beskrivning måste lämnas', 'The description is required' => 'En beskrivning måste lämnas',
'Edit this task' => 'Ändra denna uppgift', 'Edit this task' => 'Ändra denna uppgift',
'Due Date' => 'Måldatum', 'Due Date' => 'Måldatum',
'm/d/Y' => 'd/m/Y', // Date format parsed with php 'm/d/Y' => 'd/m/Y',
'month/day/year' => 'dag/månad/år', // Help shown to the user 'month/day/year' => 'dag/månad/år',
'Invalid date' => 'Ej tillåtet datum', 'Invalid date' => 'Ej tillåtet datum',
'Must be done before %B %e, %G' => 'Måste vara klart innan %B %e, %G', 'Must be done before %B %e, %Y' => 'Måste vara klart innan %B %e, %Y',
'%B %e, %G' => '%d %B %G', '%B %e, %Y' => '%d %B %Y',
'Automatic actions' => 'Automatiska åtgärder', 'Automatic actions' => 'Automatiska åtgärder',
'Your automatic action have been created successfully.' => 'Din automatiska åtgärd har skapats.', 'Your automatic action have been created successfully.' => 'Din automatiska åtgärd har skapats.',
'Unable to create your automatic action.' => 'Kunde inte skapa din automatiska åtgärd.', 'Unable to create your automatic action.' => 'Kunde inte skapa din automatiska åtgärd.',
@ -230,6 +222,7 @@ return array(
'Action removed successfully.' => 'Åtgärden har tagits bort.', 'Action removed successfully.' => 'Åtgärden har tagits bort.',
'Automatic actions for the project "%s"' => 'Automatiska åtgärder för projektet "%s"', 'Automatic actions for the project "%s"' => 'Automatiska åtgärder för projektet "%s"',
'Defined actions' => 'Definierade åtgärder', 'Defined actions' => 'Definierade åtgärder',
'Add an action' => 'Lägg till en åtgärd',
'Event name' => 'Händelsenamn', 'Event name' => 'Händelsenamn',
'Action name' => 'Åtgärdsnamn', 'Action name' => 'Åtgärdsnamn',
'Action parameters' => 'Åtgärdsparametrar', 'Action parameters' => 'Åtgärdsparametrar',
@ -256,7 +249,7 @@ return array(
'Position' => 'Position', 'Position' => 'Position',
'Move Up' => 'Flytta upp', 'Move Up' => 'Flytta upp',
'Move Down' => 'Flytta ned', 'Move Down' => 'Flytta ned',
'Kopiera till ett annat projekt' => '', 'Duplicate to another project' => 'Kopiera till ett annat projekt',
'Duplicate' => 'Kopiera uppgiften', 'Duplicate' => 'Kopiera uppgiften',
'link' => 'länk', 'link' => 'länk',
'Update this comment' => 'Uppdatera kommentaren', 'Update this comment' => 'Uppdatera kommentaren',
@ -280,7 +273,7 @@ return array(
'IP address' => 'IP-adress', 'IP address' => 'IP-adress',
'User agent' => 'Användaragent/webbläsare', 'User agent' => 'Användaragent/webbläsare',
'Persistent connections' => 'Beständiga anslutningar', 'Persistent connections' => 'Beständiga anslutningar',
'No session' => 'Ingen session', 'No session.' => 'Ingen session.',
'Expiration date' => 'Förfallodatum', 'Expiration date' => 'Förfallodatum',
'Remember Me' => 'Kom ihåg mig', 'Remember Me' => 'Kom ihåg mig',
'Creation date' => 'Skapatdatum', 'Creation date' => 'Skapatdatum',
@ -366,7 +359,6 @@ return array(
'The time must be a numeric value' => 'Tiden måste ha ett numeriskt värde', 'The time must be a numeric value' => 'Tiden måste ha ett numeriskt värde',
'Todo' => 'Att göra', 'Todo' => 'Att göra',
'In progress' => 'Pågående', 'In progress' => 'Pågående',
'Done' => 'Slutfört',
'Sub-task removed successfully.' => 'Deluppgiften har tagits bort.', 'Sub-task removed successfully.' => 'Deluppgiften har tagits bort.',
'Unable to remove this sub-task.' => 'Kunde inte ta bort denna deluppgift.', 'Unable to remove this sub-task.' => 'Kunde inte ta bort denna deluppgift.',
'Sub-task updated successfully.' => 'Deluppgiften har uppdaterats.', 'Sub-task updated successfully.' => 'Deluppgiften har uppdaterats.',
@ -383,7 +375,132 @@ return array(
'Unable to unlink your GitHub Account.' => 'Kunde inte koppla ifrån ditt GitHub-konto.', 'Unable to unlink your GitHub Account.' => 'Kunde inte koppla ifrån ditt GitHub-konto.',
'Login with my GitHub Account' => 'Logga in med mitt GitHub-konto', 'Login with my GitHub Account' => 'Logga in med mitt GitHub-konto',
'Link my GitHub Account' => 'Anslut mitt GitHub-konto', 'Link my GitHub Account' => 'Anslut mitt GitHub-konto',
'Unlink my GitHub Account' => 'Koppla ifrån mitt GitHub-konto', 'Unlink my GitHub Account' => 'Koppla ifrån mitt GitHub-konto',
'Created by %s' => 'Skapad av %s', 'Created by %s' => 'Skapad av %s',
'Last modified on %B %e, %G at %k:%M %p' => 'Senaste ändring %B %e, %G kl %k:%M %p'', 'Last modified on %B %e, %Y at %k:%M %p' => 'Senaste ändring %B %e, %Y kl %k:%M %p',
'Tasks Export' => 'Exportera uppgifter',
'Tasks exportation for "%s"' => 'Exportera uppgifter för "%s"',
'Start Date' => 'Startdatum',
'End Date' => 'Slutdatum',
'Execute' => 'Utför',
'Task Id' => 'Uppgift ID',
'Creator' => 'Skapare',
'Modification date' => 'Ändringsdatum',
'Completion date' => 'Slutfört datum',
'Webhook URL for task creation' => 'Webhook URL för att skapa uppgift',
'Webhook URL for task modification' => 'Webhook URL för att ändra uppgift',
'Clone' => 'Klona',
'Clone Project' => 'Klona projekt',
'Project cloned successfully.' => 'Projektet har klonats.',
'Unable to clone this project.' => 'Kunde inte klona projektet.',
'Email notifications' => 'Epostnotiser',
'Enable email notifications' => 'Aktivera epostnotiser',
'Task position:' => 'Uppgiftsposition:',
'The task #%d have been opened.' => 'Uppgiften #%d har öppnats.',
'The task #%d have been closed.' => 'Uppgiften #%d har stängts.',
'Sub-task updated' => 'Deluppgift uppdaterad',
'Title:' => 'Titel:',
'Status:' => 'Status:',
'Assignee:' => 'Tilldelad:',
'Time tracking:' => 'Tidsspårning',
'New sub-task' => 'Ny deluppgift',
'New attachment added "%s"' => 'Ny bifogning tillagd "%s"',
'Comment updated' => 'Kommentaren har uppdaterats',
'New comment posted by %s' => 'Ny kommentar postad av %s',
'List of due tasks for the project "%s"' => 'Lista med uppgifter för projektet "%s"',
// '[%s][New attachment] %s (#%d)' => '',
// '[%s][New comment] %s (#%d)' => '',
// '[%s][Comment updated] %s (#%d)' => '',
// '[%s][New subtask] %s (#%d)' => '',
// '[%s][Subtask updated] %s (#%d)' => '',
// '[%s][New task] %s (#%d)' => '',
// '[%s][Task updated] %s (#%d)' => '',
// '[%s][Task closed] %s (#%d)' => '',
// '[%s][Task opened] %s (#%d)' => '',
// '[%s][Due tasks]' => '',
'[Kanboard] Notification' => '[Kanboard] Notis',
'I want to receive notifications only for those projects:' => 'Jag vill endast få notiser för dessa projekt:',
'view the task on Kanboard' => 'Visa uppgiften på Kanboard',
// 'Public access' => '',
// 'Categories management' => '',
// 'Users management' => '',
// 'Active tasks' => '',
// 'Disable public access' => '',
// 'Enable public access' => '',
// 'Active projects' => '',
// 'Inactive projects' => '',
// 'Public access disabled' => '',
// 'Do you really want to disable this project: "%s"?' => '',
// 'Do you really want to duplicate this project: "%s"?' => '',
// 'Do you really want to enable this project: "%s"?' => '',
// 'Project activation' => '',
// 'Move the task to another project' => '',
// 'Move to another project' => '',
// 'Do you really want to duplicate this task?' => '',
// 'Duplicate a task' => '',
// 'External accounts' => '',
// 'Account type' => '',
// 'Local' => '',
// 'Remote' => '',
// 'Enabled' => '',
// 'Disabled' => '',
// 'Google account linked' => '',
// 'Github account linked' => '',
// 'Username:' => '',
// 'Name:' => '',
// 'Email:' => '',
// 'Default project:' => '',
// 'Notifications:' => '',
// 'Group:' => '',
// 'Regular user' => '',
// 'Account type:' => '',
// 'Edit profile' => '',
// 'Change password' => '',
// 'Password modification' => '',
// 'External authentications' => '',
// 'Google Account' => '',
// 'Github Account' => '',
// 'Never connected.' => '',
// 'No account linked.' => '',
// 'Account linked.' => '',
// 'No external authentication enabled.' => '',
// 'Password modified successfully.' => '',
// 'Unable to change the password.' => '',
// 'Change category for the task "%s"' => '',
// 'Change category' => '',
// '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '',
// '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '',
// '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// 'Assigned to %s with an estimate of %s/%sh' => '',
// 'Not assigned, estimate of %sh' => '',
// '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s\'s activity' => '',
// 'No activity.' => '',
// 'RSS feed' => '',
// '%s updated a comment on the task #%d' => '',
// '%s commented on the task #%d' => '',
// '%s updated a subtask for the task #%d' => '',
// '%s created a subtask for the task #%d' => '',
// '%s updated the task #%d' => '',
// '%s created the task #%d' => '',
// '%s closed the task #%d' => '',
// '%s open the task #%d' => '',
// '%s moved the task #%d to the column "%s"' => '',
// '%s moved the task #%d to the position %d in the column "%s"' => '',
// 'Activity' => '',
// 'Default values are "%s"' => '',
// 'Default columns for new projects (Comma-separated)' => '',
// 'Task assignee change' => '',
// '%s change the assignee of the task #%d' => '',
// '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '[%s][Column Change] %s (#%d)' => '',
// '[%s][Position Change] %s (#%d)' => '',
// '[%s][Assignee Change] %s (#%d)' => '',
// 'New password for the user "%s"' => '',
); );

View file

@ -1,13 +1,6 @@
<?php <?php
return array( return array(
'English' => '英语',
'French' => '法语',
'Polish' => '波兰语',
'Portuguese (Brazilian)' => '葡萄牙语 (巴西)',
'Spanish' => '西班牙语',
'German' => '德语',
'Chinese (Simplified)' => '中文(简体)',
'None' => '未知', 'None' => '未知',
'edit' => '修改', 'edit' => '修改',
'Edit' => '修改', 'Edit' => '修改',
@ -30,19 +23,15 @@ return array(
'Official website:' => '官方网站:', 'Official website:' => '官方网站:',
'Unassigned' => '未指定', 'Unassigned' => '未指定',
'View this task' => '查看该任务', 'View this task' => '查看该任务',
'Add a sub-task' => '添加一个子任务',
'Original Estimate' => '初步预计耗时',
'hours' => '小时',
'Create another sub-task' => '创建另一个子任务',
'Remove user' => '移除用户', 'Remove user' => '移除用户',
'Do you really want to remove this user: "%s"?' => '你确定要移除这个用户吗:"%s"', 'Do you really want to remove this user: "%s"?' => '你确定要移除这个用户吗:"%s"',
'New user' => '新用户', 'New user' => '新用户',
'All users' => '所有用户', 'All users' => '所有用户',
'Username' => '用户名', 'Username' => '用户名',
'Password' => '密码', 'Password' => '密码',
'Default Project' => '默认项目', 'Default project' => '默认项目',
'Administrator' => '管理员', 'Administrator' => '管理员',
'Sign in' => '注册', 'Sign in' => '登录',
'Users' => '用户组', 'Users' => '用户组',
'No user' => '没有用户', 'No user' => '没有用户',
'Forbidden' => '禁止', 'Forbidden' => '禁止',
@ -62,6 +51,7 @@ return array(
'Status' => '状态', 'Status' => '状态',
'Tasks' => '任务群', 'Tasks' => '任务群',
'Board' => '看板', 'Board' => '看板',
'Actions' => '行为',
'Inactive' => '未激活', 'Inactive' => '未激活',
'Active' => '激活', 'Active' => '激活',
'Column %d' => '第%d栏目', 'Column %d' => '第%d栏目',
@ -94,6 +84,7 @@ return array(
'Application settings' => '应用设置', 'Application settings' => '应用设置',
'Language' => '语言', 'Language' => '语言',
'Webhooks token:' => '页面钩子令牌:', 'Webhooks token:' => '页面钩子令牌:',
// 'API token:' => '',
'More information' => '更多信息', 'More information' => '更多信息',
'Database size:' => '数据库大小:', 'Database size:' => '数据库大小:',
'Download the database' => '下载数据库', 'Download the database' => '下载数据库',
@ -113,7 +104,7 @@ return array(
'Open a task' => '开一个项目', 'Open a task' => '开一个项目',
'Do you really want to open this task: "%s"?' => '你确定要开这个项目吗?"%s"', 'Do you really want to open this task: "%s"?' => '你确定要开这个项目吗?"%s"',
'Back to the board' => '回到看板', 'Back to the board' => '回到看板',
'Created on %B %e, %G at %k:%M %p' => '在%d/%m/%Y %H:%M创建', 'Created on %B %e, %Y at %k:%M %p' => '在%d/%m/%Y %H:%M创建',
'There is nobody assigned' => '无人负责', 'There is nobody assigned' => '无人负责',
'Column on the board:' => '看板上的栏目:', 'Column on the board:' => '看板上的栏目:',
'Status is open' => '开放状态', 'Status is open' => '开放状态',
@ -130,7 +121,7 @@ return array(
'The username must be unique' => '用户名必须唯一', 'The username must be unique' => '用户名必须唯一',
'The username must be alphanumeric' => '用户名必须是英文字符或数字组成', 'The username must be alphanumeric' => '用户名必须是英文字符或数字组成',
'The user id is required' => '用户id是必须的', 'The user id is required' => '用户id是必须的',
'Passwords doesn\'t matches' => '密码不匹配', // 'Passwords don\'t match' => '',
'The confirmation is required' => '需要确认', 'The confirmation is required' => '需要确认',
'The column is required' => '需要指定栏目', 'The column is required' => '需要指定栏目',
'The project is required' => '需要指定项目', 'The project is required' => '需要指定项目',
@ -175,8 +166,8 @@ return array(
'Work in progress' => '工作进行中', 'Work in progress' => '工作进行中',
'Done' => '完成', 'Done' => '完成',
'Application version:' => '应用程序版本', 'Application version:' => '应用程序版本',
'Completed on %B %e, %G at %k:%M %p' => '于%Y年%m月%d日%H时%M分完成', 'Completed on %B %e, %Y at %k:%M %p' => '于%Y年%m月%d日%H时%M分完成',
'%B %e, %G at %k:%M %p' => '%Y年%m月%d日%H时%M分', '%B %e, %Y at %k:%M %p' => '%Y年%m月%d日%H时%M分',
'Date created' => '创建时间', 'Date created' => '创建时间',
'Date completed' => '完成时间', 'Date completed' => '完成时间',
'Id' => '昵称', 'Id' => '昵称',
@ -185,7 +176,7 @@ return array(
'List of projects' => '项目列表', 'List of projects' => '项目列表',
'Completed tasks for "%s"' => '任务因"%s"原因完成', 'Completed tasks for "%s"' => '任务因"%s"原因完成',
'%d closed tasks' => '%d个已关闭任务', '%d closed tasks' => '%d个已关闭任务',
'no task for this project' => '该项目尚无任务', 'No task for this project' => '该项目尚无任务',
'Public link' => '公开链接', 'Public link' => '公开链接',
'There is no column in your project!' => '该项目尚无栏目项!', 'There is no column in your project!' => '该项目尚无栏目项!',
'Change assignee' => '被指派人变更', 'Change assignee' => '被指派人变更',
@ -193,14 +184,13 @@ return array(
'Timezone' => '时区', 'Timezone' => '时区',
'Sorry, I didn\'t found this information in my database!' => '抱歉,无法在数据库中找到该信息!', 'Sorry, I didn\'t found this information in my database!' => '抱歉,无法在数据库中找到该信息!',
'Page not found' => '页面未找到', 'Page not found' => '页面未找到',
'Story Points' => '评估分值', 'Complexity' => '评估分值',
'limit' => '限制', 'limit' => '限制',
'Task limit' => '任务限制', 'Task limit' => '任务限制',
'This value must be greater than %d' => '该数值必须大于%d', 'This value must be greater than %d' => '该数值必须大于%d',
'Edit project access list' => '编辑项目存取列表', 'Edit project access list' => '编辑项目存取列表',
'Edit users access' => '编辑用户存取权限', 'Edit users access' => '编辑用户存取权限',
'Allow this user' => '允许该用户', 'Allow this user' => '允许该用户',
'Project access list for "%s"' => '"%s"的项目存取列表',
'Only those users have access to this project:' => '只有这些用户有该项目的存取权限:', 'Only those users have access to this project:' => '只有这些用户有该项目的存取权限:',
'Don\'t forget that administrators have access to everything.' => '别忘了管理员有一切的权限。', 'Don\'t forget that administrators have access to everything.' => '别忘了管理员有一切的权限。',
'revoke' => '撤销', 'revoke' => '撤销',
@ -219,15 +209,12 @@ return array(
'The description is required' => '必须得有描述', 'The description is required' => '必须得有描述',
'Edit this task' => '编辑该任务', 'Edit this task' => '编辑该任务',
'Due Date' => '到期', 'Due Date' => '到期',
'm/d/Y' => 'Y/m/d', // Date format parsed with php 'm/d/Y' => 'Y/m/d',
'month/day/year' => '年/月/日', // Help shown to the user 'month/day/year' => '年/月/日',
'Invalid date' => '无效日期', 'Invalid date' => '无效日期',
'Must be done before %B %e, %G' => '必须在%Y年%m月%d日前完成', 'Must be done before %B %e, %Y' => '必须在%Y年%m月%d日前完成',
'%B %e, %G' => '%Y/%m/%d', '%B %e, %Y' => '%Y/%m/%d',
'Automatic actions' => '自动行为', 'Automatic actions' => '自动行为',
'Add an action' => '添加一个行为',
'Assign automatically a color based on a category' => '基于一个分类自动指派颜色',
'Assign automatically a category based on a color' => '基于一种颜色自动指派分类',
'Your automatic action have been created successfully.' => '您的自动行为已成功创建', 'Your automatic action have been created successfully.' => '您的自动行为已成功创建',
'Unable to create your automatic action.' => '无法为您创建自动行为。', 'Unable to create your automatic action.' => '无法为您创建自动行为。',
'Remove an action' => '移除一个行为。', 'Remove an action' => '移除一个行为。',
@ -235,11 +222,11 @@ return array(
'Action removed successfully.' => '成功移除行为。', 'Action removed successfully.' => '成功移除行为。',
'Automatic actions for the project "%s"' => '项目"%s"的自动行为', 'Automatic actions for the project "%s"' => '项目"%s"的自动行为',
'Defined actions' => '已定义的行为', 'Defined actions' => '已定义的行为',
'Add an action' => '添加一个行为',
'Event name' => '事件名称', 'Event name' => '事件名称',
'Action name' => '行为名称', 'Action name' => '行为名称',
'Action parameters' => '行为参数', 'Action parameters' => '行为参数',
'Action' => '行为', 'Action' => '行为',
'Actions' => '行为',
'Event' => '事件', 'Event' => '事件',
'When the selected event occurs execute the corresponding action.' => '当所选事件发生时执行相应行为。', 'When the selected event occurs execute the corresponding action.' => '当所选事件发生时执行相应行为。',
'Next step' => '下一步', 'Next step' => '下一步',
@ -277,7 +264,6 @@ return array(
'Current password for the user "%s"' => '用户"%s"的当前密码', 'Current password for the user "%s"' => '用户"%s"的当前密码',
'The current password is required' => '需要输入当前密码', 'The current password is required' => '需要输入当前密码',
'Wrong password' => '密码错误', 'Wrong password' => '密码错误',
'Confirmation' => '再输一次新密码',
'Reset all tokens' => '重置所有令牌', 'Reset all tokens' => '重置所有令牌',
'All tokens have been regenerated.' => '所有令牌都重新生成了。', 'All tokens have been regenerated.' => '所有令牌都重新生成了。',
'Unknown' => '未知', 'Unknown' => '未知',
@ -287,7 +273,7 @@ return array(
'IP address' => 'IP地址', 'IP address' => 'IP地址',
'User agent' => '用户代理', 'User agent' => '用户代理',
'Persistent connections' => '持续连接', 'Persistent connections' => '持续连接',
'No session' => '无会话', 'No session.' => '无会话',
'Expiration date' => '过期', 'Expiration date' => '过期',
'Remember Me' => '记住我', 'Remember Me' => '记住我',
'Creation date' => '创建日期', 'Creation date' => '创建日期',
@ -320,7 +306,8 @@ return array(
'Unable to remove this task.' => '无法移除该任务。', 'Unable to remove this task.' => '无法移除该任务。',
'Remove a task' => '移除一个任务', 'Remove a task' => '移除一个任务',
'Do you really want to remove this task: "%s"?' => '确定要溢出该任务"%s"吗?', 'Do you really want to remove this task: "%s"?' => '确定要溢出该任务"%s"吗?',
'Assign a color to a specific category' => '指派颜色给一个特定分类', 'Assign automatically a color based on a category' => '基于一个分类自动指派颜色',
'Assign automatically a category based on a color' => '基于一种颜色自动指派分类',
'Task creation or modification' => '任务创建或修改', 'Task creation or modification' => '任务创建或修改',
'Category' => '分类', 'Category' => '分类',
'Category:' => '分类:', 'Category:' => '分类:',
@ -359,20 +346,19 @@ return array(
// 'Spent:' => '', // 'Spent:' => '',
// 'Do you really want to remove this sub-task?' => '', // 'Do you really want to remove this sub-task?' => '',
// 'Remaining:' => '', // 'Remaining:' => '',
// 'hours' => '', 'hours' => '小时',
// 'spent' => '', // 'spent' => '',
// 'estimated' => '', // 'estimated' => '',
// 'Sub-Tasks' => '', // 'Sub-Tasks' => '',
// 'Add a sub-task' => '', 'Add a sub-task' => '添加一个子任务',
// 'Original Estimate' => '', 'Original Estimate' => '初步预计耗时',
// 'Create another sub-task' => '', 'Create another sub-task' => '创建另一个子任务',
// 'Time Spent' => '', // 'Time Spent' => '',
// 'Edit a sub-task' => '', // 'Edit a sub-task' => '',
// 'Remove a sub-task' => '', // 'Remove a sub-task' => '',
// 'The time must be a numeric value' => '', // 'The time must be a numeric value' => '',
// 'Todo' => '', // 'Todo' => '',
// 'In progress' => '', // 'In progress' => '',
// 'Done' => '',
// 'Sub-task removed successfully.' => '', // 'Sub-task removed successfully.' => '',
// 'Unable to remove this sub-task.' => '', // 'Unable to remove this sub-task.' => '',
// 'Sub-task updated successfully.' => '', // 'Sub-task updated successfully.' => '',
@ -389,7 +375,132 @@ return array(
// 'Unable to unlink your GitHub Account.' => '', // 'Unable to unlink your GitHub Account.' => '',
// 'Login with my GitHub Account' => '', // 'Login with my GitHub Account' => '',
// 'Link my GitHub Account' => '', // 'Link my GitHub Account' => '',
// 'Unlink my GitHub Account' => '', // 'Unlink my GitHub Account' => '',
// 'Created by %s' => 'Créé par %s', // 'Created by %s' => '',
// 'Last modified on %B %e, %G at %k:%M %p' => '', // 'Last modified on %B %e, %Y at %k:%M %p' => '',
// 'Tasks Export' => '',
// 'Tasks exportation for "%s"' => '',
// 'Start Date' => '',
// 'End Date' => '',
// 'Execute' => '',
// 'Task Id' => '',
// 'Creator' => '',
// 'Modification date' => '',
// 'Completion date' => '',
// 'Webhook URL for task creation' => '',
// 'Webhook URL for task modification' => '',
// 'Clone' => '',
// 'Clone Project' => '',
// 'Project cloned successfully.' => '',
// 'Unable to clone this project.' => '',
// 'Email notifications' => '',
// 'Enable email notifications' => '',
// 'Task position:' => '',
// 'The task #%d have been opened.' => '',
// 'The task #%d have been closed.' => '',
// 'Sub-task updated' => '',
// 'Title:' => '',
// 'Status:' => '',
// 'Assignee:' => '',
// 'Time tracking:' => '',
// 'New sub-task' => '',
// 'New attachment added "%s"' => '',
// 'Comment updated' => '',
// 'New comment posted by %s' => '',
// 'List of due tasks for the project "%s"' => '',
// '[%s][New attachment] %s (#%d)' => '',
// '[%s][New comment] %s (#%d)' => '',
// '[%s][Comment updated] %s (#%d)' => '',
// '[%s][New subtask] %s (#%d)' => '',
// '[%s][Subtask updated] %s (#%d)' => '',
// '[%s][New task] %s (#%d)' => '',
// '[%s][Task updated] %s (#%d)' => '',
// '[%s][Task closed] %s (#%d)' => '',
// '[%s][Task opened] %s (#%d)' => '',
// '[%s][Due tasks]' => '',
// '[Kanboard] Notification' => '',
// 'I want to receive notifications only for those projects:' => '',
// 'view the task on Kanboard' => '',
// 'Public access' => '',
// 'Categories management' => '',
// 'Users management' => '',
// 'Active tasks' => '',
// 'Disable public access' => '',
// 'Enable public access' => '',
// 'Active projects' => '',
// 'Inactive projects' => '',
// 'Public access disabled' => '',
// 'Do you really want to disable this project: "%s"?' => '',
// 'Do you really want to duplicate this project: "%s"?' => '',
// 'Do you really want to enable this project: "%s"?' => '',
// 'Project activation' => '',
// 'Move the task to another project' => '',
// 'Move to another project' => '',
// 'Do you really want to duplicate this task?' => '',
// 'Duplicate a task' => '',
// 'External accounts' => '',
// 'Account type' => '',
// 'Local' => '',
// 'Remote' => '',
// 'Enabled' => '',
// 'Disabled' => '',
// 'Google account linked' => '',
// 'Github account linked' => '',
// 'Username:' => '',
// 'Name:' => '',
// 'Email:' => '',
// 'Default project:' => '',
// 'Notifications:' => '',
// 'Group:' => '',
// 'Regular user' => '',
// 'Account type:' => '',
// 'Edit profile' => '',
// 'Change password' => '',
// 'Password modification' => '',
// 'External authentications' => '',
// 'Google Account' => '',
// 'Github Account' => '',
// 'Never connected.' => '',
// 'No account linked.' => '',
// 'Account linked.' => '',
// 'No external authentication enabled.' => '',
// 'Password modified successfully.' => '',
// 'Unable to change the password.' => '',
// 'Change category for the task "%s"' => '',
// 'Change category' => '',
// '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '',
// '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '',
// '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// 'Assigned to %s with an estimate of %s/%sh' => '',
// 'Not assigned, estimate of %sh' => '',
// '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '%s\'s activity' => '',
// 'No activity.' => '',
// 'RSS feed' => '',
// '%s updated a comment on the task #%d' => '',
// '%s commented on the task #%d' => '',
// '%s updated a subtask for the task #%d' => '',
// '%s created a subtask for the task #%d' => '',
// '%s updated the task #%d' => '',
// '%s created the task #%d' => '',
// '%s closed the task #%d' => '',
// '%s open the task #%d' => '',
// '%s moved the task #%d to the column "%s"' => '',
// '%s moved the task #%d to the position %d in the column "%s"' => '',
// 'Activity' => '',
// 'Default values are "%s"' => '',
// 'Default columns for new projects (Comma-separated)' => '',
// 'Task assignee change' => '',
// '%s change the assignee of the task #%d' => '',
// '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
// '[%s][Column Change] %s (#%d)' => '',
// '[%s][Position Change] %s (#%d)' => '',
// '[%s][Assignee Change] %s (#%d)' => '',
// 'New password for the user "%s"' => '',
); );

View file

@ -18,8 +18,9 @@ class Acl extends Base
*/ */
private $public_actions = array( private $public_actions = array(
'user' => array('login', 'check', 'google', 'github'), 'user' => array('login', 'check', 'google', 'github'),
'task' => array('add'), 'task' => array('add', 'readonly'),
'board' => array('readonly'), 'board' => array('readonly'),
'project' => array('feed'),
); );
/** /**
@ -30,29 +31,13 @@ class Acl extends Base
*/ */
private $user_actions = array( private $user_actions = array(
'app' => array('index'), 'app' => array('index'),
'board' => array('index', 'show', 'assign', 'assigntask', 'save', 'check'), 'board' => array('index', 'show', 'save', 'check', 'changeassignee', 'updateassignee', 'changecategory', 'updatecategory'),
'project' => array('tasks', 'index', 'forbidden', 'search'), 'project' => array('tasks', 'index', 'forbidden', 'search', 'export', 'show', 'activity'),
'user' => array('index', 'edit', 'update', 'forbidden', 'logout', 'index', 'unlinkgoogle', 'unlinkgithub'), 'user' => array('index', 'edit', 'forbidden', 'logout', 'index', 'show', 'external', 'unlinkgoogle', 'unlinkgithub', 'sessions', 'removesession', 'last', 'notifications', 'password'),
'config' => array('index', 'removeremembermetoken'),
'comment' => array('create', 'save', 'confirm', 'remove', 'update', 'edit', 'forbidden'), 'comment' => array('create', 'save', 'confirm', 'remove', 'update', 'edit', 'forbidden'),
'file' => array('create', 'save', 'download', 'confirm', 'remove', 'open', 'image'), 'file' => array('create', 'save', 'download', 'confirm', 'remove', 'open', 'image'),
'subtask' => array('create', 'save', 'edit', 'update', 'confirm', 'remove'), 'subtask' => array('create', 'save', 'edit', 'update', 'confirm', 'remove'),
'task' => array( 'task' => array('show', 'create', 'save', 'edit', 'update', 'close', 'open', 'duplicate', 'remove', 'description', 'move', 'copy'),
'show',
'create',
'save',
'edit',
'update',
'close',
'confirmclose',
'open',
'confirmopen',
'duplicate',
'remove',
'confirmremove',
'editdescription',
'savedescription',
),
); );
/** /**

View file

@ -41,6 +41,7 @@ class Action extends Base
'TaskAssignSpecificUser' => t('Assign the task to a specific user'), 'TaskAssignSpecificUser' => t('Assign the task to a specific user'),
'TaskAssignCurrentUser' => t('Assign the task to the person who does the action'), 'TaskAssignCurrentUser' => t('Assign the task to the person who does the action'),
'TaskDuplicateAnotherProject' => t('Duplicate the task to another project'), 'TaskDuplicateAnotherProject' => t('Duplicate the task to another project'),
'TaskMoveAnotherProject' => t('Move the task to another project'),
'TaskAssignColorUser' => t('Assign a color to a specific user'), 'TaskAssignColorUser' => t('Assign a color to a specific user'),
'TaskAssignColorCategory' => t('Assign automatically a color based on a category'), 'TaskAssignColorCategory' => t('Assign automatically a color based on a category'),
'TaskAssignCategoryColor' => t('Assign automatically a category based on a color'), 'TaskAssignCategoryColor' => t('Assign automatically a category based on a color'),
@ -63,6 +64,7 @@ class Action extends Base
Task::EVENT_OPEN => t('Open a closed task'), Task::EVENT_OPEN => t('Open a closed task'),
Task::EVENT_CLOSE => t('Closing a task'), Task::EVENT_CLOSE => t('Closing a task'),
Task::EVENT_CREATE_UPDATE => t('Task creation or modification'), Task::EVENT_CREATE_UPDATE => t('Task creation or modification'),
Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'),
); );
} }
@ -217,34 +219,81 @@ class Action extends Base
* @param integer $project_id Project id * @param integer $project_id Project id
* @throws \LogicException * @throws \LogicException
* @return \Core\Listener Action Instance * @return \Core\Listener Action Instance
* @throw LogicException
*/ */
public function load($name, $project_id) public function load($name, $project_id)
{ {
switch ($name) { $className = '\Action\\'.$name;
case 'TaskClose':
$className = '\Action\TaskClose'; if ($name === 'TaskAssignCurrentUser') {
return new $className($project_id, new Task($this->db, $this->event)); return new $className($project_id, new Task($this->registry), new Acl($this->registry));
case 'TaskAssignCurrentUser': }
$className = '\Action\TaskAssignCurrentUser'; else {
return new $className($project_id, new Task($this->db, $this->event), new Acl($this->db, $this->event)); return new $className($project_id, new Task($this->registry));
case 'TaskAssignSpecificUser': }
$className = '\Action\TaskAssignSpecificUser'; }
return new $className($project_id, new Task($this->db, $this->event));
case 'TaskDuplicateAnotherProject': /**
$className = '\Action\TaskDuplicateAnotherProject'; * Copy Actions and related Actions Parameters from a project to another one
return new $className($project_id, new Task($this->db, $this->event)); *
case 'TaskAssignColorUser': * @author Antonio Rabelo
$className = '\Action\TaskAssignColorUser'; * @param integer $project_from Project Template
return new $className($project_id, new Task($this->db, $this->event)); * @return integer $project_to Project that receives the copy
case 'TaskAssignColorCategory': * @return boolean
$className = '\Action\TaskAssignColorCategory'; */
return new $className($project_id, new Task($this->db, $this->event)); public function duplicate($project_from, $project_to)
case 'TaskAssignCategoryColor': {
$className = '\Action\TaskAssignCategoryColor'; $actionTemplate = $this->action->getAllByProject($project_from);
return new $className($project_id, new Task($this->db, $this->event));
foreach ($actionTemplate as $action) {
unset($action['id']);
$action['project_id'] = $project_to;
$actionParams = $action['params'];
unset($action['params']);
if (! $this->db->table(self::TABLE)->save($action)) {
return false;
}
$action_clone_id = $this->db->getConnection()->getLastId();
foreach ($actionParams as $param) {
unset($param['id']);
$param['value'] = $this->resolveDuplicatedParameters($param, $project_to);
$param['action_id'] = $action_clone_id;
if (! $this->db->table(self::TABLE_PARAMS)->save($param)) {
return false;
}
}
}
return true;
}
/**
* Resolve type of action value from a project to the respective value in another project
*
* @author Antonio Rabelo
* @param integer $param An action parameter
* @return integer $project_to Project to find the corresponding values
* @return mixed The corresponding values from $project_to
*/
private function resolveDuplicatedParameters($param, $project_to)
{
switch($param['name']) {
case 'project_id':
return $project_to;
case 'category_id':
$categoryTemplate = $this->category->getById($param['value']);
$categoryFromNewProject = $this->db->table(Category::TABLE)->eq('project_id', $project_to)->eq('name', $categoryTemplate['name'])->findOne();
return $categoryFromNewProject['id'];
case 'column_id':
$boardTemplate = $this->board->getColumn($param['value']);
$boardFromNewProject = $this->db->table(Board::TABLE)->eq('project_id', $project_to)->eq('title', $boardTemplate['title'])->findOne();
return $boardFromNewProject['id'];
default: default:
throw new LogicException('Action not found: '.$name); return $param['value'];
} }
} }

View file

@ -0,0 +1,136 @@
<?php
namespace Model;
use Auth\Database;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
/**
* Authentication model
*
* @package model
* @author Frederic Guillot
*/
class Authentication extends Base
{
/**
* Load automatically an authentication backend
*
* @access public
* @param string $name Backend class name
* @return mixed
*/
public function backend($name)
{
if (! isset($this->registry->$name)) {
$class = '\Auth\\'.ucfirst($name);
$this->registry->$name = new $class($this->registry);
}
return $this->registry->shared($name);
}
/**
* Check if the current user is authenticated
*
* @access public
* @param string $controller Controller
* @param string $action Action name
* @return bool
*/
public function isAuthenticated($controller, $action)
{
// If the action is public we don't need to do any checks
if ($this->acl->isPublicAction($controller, $action)) {
return true;
}
// If the user is already logged it's ok
if ($this->acl->isLogged()) {
// We update each time the RememberMe cookie tokens
if ($this->backend('rememberMe')->hasCookie()) {
$this->backend('rememberMe')->refresh();
}
return true;
}
// We try first with the RememberMe cookie
if ($this->backend('rememberMe')->authenticate()) {
return true;
}
// Then with the ReverseProxy authentication
if (REVERSE_PROXY_AUTH && $this->backend('reverseProxy')->authenticate()) {
return true;
}
return false;
}
/**
* Authenticate a user by different methods
*
* @access public
* @param string $username Username
* @param string $password Password
* @return boolean
*/
public function authenticate($username, $password)
{
// Try first the database auth and then LDAP if activated
if ($this->backend('database')->authenticate($username, $password)) {
return true;
}
else if (LDAP_AUTH && $this->backend('ldap')->authenticate($username, $password)) {
return true;
}
return false;
}
/**
* Validate user login form
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateForm(array $values)
{
$v = new Validator($values, array(
new Validators\Required('username', t('The username is required')),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
new Validators\Required('password', t('The password is required')),
));
$result = $v->execute();
$errors = $v->getErrors();
if ($result) {
if ($this->authenticate($values['username'], $values['password'])) {
// Setup the remember me feature
if (! empty($values['remember_me'])) {
$credentials = $this->backend('rememberMe')
->create($this->acl->getUserId(), $this->user->getIpAddress(), $this->user->getUserAgent());
$this->backend('rememberMe')->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']);
}
}
else {
$result = false;
$errors['login'] = t('Bad username or password');
}
}
return array(
$result,
$errors
);
}
}

View file

@ -2,21 +2,9 @@
namespace Model; namespace Model;
require __DIR__.'/../../vendor/SimpleValidator/Validator.php';
require __DIR__.'/../../vendor/SimpleValidator/Base.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Required.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Unique.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/MaxLength.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/MinLength.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Integer.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Equals.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/AlphaNumeric.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/GreaterThan.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Date.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Email.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Numeric.php';
use Core\Event; use Core\Event;
use Core\Tool;
use Core\Registry;
use PicoDb\Database; use PicoDb\Database;
/** /**
@ -24,6 +12,23 @@ use PicoDb\Database;
* *
* @package model * @package model
* @author Frederic Guillot * @author Frederic Guillot
*
* @property \Model\Acl $acl
* @property \Model\Action $action
* @property \Model\Authentication $authentication
* @property \Model\Board $board
* @property \Model\Category $category
* @property \Model\Comment $comment
* @property \Model\Config $config
* @property \Model\File $file
* @property \Model\LastLogin $lastLogin
* @property \Model\Notification $notification
* @property \Model\Project $project
* @property \Model\SubTask $subTask
* @property \Model\Task $task
* @property \Model\TaskHistory $taskHistory
* @property \Model\User $user
* @property \Model\Webhook $webhook
*/ */
abstract class Base abstract class Base
{ {
@ -38,21 +43,41 @@ abstract class Base
/** /**
* Event dispatcher instance * Event dispatcher instance
* *
* @access protected * @access public
* @var \Core\Event * @var \Core\Event
*/ */
protected $event; public $event;
/**
* Registry instance
*
* @access protected
* @var \Core\Registry
*/
protected $registry;
/** /**
* Constructor * Constructor
* *
* @access public * @access public
* @param \PicoDb\Database $db Database instance * @param \Core\Registry $registry Registry instance
* @param \Core\Event $event Event dispatcher instance
*/ */
public function __construct(Database $db, Event $event) public function __construct(Registry $registry)
{ {
$this->db = $db; $this->registry = $registry;
$this->event = $event; $this->db = $this->registry->shared('db');
$this->event = $this->registry->shared('event');
}
/**
* Load automatically models
*
* @access public
* @param string $name Model name
* @return mixed
*/
public function __get($name)
{
return Tool::loadModel($this->registry, $name);
} }
} }

View file

@ -0,0 +1,70 @@
<?php
namespace Model;
use PDO;
use Core\Template;
/**
* Task history model
*
* @package model
* @author Frederic Guillot
*/
abstract class BaseHistory extends Base
{
/**
* SQL table name
*
* @access protected
* @var string
*/
protected $table = '';
/**
* Remove old event entries to avoid a large table
*
* @access public
* @param integer $max Maximum number of items to keep in the table
*/
public function cleanup($max)
{
if ($this->db->table($this->table)->count() > $max) {
$this->db->execute('
DELETE FROM '.$this->table.'
WHERE id <= (
SELECT id FROM (
SELECT id FROM '.$this->table.' ORDER BY id DESC LIMIT 1 OFFSET '.$max.'
) foo
)');
}
}
/**
* Get all events for a given project
*
* @access public
* @return array
*/
public function getAllByProjectId($project_id)
{
return $this->db->table($this->table)
->eq('project_id', $project_id)
->desc('id')
->findAll();
}
/**
* Get the event html content
*
* @access public
* @param array $params Event properties
* @return string
*/
public function getContent(array $params)
{
$tpl = new Template;
return $tpl->load('event_'.str_replace('.', '_', $params['event_name']), $params);
}
}

View file

@ -21,28 +21,14 @@ class Board extends Base
const TABLE = 'columns'; const TABLE = 'columns';
/** /**
* Save task positions for each column * Get Kanboard default columns
* *
* @access public * @access public
* @param array $values [['task_id' => X, 'column_id' => X, 'position' => X], ...] * @return array
* @return boolean
*/ */
public function saveTasksPosition(array $values) public function getDefaultColumns()
{ {
$taskModel = new Task($this->db, $this->event); return array(t('Backlog'), t('Ready'), t('Work in progress'), t('Done'));
$this->db->startTransaction();
foreach ($values as $value) {
if (! $taskModel->move($value['task_id'], $value['column_id'], $value['position'])) {
$this->db->cancelTransaction();
return false;
}
}
$this->db->closeTransaction();
return true;
} }
/** /**
@ -50,19 +36,20 @@ class Board extends Base
* *
* @access public * @access public
* @param integer $project_id Project id * @param integer $project_id Project id
* @param array $columns List of columns title ['column1', 'column2', ...] * @param array $columns Column parameters [ 'title' => 'boo', 'task_limit' => 2 ... ]
* @return boolean * @return boolean
*/ */
public function create($project_id, array $columns) public function create($project_id, array $columns)
{ {
$position = 0; $position = 0;
foreach ($columns as $title) { foreach ($columns as $column) {
$values = array( $values = array(
'title' => $title, 'title' => $column['title'],
'position' => ++$position, 'position' => ++$position,
'project_id' => $project_id, 'project_id' => $project_id,
'task_limit' => $column['task_limit'],
); );
if (! $this->db->table(self::TABLE)->save($values)) { if (! $this->db->table(self::TABLE)->save($values)) {
@ -73,17 +60,42 @@ class Board extends Base
return true; return true;
} }
/**
* Copy board columns from a project to another one
*
* @author Antonio Rabelo
* @param integer $project_from Project Template
* @return integer $project_to Project that receives the copy
* @return boolean
*/
public function duplicate($project_from, $project_to)
{
$columns = $this->db->table(Board::TABLE)
->columns('title', 'task_limit')
->eq('project_id', $project_from)
->asc('position')
->findAll();
return $this->board->create($project_to, $columns);
}
/** /**
* Add a new column to the board * Add a new column to the board
* *
* @access public * @access public
* @param array $values ['title' => X, 'project_id' => X] * @param integer $project_id Project id
* @param string $title Column title
* @param integer $task_limit Task limit
* @return boolean * @return boolean
*/ */
public function add(array $values) public function addColumn($project_id, $title, $task_limit = 0)
{ {
$values['position'] = $this->getLastColumnPosition($values['project_id']) + 1; return $this->db->table(self::TABLE)->save(array(
return $this->db->table(self::TABLE)->save($values); 'project_id' => $project_id,
'title' => $title,
'task_limit' => $task_limit,
'position' => $this->getLastColumnPosition($project_id) + 1,
));
} }
/** /**
@ -95,19 +107,20 @@ class Board extends Base
*/ */
public function update(array $values) public function update(array $values)
{ {
$this->db->startTransaction(); $columns = array();
foreach (array('title', 'task_limit') as $field) { foreach (array('title', 'task_limit') as $field) {
foreach ($values[$field] as $column_id => $field_value) { foreach ($values[$field] as $column_id => $value) {
$columns[$column_id][$field] = $value;
if ($field === 'task_limit' && empty($field_value)) {
$field_value = 0;
}
$this->updateColumn($column_id, array($field => $field_value));
} }
} }
$this->db->startTransaction();
foreach ($columns as $column_id => $values) {
$this->updateColumn($column_id, $values['title'], (int) $values['task_limit']);
}
$this->db->closeTransaction(); $this->db->closeTransaction();
return true; return true;
@ -117,13 +130,17 @@ class Board extends Base
* Update a column * Update a column
* *
* @access public * @access public
* @param integer $column_id Column id * @param integer $column_id Column id
* @param array $values Form values * @param string $title Column title
* @param integer $task_limit Task limit
* @return boolean * @return boolean
*/ */
public function updateColumn($column_id, array $values) public function updateColumn($column_id, $title, $task_limit = 0)
{ {
return $this->db->table(self::TABLE)->eq('id', $column_id)->update($values); return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array(
'title' => $title,
'task_limit' => $task_limit,
));
} }
/** /**
@ -189,7 +206,7 @@ class Board extends Base
* *
* @access public * @access public
* @param integer $project_id Project id * @param integer $project_id Project id
* @param array $filters * @param array $filters
* @return array * @return array
*/ */
public function get($project_id, array $filters = array()) public function get($project_id, array $filters = array())
@ -201,8 +218,7 @@ class Board extends Base
$filters[] = array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id); $filters[] = array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id);
$filters[] = array('column' => 'is_active', 'operator' => 'eq', 'value' => Task::STATUS_OPEN); $filters[] = array('column' => 'is_active', 'operator' => 'eq', 'value' => Task::STATUS_OPEN);
$taskModel = new Task($this->db, $this->event); $tasks = $this->task->find($filters);
$tasks = $taskModel->find($filters);
foreach ($columns as &$column) { foreach ($columns as &$column) {

View file

@ -20,6 +20,19 @@ class Category extends Base
*/ */
const TABLE = 'project_has_categories'; const TABLE = 'project_has_categories';
/**
* Return true if a category exists for a given project
*
* @access public
* @param integer $category_id Category id
* @param integer $project_id Project id
* @return boolean
*/
public function exists($category_id, $project_id)
{
return $this->db->table(self::TABLE)->eq('id', $category_id)->eq('project_id', $project_id)->count() > 0;
}
/** /**
* Get a category by the id * Get a category by the id
* *
@ -110,11 +123,45 @@ class Category extends Base
public function remove($category_id) public function remove($category_id)
{ {
$this->db->startTransaction(); $this->db->startTransaction();
$r1 = $this->db->table(Task::TABLE)->eq('category_id', $category_id)->update(array('category_id' => 0));
$r2 = $this->db->table(self::TABLE)->eq('id', $category_id)->remove(); $this->db->table(Task::TABLE)->eq('category_id', $category_id)->update(array('category_id' => 0));
if (! $this->db->table(self::TABLE)->eq('id', $category_id)->remove()) {
$this->db->cancelTransaction();
return false;
}
$this->db->closeTransaction(); $this->db->closeTransaction();
return $r1 && $r2; return true;
}
/**
* Duplicate categories from a project to another one
*
* @author Antonio Rabelo
* @param integer $project_from Project Template
* @return integer $project_to Project that receives the copy
* @return boolean
*/
public function duplicate($project_from, $project_to)
{
$categories = $this->db->table(self::TABLE)
->columns('name')
->eq('project_id', $project_from)
->asc('name')
->findAll();
foreach ($categories as $category) {
$category['project_id'] = $project_to;
if (! $this->category->create($category)) {
return false;
}
}
return true;
} }
/** /**
@ -126,12 +173,12 @@ class Category extends Base
*/ */
public function validateCreation(array $values) public function validateCreation(array $values)
{ {
$v = new Validator($values, array( $rules = array(
new Validators\Required('project_id', t('The project id is required')), new Validators\Required('project_id', t('The project id is required')),
new Validators\Integer('project_id', t('The project id must be an integer')),
new Validators\Required('name', t('The name is required')), new Validators\Required('name', t('The name is required')),
new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) );
));
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
return array( return array(
$v->execute(), $v->execute(),
@ -148,18 +195,31 @@ class Category extends Base
*/ */
public function validateModification(array $values) public function validateModification(array $values)
{ {
$v = new Validator($values, array( $rules = array(
new Validators\Required('id', t('The id is required')), new Validators\Required('id', t('The id is required')),
new Validators\Integer('id', t('The id must be an integer')),
new Validators\Required('project_id', t('The project id is required')),
new Validators\Integer('project_id', t('The project id must be an integer')),
new Validators\Required('name', t('The name is required')), new Validators\Required('name', t('The name is required')),
new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) );
));
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
return array( return array(
$v->execute(), $v->execute(),
$v->getErrors() $v->getErrors()
); );
} }
/**
* Common validation rules
*
* @access private
* @return array
*/
private function commonValidationRules()
{
return array(
new Validators\Integer('id', t('The id must be an integer')),
new Validators\Integer('project_id', t('The project id must be an integer')),
new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50)
);
}
} }

View file

@ -20,6 +20,14 @@ class Comment extends Base
*/ */
const TABLE = 'comments'; const TABLE = 'comments';
/**
* Events
*
* @var string
*/
const EVENT_UPDATE = 'comment.update';
const EVENT_CREATE = 'comment.create';
/** /**
* Get all comments for a given task * Get all comments for a given task
* *
@ -37,7 +45,8 @@ class Comment extends Base
self::TABLE.'.task_id', self::TABLE.'.task_id',
self::TABLE.'.user_id', self::TABLE.'.user_id',
self::TABLE.'.comment', self::TABLE.'.comment',
User::TABLE.'.username' User::TABLE.'.username',
User::TABLE.'.name'
) )
->join(User::TABLE, 'id', 'user_id') ->join(User::TABLE, 'id', 'user_id')
->orderBy(self::TABLE.'.date', 'ASC') ->orderBy(self::TABLE.'.date', 'ASC')
@ -62,7 +71,8 @@ class Comment extends Base
self::TABLE.'.user_id', self::TABLE.'.user_id',
self::TABLE.'.date', self::TABLE.'.date',
self::TABLE.'.comment', self::TABLE.'.comment',
User::TABLE.'.username' User::TABLE.'.username',
User::TABLE.'.name'
) )
->join(User::TABLE, 'id', 'user_id') ->join(User::TABLE, 'id', 'user_id')
->eq(self::TABLE.'.id', $comment_id) ->eq(self::TABLE.'.id', $comment_id)
@ -95,7 +105,14 @@ class Comment extends Base
{ {
$values['date'] = time(); $values['date'] = time();
return $this->db->table(self::TABLE)->save($values); if ($this->db->table(self::TABLE)->save($values)) {
$values['id'] = $this->db->getConnection()->getLastId();
$this->event->trigger(self::EVENT_CREATE, $values);
return true;
}
return false;
} }
/** /**
@ -107,10 +124,14 @@ class Comment extends Base
*/ */
public function update(array $values) public function update(array $values)
{ {
return $this->db $result = $this->db
->table(self::TABLE) ->table(self::TABLE)
->eq('id', $values['id']) ->eq('id', $values['id'])
->update(array('comment' => $values['comment'])); ->update(array('comment' => $values['comment']));
$this->event->trigger(self::EVENT_UPDATE, $values);
return $result;
} }
/** /**
@ -134,13 +155,12 @@ class Comment extends Base
*/ */
public function validateCreation(array $values) public function validateCreation(array $values)
{ {
$v = new Validator($values, array( $rules = array(
new Validators\Required('task_id', t('This value is required')),
new Validators\Integer('task_id', t('This value must be an integer')),
new Validators\Required('user_id', t('This value is required')), new Validators\Required('user_id', t('This value is required')),
new Validators\Integer('user_id', t('This value must be an integer')), new Validators\Required('task_id', t('This value is required')),
new Validators\Required('comment', t('Comment is required')) );
));
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
return array( return array(
$v->execute(), $v->execute(),
@ -157,15 +177,31 @@ class Comment extends Base
*/ */
public function validateModification(array $values) public function validateModification(array $values)
{ {
$v = new Validator($values, array( $rules = array(
new Validators\Required('id', t('This value is required')), new Validators\Required('id', t('This value is required')),
new Validators\Integer('id', t('This value must be an integer')), );
new Validators\Required('comment', t('Comment is required'))
)); $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
return array( return array(
$v->execute(), $v->execute(),
$v->getErrors() $v->getErrors()
); );
} }
/**
* Common validation rules
*
* @access private
* @return array
*/
private function commonValidationRules()
{
return array(
new Validators\Integer('id', t('This value must be an integer')),
new Validators\Integer('task_id', t('This value must be an integer')),
new Validators\Integer('user_id', t('This value must be an integer')),
new Validators\Required('comment', t('Comment is required'))
);
}
} }

View file

@ -0,0 +1,152 @@
<?php
namespace Model;
use PDO;
use Core\Registry;
use Event\CommentHistoryListener;
/**
* Comment history model
*
* @package model
* @author Frederic Guillot
*/
class CommentHistory extends BaseHistory
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'comment_has_events';
/**
* Maximum number of events
*
* @var integer
*/
const MAX_EVENTS = 5000;
/**
* Constructor
*
* @access public
* @param \Core\Registry $registry Registry instance
*/
public function __construct(Registry $registry)
{
parent::__construct($registry);
$this->table = self::TABLE;
}
/**
* Create a new event
*
* @access public
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param integer $comment_id Comment id
* @param integer $creator_id Author of the event (user id)
* @param string $event_name Task event name
* @param string $data Current comment
* @return boolean
*/
public function create($project_id, $task_id, $comment_id, $creator_id, $event_name, $data)
{
$values = array(
'project_id' => $project_id,
'task_id' => $task_id,
'comment_id' => $comment_id,
'creator_id' => $creator_id,
'event_name' => $event_name,
'date_creation' => time(),
'data' => $data,
);
$this->db->startTransaction();
$this->cleanup(self::MAX_EVENTS - 1);
$result = $this->db->table(self::TABLE)->insert($values);
$this->db->closeTransaction();
return $result;
}
/**
* Get all necessary content to display activity feed
*
* $author_name
* $author_username
* $task['id', 'title', 'position', 'column_name']
*/
public function getAllContentByProjectId($project_id, $limit = 50)
{
$sql = '
SELECT
comment_has_events.id,
comment_has_events.date_creation,
comment_has_events.event_name,
comment_has_events.data as comment,
comment_has_events.task_id,
tasks.title as task_title,
users.username as author_username,
users.name as author_name
FROM comment_has_events
LEFT JOIN users ON users.id=comment_has_events.creator_id
LEFT JOIN tasks ON tasks.id=comment_has_events.task_id
WHERE comment_has_events.project_id = ?
ORDER BY comment_has_events.id DESC
LIMIT '.$limit.' OFFSET 0
';
$rq = $this->db->execute($sql, array($project_id));
$events = $rq->fetchAll(PDO::FETCH_ASSOC);
foreach ($events as &$event) {
$event['author'] = $event['author_name'] ?: $event['author_username'];
$event['event_title'] = $this->getTitle($event);
$event['event_content'] = $this->getContent($event);
$event['event_type'] = 'comment';
}
return $events;
}
/**
* Get the event title (translated)
*
* @access public
* @param array $event Event properties
* @return string
*/
public function getTitle(array $event)
{
$titles = array(
Comment::EVENT_UPDATE => t('%s updated a comment on the task #%d', $event['author'], $event['task_id']),
Comment::EVENT_CREATE => t('%s commented on the task #%d', $event['author'], $event['task_id']),
);
return isset($titles[$event['event_name']]) ? $titles[$event['event_name']] : '';
}
/**
* Attach events to be able to record the history
*
* @access public
*/
public function attachEvents()
{
$events = array(
Comment::EVENT_UPDATE,
Comment::EVENT_CREATE,
);
$listener = new CommentHistoryListener($this);
foreach ($events as $event_name) {
$this->event->attach($event_name, $listener);
}
}
}

View file

@ -42,20 +42,20 @@ class Config extends Base
*/ */
public function getLanguages() public function getLanguages()
{ {
$languages = array( // Sorted by value
'de_DE' => t('German'), return array(
'en_US' => t('English'), 'de_DE' => 'Deutsch',
'es_ES' => t('Spanish'), 'en_US' => 'English',
'fr_FR' => t('French'), 'es_ES' => 'Español',
'pl_PL' => t('Polish'), 'fr_FR' => 'Français',
'pt_BR' => t('Portuguese (Brazilian)'), 'it_IT' => 'Italiano',
'sv_SE' => t('Swedish'), 'pl_PL' => 'Polski',
'zh_CN' => t('Chinese (Simplified)'), 'pt_BR' => 'Português (Brasil)',
'ru_RU' => 'Русский',
'fi_FI' => 'Suomi',
'sv_SE' => 'Svenska',
'zh_CN' => '中文(简体)',
); );
asort($languages);
return $languages;
} }
/** /**
@ -72,7 +72,7 @@ class Config extends Base
$_SESSION['config'] = $this->getAll(); $_SESSION['config'] = $this->getAll();
} }
if (isset($_SESSION['config'][$name])) { if (! empty($_SESSION['config'][$name])) {
return $_SESSION['config'][$name]; return $_SESSION['config'][$name];
} }

View file

@ -24,6 +24,13 @@ class File extends Base
*/ */
const BASE_PATH = 'data/files/'; const BASE_PATH = 'data/files/';
/**
* Events
*
* @var string
*/
const EVENT_CREATE = 'file.create';
/** /**
* Get a file by the id * Get a file by the id
* *
@ -82,6 +89,8 @@ class File extends Base
*/ */
public function create($task_id, $name, $path, $is_image) public function create($task_id, $name, $path, $is_image)
{ {
$this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id, 'name' => $name));
return $this->db->table(self::TABLE)->save(array( return $this->db->table(self::TABLE)->save(array(
'task_id' => $task_id, 'task_id' => $task_id,
'name' => $name, 'name' => $name,

View file

@ -24,17 +24,6 @@ class LastLogin extends Base
*/ */
const NB_LOGINS = 10; const NB_LOGINS = 10;
/**
* Authentication methods
*
* @var string
*/
const AUTH_DATABASE = 'database';
const AUTH_REMEMBER_ME = 'remember_me';
const AUTH_LDAP = 'ldap';
const AUTH_GOOGLE = 'google';
const AUTH_GITHUB = 'github';
/** /**
* Create a new record * Create a new record
* *

View file

@ -1,105 +0,0 @@
<?php
namespace Model;
/**
* LDAP model
*
* @package model
* @author Frederic Guillot
*/
class Ldap extends Base
{
/**
* Authenticate a user
*
* @access public
* @param string $username Username
* @param string $password Password
* @return null|boolean
*/
public function authenticate($username, $password)
{
if (! function_exists('ldap_connect')) {
die('The PHP LDAP extension is required');
}
// Skip SSL certificate verification
if (! LDAP_SSL_VERIFY) {
putenv('LDAPTLS_REQCERT=never');
}
$ldap = ldap_connect(LDAP_SERVER, LDAP_PORT);
if (! is_resource($ldap)) {
die('Unable to connect to the LDAP server: "'.LDAP_SERVER.'"');
}
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
if (! @ldap_bind($ldap, LDAP_USERNAME, LDAP_PASSWORD)) {
die('Unable to bind to the LDAP server: "'.LDAP_SERVER.'"');
}
$sr = @ldap_search($ldap, LDAP_ACCOUNT_BASE, sprintf(LDAP_USER_PATTERN, $username), array(LDAP_ACCOUNT_FULLNAME, LDAP_ACCOUNT_EMAIL));
if ($sr === false) {
return false;
}
$info = ldap_get_entries($ldap, $sr);
// User not found
if (count($info) == 0 || $info['count'] == 0) {
return false;
}
if (@ldap_bind($ldap, $info[0]['dn'], $password)) {
return $this->create($username, $info[0][LDAP_ACCOUNT_FULLNAME][0], $info[0][LDAP_ACCOUNT_EMAIL][0]);
}
return false;
}
/**
* Create automatically a new local user after the LDAP authentication
*
* @access public
* @param string $username Username
* @param string $name Name of the user
* @param string $email Email address
* @return bool
*/
public function create($username, $name, $email)
{
$userModel = new User($this->db, $this->event);
$user = $userModel->getByUsername($username);
// There is an existing user account
if ($user) {
if ($user['is_ldap_user'] == 1) {
// LDAP user already created
return true;
}
else {
// There is already a local user with that username
return false;
}
}
// Create a LDAP user
$values = array(
'username' => $username,
'name' => $name,
'email' => $email,
'is_admin' => 0,
'is_ldap_user' => 1,
);
return $userModel->create($values);
}
}

View file

@ -0,0 +1,239 @@
<?php
namespace Model;
use Core\Translator;
use Core\Template;
use Event\TaskNotificationListener;
use Event\CommentNotificationListener;
use Event\FileNotificationListener;
use Event\SubTaskNotificationListener;
use Swift_Message;
use Swift_Mailer;
/**
* Notification model
*
* @package model
* @author Frederic Guillot
*/
class Notification extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'user_has_notifications';
/**
* Get the list of users to send the notification for a given project
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getUsersList($project_id)
{
$users = $this->db->table(User::TABLE)
->columns('id', 'username', 'name', 'email')
->eq('notifications_enabled', '1')
->neq('email', '')
->findAll();
foreach ($users as $index => $user) {
$projects = $this->db->table(self::TABLE)
->eq('user_id', $user['id'])
->findAllByColumn('project_id');
// The user have selected only some projects
if (! empty($projects)) {
// If the user didn't select this project we remove that guy from the list
if (! in_array($project_id, $projects)) {
unset($users[$index]);
}
}
}
return $users;
}
/**
* Attach events
*
* @access public
*/
public function attachEvents()
{
$this->event->attach(File::EVENT_CREATE, new FileNotificationListener($this, 'notification_file_creation'));
$this->event->attach(Comment::EVENT_CREATE, new CommentNotificationListener($this, 'notification_comment_creation'));
$this->event->attach(Comment::EVENT_UPDATE, new CommentNotificationListener($this, 'notification_comment_update'));
$this->event->attach(SubTask::EVENT_CREATE, new SubTaskNotificationListener($this, 'notification_subtask_creation'));
$this->event->attach(SubTask::EVENT_UPDATE, new SubTaskNotificationListener($this, 'notification_subtask_update'));
$this->event->attach(Task::EVENT_CREATE, new TaskNotificationListener($this, 'notification_task_creation'));
$this->event->attach(Task::EVENT_UPDATE, new TaskNotificationListener($this, 'notification_task_update'));
$this->event->attach(Task::EVENT_CLOSE, new TaskNotificationListener($this, 'notification_task_close'));
$this->event->attach(Task::EVENT_OPEN, new TaskNotificationListener($this, 'notification_task_open'));
$this->event->attach(Task::EVENT_MOVE_COLUMN, new TaskNotificationListener($this, 'notification_task_move_column'));
$this->event->attach(Task::EVENT_MOVE_POSITION, new TaskNotificationListener($this, 'notification_task_move_position'));
$this->event->attach(Task::EVENT_ASSIGNEE_CHANGE, new TaskNotificationListener($this, 'notification_task_assignee_change'));
}
/**
* Send the email notifications
*
* @access public
* @param string $template Template name
* @param array $users List of users
* @param array $data Template data
*/
public function sendEmails($template, array $users, array $data)
{
$transport = $this->registry->shared('mailer');
$mailer = Swift_Mailer::newInstance($transport);
$message = Swift_Message::newInstance()
->setSubject($this->getMailSubject($template, $data))
->setFrom(array(MAIL_FROM => 'Kanboard'))
->setBody($this->getMailContent($template, $data), 'text/html');
foreach ($users as $user) {
$message->setTo(array($user['email'] => $user['name'] ?: $user['username']));
$mailer->send($message);
}
}
/**
* Get the mail subject for a given template name
*
* @access public
* @param string $template Template name
* @param array $data Template data
*/
public function getMailSubject($template, array $data)
{
switch ($template) {
case 'notification_file_creation':
$subject = e('[%s][New attachment] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_comment_creation':
$subject = e('[%s][New comment] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_comment_update':
$subject = e('[%s][Comment updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_subtask_creation':
$subject = e('[%s][New subtask] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_subtask_update':
$subject = e('[%s][Subtask updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_task_creation':
$subject = e('[%s][New task] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_task_update':
$subject = e('[%s][Task updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_task_close':
$subject = e('[%s][Task closed] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_task_open':
$subject = e('[%s][Task opened] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_task_move_column':
$subject = e('[%s][Column Change] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_task_move_position':
$subject = e('[%s][Position Change] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_task_assignee_change':
$subject = e('[%s][Assignee Change] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_task_due':
$subject = e('[%s][Due tasks]', $data['project']);
break;
default:
$subject = e('[Kanboard] Notification');
}
return $subject;
}
/**
* Get the mail content for a given template name
*
* @access public
* @param string $template Template name
* @param array $data Template data
*/
public function getMailContent($template, array $data)
{
$tpl = new Template;
return $tpl->load($template, $data);
}
/**
* Save settings for the given user
*
* @access public
* @param integer $user_id User id
* @param array $values Form values
*/
public function saveSettings($user_id, array $values)
{
// Delete all selected projects
$this->db->table(self::TABLE)->eq('user_id', $user_id)->remove();
if (isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1) {
// Activate notifications
$this->db->table(User::TABLE)->eq('id', $user_id)->update(array(
'notifications_enabled' => '1'
));
// Save selected projects
if (! empty($values['projects'])) {
foreach ($values['projects'] as $project_id => $checkbox_value) {
$this->db->table(self::TABLE)->insert(array(
'user_id' => $user_id,
'project_id' => $project_id,
));
}
}
}
else {
// Disable notifications
$this->db->table(User::TABLE)->eq('id', $user_id)->update(array(
'notifications_enabled' => '0'
));
}
}
/**
* Read user settings to display the form
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function readSettings($user_id)
{
$values = array();
$values['notifications_enabled'] = $this->db->table(User::TABLE)->eq('id', $user_id)->findOneColumn('notifications_enabled');
$projects = $this->db->table(self::TABLE)->eq('user_id', $user_id)->findAllByColumn('project_id');
foreach ($projects as $project_id) {
$values['project_'.$project_id] = true;
}
return $values;
}
}

View file

@ -4,7 +4,7 @@ namespace Model;
use SimpleValidator\Validator; use SimpleValidator\Validator;
use SimpleValidator\Validators; use SimpleValidator\Validators;
use Event\TaskModification; use Event\ProjectModificationDate;
use Core\Security; use Core\Security;
/** /**
@ -55,10 +55,9 @@ class Project extends Base
public function getUsersList($project_id, $prepend_unassigned = true, $prepend_everybody = false) public function getUsersList($project_id, $prepend_unassigned = true, $prepend_everybody = false)
{ {
$allowed_users = $this->getAllowedUsers($project_id); $allowed_users = $this->getAllowedUsers($project_id);
$userModel = new User($this->db, $this->event);
if (empty($allowed_users)) { if (empty($allowed_users)) {
$allowed_users = $userModel->getList(); $allowed_users = $this->user->getList();
} }
if ($prepend_unassigned) { if ($prepend_unassigned) {
@ -81,12 +80,23 @@ class Project extends Base
*/ */
public function getAllowedUsers($project_id) public function getAllowedUsers($project_id)
{ {
return $this->db $users = $this->db
->table(self::TABLE_USERS) ->table(self::TABLE_USERS)
->join(User::TABLE, 'id', 'user_id') ->join(User::TABLE, 'id', 'user_id')
->eq('project_id', $project_id) ->eq('project_id', $project_id)
->asc('username') ->asc('username')
->listing('user_id', 'username'); ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')
->findAll();
$result = array();
foreach ($users as $user) {
$result[$user['id']] = $user['name'] ?: $user['username'];
}
asort($result);
return $result;
} }
/** /**
@ -103,8 +113,7 @@ class Project extends Base
'not_allowed' => array(), 'not_allowed' => array(),
); );
$userModel = new User($this->db, $this->event); $all_users = $this->user->getList();
$all_users = $userModel->getList();
$users['allowed'] = $this->getAllowedUsers($project_id); $users['allowed'] = $this->getAllowedUsers($project_id);
@ -218,7 +227,7 @@ class Project extends Base
*/ */
public function getByToken($token) public function getByToken($token)
{ {
return $this->db->table(self::TABLE)->eq('token', $token)->findOne(); return $this->db->table(self::TABLE)->eq('token', $token)->eq('is_public', 1)->findOne();
} }
/** /**
@ -236,50 +245,23 @@ class Project extends Base
* Get all projects, optionaly fetch stats for each project and can check users permissions * Get all projects, optionaly fetch stats for each project and can check users permissions
* *
* @access public * @access public
* @param bool $fetch_stats If true, return metrics about each projects * @param bool $filter_permissions If true, remove projects not allowed for the current user
* @param bool $check_permissions If true, remove projects not allowed for the current user
* @return array * @return array
*/ */
public function getAll($fetch_stats = false, $check_permissions = false) public function getAll($filter_permissions = false)
{ {
if (! $fetch_stats) { $projects = $this->db->table(self::TABLE)->asc('name')->findAll();
return $this->db->table(self::TABLE)->asc('name')->findAll();
}
$this->db->startTransaction(); if ($filter_permissions) {
$projects = $this->db foreach ($projects as $key => $project) {
->table(self::TABLE)
->asc('name')
->findAll();
$boardModel = new Board($this->db, $this->event); if (! $this->isUserAllowed($project['id'], $this->acl->getUserId())) {
$taskModel = new Task($this->db, $this->event); unset($projects[$key]);
$aclModel = new Acl($this->db, $this->event);
foreach ($projects as $pkey => &$project) {
if ($check_permissions && ! $this->isUserAllowed($project['id'], $aclModel->getUserId())) {
unset($projects[$pkey]);
}
else {
$columns = $boardModel->getcolumns($project['id']);
$project['nb_active_tasks'] = 0;
foreach ($columns as &$column) {
$column['nb_active_tasks'] = $taskModel->countByColumnId($project['id'], $column['id']);
$project['nb_active_tasks'] += $column['nb_active_tasks'];
} }
$project['columns'] = $columns;
$project['nb_tasks'] = $taskModel->countByProjectId($project['id']);
$project['nb_inactive_tasks'] = $project['nb_tasks'] - $project['nb_active_tasks'];
} }
} }
$this->db->closeTransaction();
return $projects; return $projects;
} }
@ -377,6 +359,124 @@ class Project extends Base
return $this->filterListByAccess($this->getListByStatus(self::ACTIVE), $user_id); return $this->filterListByAccess($this->getListByStatus(self::ACTIVE), $user_id);
} }
/**
* Gather some task metrics for a given project
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getStats($project_id)
{
$stats = array();
$columns = $this->board->getcolumns($project_id);
$stats['nb_active_tasks'] = 0;
foreach ($columns as &$column) {
$column['nb_active_tasks'] = $this->task->countByColumnId($project_id, $column['id']);
$stats['nb_active_tasks'] += $column['nb_active_tasks'];
}
$stats['columns'] = $columns;
$stats['nb_tasks'] = $this->task->countByProjectId($project_id);
$stats['nb_inactive_tasks'] = $stats['nb_tasks'] - $stats['nb_active_tasks'];
return $stats;
}
/**
* Create a project from another one.
*
* @author Antonio Rabelo
* @param integer $project_id Project Id
* @return integer Cloned Project Id
*/
public function createProjectFromAnotherProject($project_id)
{
$project_name = $this->db->table(self::TABLE)->eq('id', $project_id)->findOneColumn('name');
$project = array(
'name' => $project_name.' ('.t('Clone').')',
'is_active' => true,
'last_modified' => 0,
'token' => '',
);
if (! $this->db->table(self::TABLE)->save($project)) {
return false;
}
return $this->db->getConnection()->getLastId();
}
/**
* Copy user access from a project to another one
*
* @author Antonio Rabelo
* @param integer $project_from Project Template
* @return integer $project_to Project that receives the copy
* @return boolean
*/
public function duplicateUsers($project_from, $project_to)
{
$users = $this->getAllowedUsers($project_from);
foreach ($users as $user_id => $name) {
if (! $this->allowUser($project_to, $user_id)) {
return false;
}
}
return true;
}
/**
* Clone a project
*
* @author Antonio Rabelo
* @param integer $project_id Project Id
* @return integer Cloned Project Id
*/
public function duplicate($project_id)
{
$this->db->startTransaction();
// Get the cloned project Id
$clone_project_id = $this->createProjectFromAnotherProject($project_id);
if (! $clone_project_id) {
$this->db->cancelTransaction();
return false;
}
// Clone Board
if (! $this->board->duplicate($project_id, $clone_project_id)) {
$this->db->cancelTransaction();
return false;
}
// Clone Categories
if (! $this->category->duplicate($project_id, $clone_project_id)) {
$this->db->cancelTransaction();
return false;
}
// Clone Allowed Users
if (! $this->duplicateUsers($project_id, $clone_project_id)) {
$this->db->cancelTransaction();
return false;
}
// Clone Actions
if (! $this->action->duplicate($project_id, $clone_project_id)) {
$this->db->cancelTransaction();
return false;
}
$this->db->closeTransaction();
return (int) $clone_project_id;
}
/** /**
* Create a project * Create a project
* *
@ -388,7 +488,8 @@ class Project extends Base
{ {
$this->db->startTransaction(); $this->db->startTransaction();
$values['token'] = Security::generateToken(); $values['token'] = '';
$values['last_modified'] = time();
if (! $this->db->table(self::TABLE)->save($values)) { if (! $this->db->table(self::TABLE)->save($values)) {
$this->db->cancelTransaction(); $this->db->cancelTransaction();
@ -396,15 +497,19 @@ class Project extends Base
} }
$project_id = $this->db->getConnection()->getLastId(); $project_id = $this->db->getConnection()->getLastId();
$column_names = explode(',', $this->config->get('default_columns', implode(',', $this->board->getDefaultColumns())));
$columns = array();
$boardModel = new Board($this->db, $this->event); foreach ($column_names as $column_name) {
$boardModel->create($project_id, array(
t('Backlog'),
t('Ready'),
t('Work in progress'),
t('Done'),
));
$column_name = trim($column_name);
if (! empty($column_name)) {
$columns[] = array('title' => $column_name, 'task_limit' => 0);
}
}
$this->board->create($project_id, $columns);
$this->db->closeTransaction(); $this->db->closeTransaction();
return (int) $project_id; return (int) $project_id;
@ -435,7 +540,7 @@ class Project extends Base
*/ */
public function updateModificationDate($project_id) public function updateModificationDate($project_id)
{ {
return $this->db->table(self::TABLE)->eq('id', $project_id)->save(array( return $this->db->table(self::TABLE)->eq('id', $project_id)->update(array(
'last_modified' => time() 'last_modified' => time()
)); ));
} }
@ -449,7 +554,8 @@ class Project extends Base
*/ */
public function update(array $values) public function update(array $values)
{ {
return $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); return $this->exists($values['id']) &&
$this->db->table(self::TABLE)->eq('id', $values['id'])->save($values);
} }
/** /**
@ -464,6 +570,18 @@ class Project extends Base
return $this->db->table(self::TABLE)->eq('id', $project_id)->remove(); return $this->db->table(self::TABLE)->eq('id', $project_id)->remove();
} }
/**
* Return true if the project exists
*
* @access public
* @param integer $project_id Project id
* @return boolean
*/
public function exists($project_id)
{
return $this->db->table(self::TABLE)->eq('id', $project_id)->count() === 1;
}
/** /**
* Enable a project * Enable a project
* *
@ -473,10 +591,11 @@ class Project extends Base
*/ */
public function enable($project_id) public function enable($project_id)
{ {
return $this->db return $this->exists($project_id) &&
$this->db
->table(self::TABLE) ->table(self::TABLE)
->eq('id', $project_id) ->eq('id', $project_id)
->save(array('is_active' => 1)); ->update(array('is_active' => 1));
} }
/** /**
@ -488,10 +607,60 @@ class Project extends Base
*/ */
public function disable($project_id) public function disable($project_id)
{ {
return $this->db return $this->exists($project_id) &&
$this->db
->table(self::TABLE) ->table(self::TABLE)
->eq('id', $project_id) ->eq('id', $project_id)
->save(array('is_active' => 0)); ->update(array('is_active' => 0));
}
/**
* Enable public access for a project
*
* @access public
* @param integer $project_id Project id
* @return bool
*/
public function enablePublicAccess($project_id)
{
return $this->exists($project_id) &&
$this->db
->table(self::TABLE)
->eq('id', $project_id)
->save(array('is_public' => 1, 'token' => Security::generateToken()));
}
/**
* Disable public access for a project
*
* @access public
* @param integer $project_id Project id
* @return bool
*/
public function disablePublicAccess($project_id)
{
return $this->exists($project_id) &&
$this->db
->table(self::TABLE)
->eq('id', $project_id)
->save(array('is_public' => 0, 'token' => ''));
}
/**
* Common validation rules
*
* @access private
* @return array
*/
private function commonValidationRules()
{
return array(
new Validators\Integer('id', t('This value must be an integer')),
new Validators\Integer('is_active', t('This value must be an integer')),
new Validators\Required('name', t('The project name is required')),
new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50),
new Validators\Unique('name', t('This project must be unique'), $this->db->getConnection(), self::TABLE),
);
} }
/** /**
@ -503,11 +672,7 @@ class Project extends Base
*/ */
public function validateCreation(array $values) public function validateCreation(array $values)
{ {
$v = new Validator($values, array( $v = new Validator($values, $this->commonValidationRules());
new Validators\Required('name', t('The project name is required')),
new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50),
new Validators\Unique('name', t('This project must be unique'), $this->db->getConnection(), self::TABLE)
));
return array( return array(
$v->execute(), $v->execute(),
@ -524,14 +689,11 @@ class Project extends Base
*/ */
public function validateModification(array $values) public function validateModification(array $values)
{ {
$v = new Validator($values, array( $rules = array(
new Validators\Required('id', t('The project id is required')), new Validators\Required('id', t('This value is required')),
new Validators\Integer('id', t('This value must be an integer')), );
new Validators\Required('name', t('The project name is required')),
new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50), $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
new Validators\Unique('name', t('This project must be unique'), $this->db->getConnection(), self::TABLE),
new Validators\Integer('is_active', t('This value must be an integer'))
));
return array( return array(
$v->execute(), $v->execute(),
@ -569,16 +731,49 @@ class Project extends Base
public function attachEvents() public function attachEvents()
{ {
$events = array( $events = array(
Task::EVENT_UPDATE, Task::EVENT_CREATE_UPDATE,
Task::EVENT_CREATE,
Task::EVENT_CLOSE, Task::EVENT_CLOSE,
Task::EVENT_OPEN, Task::EVENT_OPEN,
Task::EVENT_MOVE_COLUMN,
Task::EVENT_MOVE_POSITION,
Task::EVENT_ASSIGNEE_CHANGE,
); );
$listener = new TaskModification($this); $listener = new ProjectModificationDate($this);
foreach ($events as $event_name) { foreach ($events as $event_name) {
$this->event->attach($event_name, $listener); $this->event->attach($event_name, $listener);
} }
} }
/**
* Get project activity
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getActivity($project_id)
{
$activity = array();
$tasks = $this->taskHistory->getAllContentByProjectId($project_id, 25);
$comments = $this->commentHistory->getAllContentByProjectId($project_id, 25);
$subtasks = $this->subtaskHistory->getAllContentByProjectId($project_id, 25);
foreach ($tasks as &$task) {
$activity[$task['date_creation'].'-'.$task['id']] = $task;
}
foreach ($subtasks as &$subtask) {
$activity[$subtask['date_creation'].'-'.$subtask['id']] = $subtask;
}
foreach ($comments as &$comment) {
$activity[$comment['date_creation'].'-'.$comment['id']] = $comment;
}
krsort($activity);
return $activity;
}
} }

View file

@ -41,6 +41,14 @@ class SubTask extends Base
*/ */
const STATUS_TODO = 0; const STATUS_TODO = 0;
/**
* Events
*
* @var string
*/
const EVENT_UPDATE = 'subtask.update';
const EVENT_CREATE = 'subtask.create';
/** /**
* Get available status * Get available status
* *
@ -72,7 +80,7 @@ class SubTask extends Base
$status = $this->getStatusList(); $status = $this->getStatusList();
$subtasks = $this->db->table(self::TABLE) $subtasks = $this->db->table(self::TABLE)
->eq('task_id', $task_id) ->eq('task_id', $task_id)
->columns(self::TABLE.'.*', User::TABLE.'.username') ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name')
->join(User::TABLE, 'id', 'user_id') ->join(User::TABLE, 'id', 'user_id')
->findAll(); ->findAll();
@ -88,21 +96,37 @@ class SubTask extends Base
* *
* @access public * @access public
* @param integer $subtask_id Subtask id * @param integer $subtask_id Subtask id
* @param bool $more Fetch more data
* @return array * @return array
*/ */
public function getById($subtask_id) public function getById($subtask_id, $more = false)
{ {
if ($more) {
$subtask = $this->db->table(self::TABLE)
->eq(self::TABLE.'.id', $subtask_id)
->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name')
->join(User::TABLE, 'id', 'user_id')
->findOne();
if ($subtask) {
$status = $this->getStatusList();
$subtask['status_name'] = $status[$subtask['status']];
}
return $subtask;
}
return $this->db->table(self::TABLE)->eq('id', $subtask_id)->findOne(); return $this->db->table(self::TABLE)->eq('id', $subtask_id)->findOne();
} }
/** /**
* Create * Prepare data before insert/update
* *
* @access public * @access public
* @param array $values Form values * @param array $values Form values
* @return bool
*/ */
public function create(array $values) public function prepare(array &$values)
{ {
if (isset($values['another_subtask'])) { if (isset($values['another_subtask'])) {
unset($values['another_subtask']); unset($values['another_subtask']);
@ -115,8 +139,26 @@ class SubTask extends Base
if (isset($values['time_spent']) && empty($values['time_spent'])) { if (isset($values['time_spent']) && empty($values['time_spent'])) {
$values['time_spent'] = 0; $values['time_spent'] = 0;
} }
}
return $this->db->table(self::TABLE)->save($values); /**
* Create
*
* @access public
* @param array $values Form values
* @return bool
*/
public function create(array $values)
{
$this->prepare($values);
$result = $this->db->table(self::TABLE)->save($values);
if ($result) {
$values['id'] = $this->db->getConnection()->getLastId();
$this->event->trigger(self::EVENT_CREATE, $values);
}
return $result;
} }
/** /**
@ -128,15 +170,14 @@ class SubTask extends Base
*/ */
public function update(array $values) public function update(array $values)
{ {
if (isset($values['time_estimated']) && empty($values['time_estimated'])) { $this->prepare($values);
$values['time_estimated'] = 0; $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values);
if ($result) {
$this->event->trigger(self::EVENT_UPDATE, $values);
} }
if (isset($values['time_spent']) && empty($values['time_spent'])) { return $result;
$values['time_spent'] = 0;
}
return $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values);
} }
/** /**
@ -152,28 +193,93 @@ class SubTask extends Base
} }
/** /**
* Validate creation/modification * Duplicate all subtasks to another task
*
* @access public
* @param integer $src_task_id Source task id
* @param integer $dst_task_id Destination task id
* @return bool
*/
public function duplicate($src_task_id, $dst_task_id)
{
$subtasks = $this->db->table(self::TABLE)
->columns('title', 'time_estimated')
->eq('task_id', $src_task_id)
->findAll();
foreach ($subtasks as &$subtask) {
$subtask['task_id'] = $dst_task_id;
$subtask['time_spent'] = 0;
if (! $this->db->table(self::TABLE)->save($subtask)) {
return false;
}
}
return true;
}
/**
* Validate creation
* *
* @access public * @access public
* @param array $values Form values * @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors * @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/ */
public function validate(array $values) public function validateCreation(array $values)
{ {
$v = new Validator($values, array( $rules = array(
new Validators\Required('task_id', t('The task id is required')), new Validators\Required('task_id', t('The task id is required')),
new Validators\Integer('task_id', t('The task id must be an integer')),
new Validators\Required('title', t('The title is required')), new Validators\Required('title', t('The title is required')),
new Validators\MaxLength('title', t('The maximum length is %d characters', 100), 100), );
new Validators\Integer('user_id', t('The user id must be an integer')),
new Validators\Integer('status', t('The status must be an integer')), $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
new Validators\Numeric('time_estimated', t('The time must be a numeric value')),
new Validators\Numeric('time_spent', t('The time must be a numeric value')),
));
return array( return array(
$v->execute(), $v->execute(),
$v->getErrors() $v->getErrors()
); );
} }
/**
* Validate modification
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateModification(array $values)
{
$rules = array(
new Validators\Required('id', t('The subtask id is required')),
new Validators\Required('task_id', t('The task id is required')),
);
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Common validation rules
*
* @access private
* @return array
*/
private function commonValidationRules()
{
return array(
new Validators\Integer('id', t('The subtask id must be an integer')),
new Validators\Integer('task_id', t('The task id must be an integer')),
new Validators\MaxLength('title', t('The maximum length is %d characters', 100), 100),
new Validators\Integer('user_id', t('The user id must be an integer')),
new Validators\Integer('status', t('The status must be an integer')),
new Validators\Numeric('time_estimated', t('The time must be a numeric value')),
new Validators\Numeric('time_spent', t('The time must be a numeric value')),
);
}
} }

View file

@ -0,0 +1,161 @@
<?php
namespace Model;
use PDO;
use Core\Registry;
use Event\SubtaskHistoryListener;
/**
* Comment history model
*
* @package model
* @author Frederic Guillot
*/
class SubtaskHistory extends BaseHistory
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'subtask_has_events';
/**
* Maximum number of events
*
* @var integer
*/
const MAX_EVENTS = 5000;
/**
* Constructor
*
* @access public
* @param \Core\Registry $registry Registry instance
*/
public function __construct(Registry $registry)
{
parent::__construct($registry);
$this->table = self::TABLE;
}
/**
* Create a new event
*
* @access public
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param integer $subtask_id Subtask id
* @param integer $creator_id Author of the event (user id)
* @param string $event_name Task event name
* @param string $data Current comment
* @return boolean
*/
public function create($project_id, $task_id, $subtask_id, $creator_id, $event_name, $data)
{
$values = array(
'project_id' => $project_id,
'task_id' => $task_id,
'subtask_id' => $subtask_id,
'creator_id' => $creator_id,
'event_name' => $event_name,
'date_creation' => time(),
'data' => $data,
);
$this->db->startTransaction();
$this->cleanup(self::MAX_EVENTS - 1);
$result = $this->db->table(self::TABLE)->insert($values);
$this->db->closeTransaction();
return $result;
}
/**
* Get all necessary content to display activity feed
*
* $author_name
* $author_username
* $task['id', 'title', 'position', 'column_name']
*/
public function getAllContentByProjectId($project_id, $limit = 50)
{
$sql = '
SELECT
subtask_has_events.id,
subtask_has_events.date_creation,
subtask_has_events.event_name,
subtask_has_events.task_id,
tasks.title as task_title,
users.username as author_username,
users.name as author_name,
assignees.name as subtask_assignee_name,
assignees.username as subtask_assignee_username,
task_has_subtasks.title as subtask_title,
task_has_subtasks.status as subtask_status,
task_has_subtasks.time_spent as subtask_time_spent,
task_has_subtasks.time_estimated as subtask_time_estimated
FROM subtask_has_events
LEFT JOIN users ON users.id=subtask_has_events.creator_id
LEFT JOIN tasks ON tasks.id=subtask_has_events.task_id
LEFT JOIN task_has_subtasks ON task_has_subtasks.id=subtask_has_events.subtask_id
LEFT JOIN users AS assignees ON assignees.id=task_has_subtasks.user_id
WHERE subtask_has_events.project_id = ?
ORDER BY subtask_has_events.id DESC
LIMIT '.$limit.' OFFSET 0
';
$rq = $this->db->execute($sql, array($project_id));
$events = $rq->fetchAll(PDO::FETCH_ASSOC);
foreach ($events as &$event) {
$event['author'] = $event['author_name'] ?: $event['author_username'];
$event['subtask_assignee'] = $event['subtask_assignee_name'] ?: $event['subtask_assignee_username'];
$event['subtask_status_list'] = $this->subTask->getStatusList();
$event['event_title'] = $this->getTitle($event);
$event['event_content'] = $this->getContent($event);
$event['event_type'] = 'subtask';
}
return $events;
}
/**
* Get the event title (translated)
*
* @access public
* @param array $event Event properties
* @return string
*/
public function getTitle(array $event)
{
$titles = array(
SubTask::EVENT_UPDATE => t('%s updated a subtask for the task #%d', $event['author'], $event['task_id']),
SubTask::EVENT_CREATE => t('%s created a subtask for the task #%d', $event['author'], $event['task_id']),
);
return isset($titles[$event['event_name']]) ? $titles[$event['event_name']] : '';
}
/**
* Attach events to be able to record the history
*
* @access public
*/
public function attachEvents()
{
$events = array(
SubTask::EVENT_UPDATE,
SubTask::EVENT_CREATE,
);
$listener = new SubtaskHistoryListener($this);
foreach ($events as $event_name) {
$this->event->attach($event_name, $listener);
}
}
}

View file

@ -5,6 +5,7 @@ namespace Model;
use SimpleValidator\Validator; use SimpleValidator\Validator;
use SimpleValidator\Validators; use SimpleValidator\Validators;
use DateTime; use DateTime;
use PDO;
/** /**
* Task model * Task model
@ -34,13 +35,14 @@ class Task extends Base
* *
* @var string * @var string
*/ */
const EVENT_MOVE_COLUMN = 'task.move.column'; const EVENT_MOVE_COLUMN = 'task.move.column';
const EVENT_MOVE_POSITION = 'task.move.position'; const EVENT_MOVE_POSITION = 'task.move.position';
const EVENT_UPDATE = 'task.update'; const EVENT_UPDATE = 'task.update';
const EVENT_CREATE = 'task.create'; const EVENT_CREATE = 'task.create';
const EVENT_CLOSE = 'task.close'; const EVENT_CLOSE = 'task.close';
const EVENT_OPEN = 'task.open'; const EVENT_OPEN = 'task.open';
const EVENT_CREATE_UPDATE = 'task.create_update'; const EVENT_CREATE_UPDATE = 'task.create_update';
const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change';
/** /**
* Get available colors * Get available colors
@ -61,6 +63,35 @@ class Task extends Base
); );
} }
/**
* Get a list of due tasks for all projects
*
* @access public
* @return array
*/
public function getOverdueTasks()
{
$tasks = $this->db->table(self::TABLE)
->columns(
self::TABLE.'.id',
self::TABLE.'.title',
self::TABLE.'.date_due',
self::TABLE.'.project_id',
Project::TABLE.'.name AS project_name',
User::TABLE.'.username AS assignee_username',
User::TABLE.'.name AS assignee_name'
)
->join(Project::TABLE, 'id', 'project_id')
->join(User::TABLE, 'id', 'owner_id')
->eq(Project::TABLE.'.is_active', 1)
->eq(self::TABLE.'.is_active', 1)
->neq(self::TABLE.'.date_due', 0)
->lte(self::TABLE.'.date_due', mktime(23, 59, 59))
->findAll();
return $tasks;
}
/** /**
* Fetch one task * Fetch one task
* *
@ -95,7 +126,9 @@ class Task extends Base
projects.name AS project_name, projects.name AS project_name,
columns.title AS column_title, columns.title AS column_title,
users.username AS assignee_username, users.username AS assignee_username,
creators.username AS creator_username users.name AS assignee_name,
creators.username AS creator_username,
creators.name AS creator_name
FROM tasks FROM tasks
LEFT JOIN users ON users.id = tasks.owner_id LEFT JOIN users ON users.id = tasks.owner_id
LEFT JOIN users AS creators ON creators.id = tasks.creator_id LEFT JOIN users AS creators ON creators.id = tasks.creator_id
@ -106,7 +139,7 @@ class Task extends Base
'; ';
$rq = $this->db->execute($sql, array($task_id)); $rq = $this->db->execute($sql, array($task_id));
return $rq->fetch(\PDO::FETCH_ASSOC); return $rq->fetch(PDO::FETCH_ASSOC);
} }
else { else {
@ -118,16 +151,16 @@ class Task extends Base
* Count all tasks for a given project and status * Count all tasks for a given project and status
* *
* @access public * @access public
* @param integer $project_id Project id * @param integer $project_id Project id
* @param array $status List of status id * @param integer $status_id Status id
* @return array * @return array
*/ */
public function getAll($project_id, array $status = array(self::STATUS_OPEN, self::STATUS_CLOSED)) public function getAll($project_id, $status_id = self::STATUS_OPEN)
{ {
return $this->db return $this->db
->table(self::TABLE) ->table(self::TABLE)
->eq('project_id', $project_id) ->eq('project_id', $project_id)
->in('is_active', $status) ->eq('is_active', $status_id)
->findAll(); ->findAll();
} }
@ -163,23 +196,28 @@ class Task extends Base
->columns( ->columns(
'(SELECT count(*) FROM comments WHERE task_id=tasks.id) AS nb_comments', '(SELECT count(*) FROM comments WHERE task_id=tasks.id) AS nb_comments',
'(SELECT count(*) FROM task_has_files WHERE task_id=tasks.id) AS nb_files', '(SELECT count(*) FROM task_has_files WHERE task_id=tasks.id) AS nb_files',
'(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id) AS nb_subtasks',
'(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id AND status=2) AS nb_completed_subtasks',
'tasks.id', 'tasks.id',
'tasks.title', 'tasks.title',
'tasks.description', 'tasks.description',
'tasks.date_creation', 'tasks.date_creation',
'tasks.date_modification',
'tasks.date_completed', 'tasks.date_completed',
'tasks.date_due', 'tasks.date_due',
'tasks.color_id', 'tasks.color_id',
'tasks.project_id', 'tasks.project_id',
'tasks.column_id', 'tasks.column_id',
'tasks.owner_id', 'tasks.owner_id',
'tasks.creator_id',
'tasks.position', 'tasks.position',
'tasks.is_active', 'tasks.is_active',
'tasks.score', 'tasks.score',
'tasks.category_id', 'tasks.category_id',
'users.username' 'users.username AS assignee_username',
'users.name AS assignee_name'
) )
->join('users', 'id', 'owner_id'); ->join(User::TABLE, 'id', 'owner_id');
foreach ($filters as $key => $filter) { foreach ($filters as $key => $filter) {
@ -228,90 +266,127 @@ class Task extends Base
} }
/** /**
* Duplicate a task * Generic method to duplicate a task
* *
* @access public * @access public
* @param integer $task_id Task id * @param array $task Task data
* @return boolean * @param array $override Task properties to override
* @return integer|boolean
*/ */
public function duplicate($task_id) public function copy(array $task, array $override = array())
{ {
// Values to override
if (! empty($override)) {
$task = $override + $task;
}
$this->db->startTransaction(); $this->db->startTransaction();
// Get the original task
$task = $this->getById($task_id);
// Cleanup data
unset($task['id']);
unset($task['date_completed']);
// Assign new values // Assign new values
$task['date_creation'] = time(); $values = array();
$task['is_active'] = 1; $values['title'] = $task['title'];
$task['position'] = $this->countByColumnId($task['project_id'], $task['column_id']); $values['description'] = $task['description'];
$values['date_creation'] = time();
$values['date_modification'] = $values['date_creation'];
$values['date_due'] = $task['date_due'];
$values['color_id'] = $task['color_id'];
$values['project_id'] = $task['project_id'];
$values['column_id'] = $task['column_id'];
$values['owner_id'] = 0;
$values['creator_id'] = $task['creator_id'];
$values['position'] = $this->countByColumnId($values['project_id'], $values['column_id']) + 1;
$values['score'] = $task['score'];
$values['category_id'] = 0;
// Check if the assigned user is allowed for the new project
if ($task['owner_id'] && $this->project->isUserAllowed($values['project_id'], $task['owner_id'])) {
$values['owner_id'] = $task['owner_id'];
}
// Check if the category exists
if ($task['category_id'] && $this->category->exists($task['category_id'], $task['project_id'])) {
$values['category_id'] = $task['category_id'];
}
// Save task // Save task
if (! $this->db->table(self::TABLE)->save($task)) { if (! $this->db->table(self::TABLE)->save($values)) {
$this->db->cancelTransaction(); $this->db->cancelTransaction();
return false; return false;
} }
$task_id = $this->db->getConnection()->getLastId(); $task_id = $this->db->getConnection()->getLastId();
// Duplicate subtasks
if (! $this->subTask->duplicate($task['id'], $task_id)) {
$this->db->cancelTransaction();
return false;
}
$this->db->closeTransaction(); $this->db->closeTransaction();
// Trigger events // Trigger events
$this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $task); $this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $values);
$this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $task); $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $values);
return $task_id; return $task_id;
} }
/**
* Duplicate a task to the same project
*
* @access public
* @param array $task Task data
* @return integer|boolean
*/
public function duplicateSameProject($task)
{
return $this->copy($task);
}
/** /**
* Duplicate a task to another project (always copy to the first column) * Duplicate a task to another project (always copy to the first column)
* *
* @access public * @access public
* @param integer $task_id Task id * @param integer $project_id Destination project id
* @param integer $project_id Destination project id * @param array $task Task data
* @return boolean * @return integer|boolean
*/ */
public function duplicateToAnotherProject($task_id, $project_id) public function duplicateToAnotherProject($project_id, array $task)
{ {
$this->db->startTransaction(); return $this->copy($task, array(
'project_id' => $project_id,
'column_id' => $this->board->getFirstColumn($project_id),
));
}
$boardModel = new Board($this->db, $this->event); /**
* Prepare data before task creation or modification
// Get the original task *
$task = $this->getById($task_id); * @access public
* @param array $values Form values
// Cleanup data */
unset($task['id']); public function prepare(array &$values)
unset($task['date_completed']); {
if (isset($values['another_task'])) {
// Assign new values unset($values['another_task']);
$task['date_creation'] = time();
$task['owner_id'] = 0;
$task['category_id'] = 0;
$task['is_active'] = 1;
$task['column_id'] = $boardModel->getFirstColumn($project_id);
$task['project_id'] = $project_id;
$task['position'] = $this->countByColumnId($task['project_id'], $task['column_id']);
// Save task
if (! $this->db->table(self::TABLE)->save($task)) {
$this->db->cancelTransaction();
return false;
} }
$task_id = $this->db->getConnection()->getLastId(); if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) {
$values['date_due'] = $this->parseDate($values['date_due']);
}
$this->db->closeTransaction(); // Force integer fields at 0 (for Postgresql)
if (isset($values['date_due']) && empty($values['date_due'])) {
$values['date_due'] = 0;
}
// Trigger events if (isset($values['score']) && empty($values['score'])) {
$this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $task); $values['score'] = 0;
$this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $task); }
return $task_id; if (isset($values['is_active'])) {
$values['is_active'] = (int) $values['is_active'];
}
} }
/** /**
@ -326,23 +401,20 @@ class Task extends Base
$this->db->startTransaction(); $this->db->startTransaction();
// Prepare data // Prepare data
if (isset($values['another_task'])) { $this->prepare($values);
unset($values['another_task']);
if (empty($values['column_id'])) {
$values['column_id'] = $this->board->getFirstColumn($values['project_id']);
} }
if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) { if (empty($values['color_id'])) {
$values['date_due'] = $this->parseDate($values['date_due']); $colors = $this->getColors();
} $values['color_id'] = key($colors);
else {
$values['date_due'] = 0;
}
if (empty($values['score'])) {
$values['score'] = 0;
} }
$values['date_creation'] = time(); $values['date_creation'] = time();
$values['position'] = $this->countByColumnId($values['project_id'], $values['column_id']); $values['date_modification'] = $values['date_creation'];
$values['position'] = $this->countByColumnId($values['project_id'], $values['column_id']) + 1;
// Save task // Save task
if (! $this->db->table(self::TABLE)->save($values)) { if (! $this->db->table(self::TABLE)->save($values)) {
@ -365,61 +437,77 @@ class Task extends Base
* Update a task * Update a task
* *
* @access public * @access public
* @param array $values Form values * @param array $values Form values
* @param boolean $trigger_Events Trigger events
* @return boolean * @return boolean
*/ */
public function update(array $values) public function update(array $values, $trigger_events = true)
{ {
// Prepare data // Fetch original task
if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) {
$values['date_due'] = $this->parseDate($values['date_due']);
}
// Force integer fields at 0 (for Postgresql)
if (isset($values['date_due']) && empty($values['date_due'])) {
$values['date_due'] = 0;
}
if (isset($values['score']) && empty($values['score'])) {
$values['score'] = 0;
}
$original_task = $this->getById($values['id']); $original_task = $this->getById($values['id']);
if ($original_task === false) { if (! $original_task) {
return false; return false;
} }
// Prepare data
$this->prepare($values);
$updated_task = $values; $updated_task = $values;
$updated_task['date_modification'] = time(); $updated_task['date_modification'] = time();
unset($updated_task['id']); unset($updated_task['id']);
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($updated_task); $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($updated_task);
// Trigger events if ($result && $trigger_events) {
if ($result) { $this->triggerUpdateEvents($original_task, $updated_task);
$events = array(
self::EVENT_CREATE_UPDATE,
self::EVENT_UPDATE,
);
if (isset($values['column_id']) && $original_task['column_id'] != $values['column_id']) {
$events[] = self::EVENT_MOVE_COLUMN;
}
else if (isset($values['position']) && $original_task['position'] != $values['position']) {
$events[] = self::EVENT_MOVE_POSITION;
}
$event_data = array_merge($original_task, $values);
$event_data['task_id'] = $original_task['id'];
foreach ($events as $event) {
$this->event->trigger($event, $event_data);
}
} }
return $result; return true;
}
/**
* Trigger events for task modification
*
* @access public
* @param array $original_task Original task data
* @param array $updated_task Updated task data
*/
public function triggerUpdateEvents(array $original_task, array $updated_task)
{
$events = array();
if (isset($updated_task['owner_id']) && $original_task['owner_id'] != $updated_task['owner_id']) {
$events[] = self::EVENT_ASSIGNEE_CHANGE;
}
else if (isset($updated_task['column_id']) && $original_task['column_id'] != $updated_task['column_id']) {
$events[] = self::EVENT_MOVE_COLUMN;
}
else if (isset($updated_task['position']) && $original_task['position'] != $updated_task['position']) {
$events[] = self::EVENT_MOVE_POSITION;
}
else {
$events[] = self::EVENT_CREATE_UPDATE;
$events[] = self::EVENT_UPDATE;
}
$event_data = array_merge($original_task, $updated_task);
$event_data['task_id'] = $original_task['id'];
foreach ($events as $event) {
$this->event->trigger($event, $event_data);
}
}
/**
* Return true if the project exists
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function exists($task_id)
{
return $this->db->table(self::TABLE)->eq('id', $task_id)->count() === 1;
} }
/** /**
@ -431,6 +519,10 @@ class Task extends Base
*/ */
public function close($task_id) public function close($task_id)
{ {
if (! $this->exists($task_id)) {
return false;
}
$result = $this->db $result = $this->db
->table(self::TABLE) ->table(self::TABLE)
->eq('id', $task_id) ->eq('id', $task_id)
@ -455,12 +547,16 @@ class Task extends Base
*/ */
public function open($task_id) public function open($task_id)
{ {
if (! $this->exists($task_id)) {
return false;
}
$result = $this->db $result = $this->db
->table(self::TABLE) ->table(self::TABLE)
->eq('id', $task_id) ->eq('id', $task_id)
->update(array( ->update(array(
'is_active' => 1, 'is_active' => 1,
'date_completed' => '' 'date_completed' => 0
)); ));
if ($result) { if ($result) {
@ -479,8 +575,11 @@ class Task extends Base
*/ */
public function remove($task_id) public function remove($task_id)
{ {
$file = new File($this->db, $this->event); if (! $this->exists($task_id)) {
$file->removeAll($task_id); return false;
}
$this->file->removeAll($task_id);
return $this->db->table(self::TABLE)->eq('id', $task_id)->remove(); return $this->db->table(self::TABLE)->eq('id', $task_id)->remove();
} }
@ -489,20 +588,146 @@ class Task extends Base
* Move a task to another column or to another position * Move a task to another column or to another position
* *
* @access public * @access public
* @param integer $task_id Task id * @param integer $project_id Project id
* @param integer $column_id Column id * @param integer $task_id Task id
* @param integer $position Position (must be greater than 1) * @param integer $column_id Column id
* @param integer $position Position (must be >= 1)
* @return boolean * @return boolean
*/ */
public function move($task_id, $column_id, $position) public function movePosition($project_id, $task_id, $column_id, $position)
{ {
$this->event->clearTriggeredEvents(); // The position can't be lower than 1
if ($position < 1) {
return false;
}
return $this->update(array( $board = $this->db->table(Board::TABLE)->eq('project_id', $project_id)->asc('position')->findAllByColumn('id');
'id' => $task_id, $columns = array();
'column_id' => $column_id,
'position' => $position, // Prepare the columns
)); foreach ($board as $board_column_id) {
$columns[$board_column_id] = $this->db->table(self::TABLE)
->eq('is_active', 1)
->eq('project_id', $project_id)
->eq('column_id', $board_column_id)
->neq('id', $task_id)
->asc('position')
->findAllByColumn('id');
}
// The column must exists
if (! isset($columns[$column_id])) {
return false;
}
// We put our task to the new position
array_splice($columns[$column_id], $position - 1, 0, $task_id); // print_r($columns);
// We save the new positions for all tasks
return $this->savePositions($task_id, $columns);
}
/**
* Save task positions
*
* @access private
* @param integer $moved_task_id Id of the moved task
* @param array $columns Sorted tasks
* @return boolean
*/
private function savePositions($moved_task_id, array $columns)
{
$this->db->startTransaction();
foreach ($columns as $column_id => $column) {
$position = 1;
foreach ($column as $task_id) {
if ($task_id == $moved_task_id) {
// Events will be triggered only for that task
$result = $this->update(array(
'id' => $task_id,
'position' => $position,
'column_id' => $column_id
));
}
else {
$result = $this->db->table(self::TABLE)->eq('id', $task_id)->update(array(
'position' => $position,
'column_id' => $column_id
));
}
$position++;
if (! $result) {
$this->db->cancelTransaction();
return false;
}
}
}
$this->db->closeTransaction();
return true;
}
/**
* Move a task to another project
*
* @access public
* @param integer $project_id Project id
* @param array $task Task data
* @return boolean
*/
public function moveToAnotherProject($project_id, array $task)
{
$values = array();
// Clear values (categories are different for each project)
$values['category_id'] = 0;
$values['owner_id'] = 0;
// Check if the assigned user is allowed for the new project
if ($task['owner_id'] && $this->project->isUserAllowed($project_id, $task['owner_id'])) {
$values['owner_id'] = $task['owner_id'];
}
// We use the first column of the new project
$values['column_id'] = $this->board->getFirstColumn($project_id);
$values['position'] = $this->countByColumnId($project_id, $values['column_id']) + 1;
$values['project_id'] = $project_id;
if ($this->db->table(self::TABLE)->eq('id', $task['id'])->update($values)) {
return $task['id'];
}
return false;
}
/**
* Common validation rules
*
* @access private
* @return array
*/
private function commonValidationRules()
{
return array(
new Validators\Integer('id', t('This value must be an integer')),
new Validators\Integer('project_id', t('This value must be an integer')),
new Validators\Integer('column_id', t('This value must be an integer')),
new Validators\Integer('owner_id', t('This value must be an integer')),
new Validators\Integer('creator_id', t('This value must be an integer')),
new Validators\Integer('score', t('This value must be an integer')),
new Validators\Integer('category_id', t('This value must be an integer')),
new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200),
new Validators\Date('date_due', t('Invalid date'), $this->getDateFormats()),
);
} }
/** /**
@ -514,19 +739,12 @@ class Task extends Base
*/ */
public function validateCreation(array $values) public function validateCreation(array $values)
{ {
$v = new Validator($values, array( $rules = array(
new Validators\Required('color_id', t('The color is required')),
new Validators\Required('project_id', t('The project is required')), new Validators\Required('project_id', t('The project is required')),
new Validators\Integer('project_id', t('This value must be an integer')),
new Validators\Required('column_id', t('The column is required')),
new Validators\Integer('column_id', t('This value must be an integer')),
new Validators\Integer('owner_id', t('This value must be an integer')),
new Validators\Integer('creator_id', t('This value must be an integer')),
new Validators\Integer('score', t('This value must be an integer')),
new Validators\Required('title', t('The title is required')), new Validators\Required('title', t('The title is required')),
new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200), );
new Validators\Date('date_due', t('Invalid date'), $this->getDateFormats()),
)); $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
return array( return array(
$v->execute(), $v->execute(),
@ -543,11 +761,12 @@ class Task extends Base
*/ */
public function validateDescriptionCreation(array $values) public function validateDescriptionCreation(array $values)
{ {
$v = new Validator($values, array( $rules = array(
new Validators\Required('id', t('The id is required')), new Validators\Required('id', t('The id is required')),
new Validators\Integer('id', t('This value must be an integer')),
new Validators\Required('description', t('The description is required')), new Validators\Required('description', t('The description is required')),
)); );
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
return array( return array(
$v->execute(), $v->execute(),
@ -564,20 +783,11 @@ class Task extends Base
*/ */
public function validateModification(array $values) public function validateModification(array $values)
{ {
$v = new Validator($values, array( $rules = array(
new Validators\Required('id', t('The id is required')), new Validators\Required('id', t('The id is required')),
new Validators\Integer('id', t('This value must be an integer')), );
new Validators\Required('color_id', t('The color is required')),
new Validators\Required('project_id', t('The project is required')), $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
new Validators\Integer('project_id', t('This value must be an integer')),
new Validators\Required('column_id', t('The column is required')),
new Validators\Integer('column_id', t('This value must be an integer')),
new Validators\Integer('owner_id', t('This value must be an integer')),
new Validators\Integer('score', t('This value must be an integer')),
new Validators\Required('title', t('The title is required')),
new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200),
new Validators\Date('date_due', t('Invalid date'), $this->getDateFormats()),
));
return array( return array(
$v->execute(), $v->execute(),
@ -594,14 +804,59 @@ class Task extends Base
*/ */
public function validateAssigneeModification(array $values) public function validateAssigneeModification(array $values)
{ {
$v = new Validator($values, array( $rules = array(
new Validators\Required('id', t('The id is required')), new Validators\Required('id', t('The id is required')),
new Validators\Integer('id', t('This value must be an integer')),
new Validators\Required('project_id', t('The project is required')), new Validators\Required('project_id', t('The project is required')),
new Validators\Integer('project_id', t('This value must be an integer')),
new Validators\Required('owner_id', t('This value is required')), new Validators\Required('owner_id', t('This value is required')),
new Validators\Integer('owner_id', t('This value must be an integer')), );
));
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Validate category change
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateCategoryModification(array $values)
{
$rules = array(
new Validators\Required('id', t('The id is required')),
new Validators\Required('project_id', t('The project is required')),
new Validators\Required('category_id', t('This value is required')),
);
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Validate project modification
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateProjectModification(array $values)
{
$rules = array(
new Validators\Required('id', t('The id is required')),
new Validators\Required('project_id', t('The project is required')),
);
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
return array( return array(
$v->execute(), $v->execute(),
@ -667,4 +922,112 @@ class Task extends Base
'Y_m_d', 'Y_m_d',
); );
} }
/**
* For a given timestamp, reset the date to midnight
*
* @access public
* @param integer $timestamp Timestamp
* @return integer
*/
public function resetDateToMidnight($timestamp)
{
return mktime(0, 0, 0, date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp));
}
/**
* Export a list of tasks for a given project and date range
*
* @access public
* @param integer $project_id Project id
* @param mixed $from Start date (timestamp or user formatted date)
* @param mixed $to End date (timestamp or user formatted date)
* @return array
*/
public function export($project_id, $from, $to)
{
$sql = '
SELECT
tasks.id,
projects.name AS project_name,
tasks.is_active,
project_has_categories.name AS category_name,
columns.title AS column_title,
tasks.position,
tasks.color_id,
tasks.date_due,
creators.username AS creator_username,
users.username AS assignee_username,
tasks.score,
tasks.title,
tasks.date_creation,
tasks.date_modification,
tasks.date_completed
FROM tasks
LEFT JOIN users ON users.id = tasks.owner_id
LEFT JOIN users AS creators ON creators.id = tasks.creator_id
LEFT JOIN project_has_categories ON project_has_categories.id = tasks.category_id
LEFT JOIN columns ON columns.id = tasks.column_id
LEFT JOIN projects ON projects.id = tasks.project_id
WHERE tasks.date_creation >= ? AND tasks.date_creation <= ? AND tasks.project_id = ?
';
if (! is_numeric($from)) {
$from = $this->resetDateToMidnight($this->parseDate($from));
}
if (! is_numeric($to)) {
$to = $this->resetDateToMidnight(strtotime('+1 day', $this->parseDate($to)));
}
$rq = $this->db->execute($sql, array($from, $to, $project_id));
$tasks = $rq->fetchAll(PDO::FETCH_ASSOC);
$columns = array(
e('Task Id'),
e('Project'),
e('Status'),
e('Category'),
e('Column'),
e('Position'),
e('Color'),
e('Due date'),
e('Creator'),
e('Assignee'),
e('Complexity'),
e('Title'),
e('Creation date'),
e('Modification date'),
e('Completion date'),
);
$results = array($columns);
foreach ($tasks as &$task) {
$results[] = array_values($this->formatOutput($task));
}
return $results;
}
/**
* Format the output of a task array
*
* @access public
* @param array $task Task properties
* @return array
*/
public function formatOutput(array &$task)
{
$colors = $this->getColors();
$task['score'] = $task['score'] ?: '';
$task['is_active'] = $task['is_active'] == self::STATUS_OPEN ? e('Open') : e('Closed');
$task['color_id'] = $colors[$task['color_id']];
$task['date_creation'] = date('Y-m-d', $task['date_creation']);
$task['date_due'] = $task['date_due'] ? date('Y-m-d', $task['date_due']) : '';
$task['date_modification'] = $task['date_modification'] ? date('Y-m-d', $task['date_modification']) : '';
$task['date_completed'] = $task['date_completed'] ? date('Y-m-d', $task['date_completed']) : '';
return $task;
}
} }

View file

@ -0,0 +1,160 @@
<?php
namespace Model;
use PDO;
use Core\Registry;
use Event\TaskHistoryListener;
/**
* Task history model
*
* @package model
* @author Frederic Guillot
*/
class TaskHistory extends BaseHistory
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'task_has_events';
/**
* Maximum number of events
*
* @var integer
*/
const MAX_EVENTS = 5000;
/**
* Constructor
*
* @access public
* @param \Core\Registry $registry Registry instance
*/
public function __construct(Registry $registry)
{
parent::__construct($registry);
$this->table = self::TABLE;
}
/**
* Create a new event
*
* @access public
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param integer $creator_id Author of the event (user id)
* @param string $event_name Task event name
* @return boolean
*/
public function create($project_id, $task_id, $creator_id, $event_name)
{
$values = array(
'project_id' => $project_id,
'task_id' => $task_id,
'creator_id' => $creator_id,
'event_name' => $event_name,
'date_creation' => time(),
);
$this->db->startTransaction();
$this->cleanup(self::MAX_EVENTS - 1);
$result = $this->db->table(self::TABLE)->insert($values);
$this->db->closeTransaction();
return $result;
}
/**
* Get all necessary content to display activity feed
*
* $author_name
* $author_username
* $task['id', 'title', 'position', 'column_name']
*/
public function getAllContentByProjectId($project_id, $limit = 50)
{
$sql = '
SELECT
task_has_events.id,
task_has_events.date_creation,
task_has_events.event_name,
task_has_events.task_id,
tasks.title as task_title,
tasks.position as task_position,
columns.title as task_column_name,
users.username as author_username,
users.name as author_name
FROM task_has_events
LEFT JOIN users ON users.id=task_has_events.creator_id
LEFT JOIN tasks ON tasks.id=task_has_events.task_id
LEFT JOIN columns ON columns.id=tasks.column_id
WHERE task_has_events.project_id = ?
ORDER BY task_has_events.id DESC
LIMIT '.$limit.' OFFSET 0
';
$rq = $this->db->execute($sql, array($project_id));
$events = $rq->fetchAll(PDO::FETCH_ASSOC);
foreach ($events as &$event) {
$event['author'] = $event['author_name'] ?: $event['author_username'];
$event['event_title'] = $this->getTitle($event);
$event['event_content'] = $this->getContent($event);
$event['event_type'] = 'task';
}
return $events;
}
/**
* Get the event title (translated)
*
* @access public
* @param array $event Event properties
* @return string
*/
public function getTitle(array $event)
{
$titles = array(
Task::EVENT_ASSIGNEE_CHANGE => t('%s change the assignee of the task #%d', $event['author'], $event['task_id']),
Task::EVENT_UPDATE => t('%s updated the task #%d', $event['author'], $event['task_id']),
Task::EVENT_CREATE => t('%s created the task #%d', $event['author'], $event['task_id']),
Task::EVENT_CLOSE => t('%s closed the task #%d', $event['author'], $event['task_id']),
Task::EVENT_OPEN => t('%s open the task #%d', $event['author'], $event['task_id']),
Task::EVENT_MOVE_COLUMN => t('%s moved the task #%d to the column "%s"', $event['author'], $event['task_id'], $event['task_column_name']),
Task::EVENT_MOVE_POSITION => t('%s moved the task #%d to the position %d in the column "%s"', $event['author'], $event['task_id'], $event['task_position'], $event['task_column_name']),
);
return isset($titles[$event['event_name']]) ? $titles[$event['event_name']] : '';
}
/**
* Attach events to be able to record the history
*
* @access public
*/
public function attachEvents()
{
$events = array(
Task::EVENT_ASSIGNEE_CHANGE,
Task::EVENT_UPDATE,
Task::EVENT_CREATE,
Task::EVENT_CLOSE,
Task::EVENT_OPEN,
Task::EVENT_MOVE_COLUMN,
Task::EVENT_MOVE_POSITION,
);
$listener = new TaskHistoryListener($this);
foreach ($events as $event_name) {
$this->event->attach($event_name, $listener);
}
}
}

View file

@ -27,6 +27,39 @@ class User extends Base
*/ */
const EVERYBODY_ID = -1; const EVERYBODY_ID = -1;
/**
* Get the default project from the session
*
* @access public
* @return integer
*/
public function getFavoriteProjectId()
{
return isset($_SESSION['user']['default_project_id']) ? $_SESSION['user']['default_project_id'] : 0;
}
/**
* Get the last seen project from the session
*
* @access public
* @return integer
*/
public function getLastSeenProjectId()
{
return empty($_SESSION['user']['last_show_project_id']) ? 0 : $_SESSION['user']['last_show_project_id'];
}
/**
* Set the last seen project from the session
*
* @access public
* @@param integer $project_id Project id
*/
public function storeLastSeenProjectId($project_id)
{
$_SESSION['user']['last_show_project_id'] = (int) $project_id;
}
/** /**
* Get a specific user by id * Get a specific user by id
* *
@ -86,7 +119,7 @@ class User extends Base
return $this->db return $this->db
->table(self::TABLE) ->table(self::TABLE)
->asc('username') ->asc('username')
->columns('id', 'username', 'name', 'email', 'is_admin', 'default_project_id', 'is_ldap_user') ->columns('id', 'username', 'name', 'email', 'is_admin', 'default_project_id', 'is_ldap_user', 'notifications_enabled', 'google_id', 'github_id')
->findAll(); ->findAll();
} }
@ -98,7 +131,52 @@ class User extends Base
*/ */
public function getList() public function getList()
{ {
return $this->db->table(self::TABLE)->asc('username')->listing('id', 'username'); $users = $this->db->table(self::TABLE)->columns('id', 'username', 'name')->findAll();
$result = array();
foreach ($users as $user) {
$result[$user['id']] = $user['name'] ?: $user['username'];
}
asort($result);
return $result;
}
/**
* Prepare values before an update or a create
*
* @access public
* @param array $values Form values
*/
public function prepare(array &$values)
{
if (isset($values['password'])) {
if (! empty($values['password'])) {
$values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT);
}
else {
unset($values['password']);
}
}
if (isset($values['confirmation'])) {
unset($values['confirmation']);
}
if (isset($values['current_password'])) {
unset($values['current_password']);
}
if (isset($values['is_admin']) && empty($values['is_admin'])) {
$values['is_admin'] = 0;
}
if (isset($values['is_ldap_user']) && empty($values['is_ldap_user'])) {
$values['is_ldap_user'] = 0;
}
} }
/** /**
@ -110,22 +188,7 @@ class User extends Base
*/ */
public function create(array $values) public function create(array $values)
{ {
if (isset($values['confirmation'])) { $this->prepare($values);
unset($values['confirmation']);
}
if (isset($values['password'])) {
$values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT);
}
if (empty($values['is_admin'])) {
$values['is_admin'] = 0;
}
if (empty($values['is_ldap_user'])) {
$values['is_ldap_user'] = 0;
}
return $this->db->table(self::TABLE)->save($values); return $this->db->table(self::TABLE)->save($values);
} }
@ -138,31 +201,10 @@ class User extends Base
*/ */
public function update(array $values) public function update(array $values)
{ {
if (! empty($values['password'])) { $this->prepare($values);
$values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT);
}
else {
unset($values['password']);
}
if (isset($values['confirmation'])) {
unset($values['confirmation']);
}
if (isset($values['current_password'])) {
unset($values['current_password']);
}
if (empty($values['is_admin'])) {
$values['is_admin'] = 0;
}
if (empty($values['is_ldap_user'])) {
$values['is_ldap_user'] = 0;
}
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
// If the user is connected refresh his session
if (session_id() !== '' && $_SESSION['user']['id'] == $values['id']) { if (session_id() !== '' && $_SESSION['user']['id'] == $values['id']) {
$this->updateSession(); $this->updateSession();
} }
@ -182,12 +224,12 @@ class User extends Base
$this->db->startTransaction(); $this->db->startTransaction();
// All tasks assigned to this user will be unassigned // All tasks assigned to this user will be unassigned
$this->db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => '')); $this->db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => 0));
$this->db->table(self::TABLE)->eq('id', $user_id)->remove(); $result = $this->db->table(self::TABLE)->eq('id', $user_id)->remove();
$this->db->closeTransaction(); $this->db->closeTransaction();
return true; return $result;
} }
/** /**
@ -214,6 +256,39 @@ class User extends Base
$_SESSION['user'] = $user; $_SESSION['user'] = $user;
} }
/**
* Common validation rules
*
* @access private
* @return array
*/
private function commonValidationRules()
{
return array(
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'),
new Validators\Email('email', t('Email address invalid')),
new Validators\Integer('default_project_id', t('This value must be an integer')),
new Validators\Integer('is_admin', t('This value must be an integer')),
);
}
/**
* Common password validation rules
*
* @access private
* @return array
*/
private function commonPasswordValidationRules()
{
return array(
new Validators\Required('password', t('The password is required')),
new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6),
new Validators\Required('confirmation', t('The confirmation is required')),
new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')),
);
}
/** /**
* Validate user creation * Validate user creation
* *
@ -223,18 +298,11 @@ class User extends Base
*/ */
public function validateCreation(array $values) public function validateCreation(array $values)
{ {
$v = new Validator($values, array( $rules = array(
new Validators\Required('username', t('The username is required')), new Validators\Required('username', t('The username is required')),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), );
new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'),
new Validators\Required('password', t('The password is required')), $v = new Validator($values, array_merge($rules, $this->commonValidationRules(), $this->commonPasswordValidationRules()));
new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6),
new Validators\Required('confirmation', t('The confirmation is required')),
new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')),
new Validators\Integer('default_project_id', t('This value must be an integer')),
new Validators\Integer('is_admin', t('This value must be an integer')),
new Validators\Email('email', t('Email address invalid')),
));
return array( return array(
$v->execute(), $v->execute(),
@ -251,19 +319,33 @@ class User extends Base
*/ */
public function validateModification(array $values) public function validateModification(array $values)
{ {
if (! empty($values['password'])) { $rules = array(
return $this->validatePasswordModification($values);
}
$v = new Validator($values, array(
new Validators\Required('id', t('The user id is required')), new Validators\Required('id', t('The user id is required')),
new Validators\Required('username', t('The username is required')), new Validators\Required('username', t('The username is required')),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), );
new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'),
new Validators\Integer('default_project_id', t('This value must be an integer')), $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
new Validators\Integer('is_admin', t('This value must be an integer')),
new Validators\Email('email', t('Email address invalid')), return array(
)); $v->execute(),
$v->getErrors()
);
}
/**
* Validate user API modification
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateApiModification(array $values)
{
$rules = array(
new Validators\Required('id', t('The user id is required')),
);
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
return array( return array(
$v->execute(), $v->execute(),
@ -280,27 +362,17 @@ class User extends Base
*/ */
public function validatePasswordModification(array $values) public function validatePasswordModification(array $values)
{ {
$v = new Validator($values, array( $rules = array(
new Validators\Required('id', t('The user id is required')), new Validators\Required('id', t('The user id is required')),
new Validators\Required('username', t('The username is required')),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'),
new Validators\Required('current_password', t('The current password is required')), new Validators\Required('current_password', t('The current password is required')),
new Validators\Required('password', t('The password is required')), );
new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6),
new Validators\Required('confirmation', t('The confirmation is required')), $v = new Validator($values, array_merge($rules, $this->commonPasswordValidationRules()));
new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')),
new Validators\Integer('default_project_id', t('This value must be an integer')),
new Validators\Integer('is_admin', t('This value must be an integer')),
new Validators\Email('email', t('Email address invalid')),
));
if ($v->execute()) { if ($v->execute()) {
// Check password // Check password
list($authenticated,) = $this->authenticate($_SESSION['user']['username'], $values['current_password']); if ($this->authentication->authenticate($_SESSION['user']['username'], $values['current_password'])) {
if ($authenticated) {
return array(true, array()); return array(true, array());
} }
else { else {
@ -311,87 +383,6 @@ class User extends Base
return array(false, $v->getErrors()); return array(false, $v->getErrors());
} }
/**
* Validate user login
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateLogin(array $values)
{
$v = new Validator($values, array(
new Validators\Required('username', t('The username is required')),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
new Validators\Required('password', t('The password is required')),
));
$result = $v->execute();
$errors = $v->getErrors();
if ($result) {
list($authenticated, $method) = $this->authenticate($values['username'], $values['password']);
if ($authenticated === true) {
// Create the user session
$user = $this->getByUsername($values['username']);
$this->updateSession($user);
// Update login history
$lastLogin = new LastLogin($this->db, $this->event);
$lastLogin->create(
$method,
$user['id'],
$this->getIpAddress(),
$this->getUserAgent()
);
// Setup the remember me feature
if (! empty($values['remember_me'])) {
$rememberMe = new RememberMe($this->db, $this->event);
$credentials = $rememberMe->create($user['id'], $this->getIpAddress(), $this->getUserAgent());
$rememberMe->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']);
}
}
else {
$result = false;
$errors['login'] = t('Bad username or password');
}
}
return array(
$result,
$errors
);
}
/**
* Authenticate a user
*
* @access public
* @param string $username Username
* @param string $password Password
* @return array
*/
public function authenticate($username, $password)
{
// Database authentication
$user = $this->db->table(self::TABLE)->eq('username', $username)->eq('is_ldap_user', 0)->findOne();
$authenticated = $user && \password_verify($password, $user['password']);
$method = LastLogin::AUTH_DATABASE;
// LDAP authentication
if (! $authenticated && LDAP_AUTH) {
$ldap = new Ldap($this->db, $this->event);
$authenticated = $ldap->authenticate($username, $password);
$method = LastLogin::AUTH_LDAP;
}
return array($authenticated, $method);
}
/** /**
* Get the user agent of the connected user * Get the user agent of the connected user
* *

View file

@ -0,0 +1,144 @@
<?php
namespace Model;
use Event\WebhookListener;
/**
* Webhook model
*
* @package model
* @author Frederic Guillot
*/
class Webhook extends Base
{
/**
* HTTP connection timeout in seconds
*
* @var integer
*/
const HTTP_TIMEOUT = 1;
/**
* Number of maximum redirections for the HTTP client
*
* @var integer
*/
const HTTP_MAX_REDIRECTS = 3;
/**
* HTTP client user agent
*
* @var string
*/
const HTTP_USER_AGENT = 'Kanboard Webhook';
/**
* URL to call for task creation
*
* @access private
* @var string
*/
private $url_task_creation = '';
/**
* URL to call for task modification
*
* @access private
* @var string
*/
private $url_task_modification = '';
/**
* Webook token
*
* @access private
* @var string
*/
private $token = '';
/**
* Attach events
*
* @access public
*/
public function attachEvents()
{
$this->url_task_creation = $this->config->get('webhooks_url_task_creation');
$this->url_task_modification = $this->config->get('webhooks_url_task_modification');
$this->token = $this->config->get('webhooks_token');
if ($this->url_task_creation) {
$this->attachCreateEvents();
}
if ($this->url_task_modification) {
$this->attachUpdateEvents();
}
}
/**
* Attach events for task modification
*
* @access public
*/
public function attachUpdateEvents()
{
$events = array(
Task::EVENT_UPDATE,
Task::EVENT_CLOSE,
Task::EVENT_OPEN,
);
$listener = new WebhookListener($this->url_task_modification, $this);
foreach ($events as $event_name) {
$this->event->attach($event_name, $listener);
}
}
/**
* Attach events for task creation
*
* @access public
*/
public function attachCreateEvents()
{
$this->event->attach(Task::EVENT_CREATE, new WebhookListener($this->url_task_creation, $this));
}
/**
* Call the external URL
*
* @access public
* @param string $url URL to call
* @param array $task Task data
*/
public function notify($url, array $task)
{
$headers = array(
'Connection: close',
'User-Agent: '.self::HTTP_USER_AGENT,
);
$context = stream_context_create(array(
'http' => array(
'method' => 'POST',
'protocol_version' => 1.1,
'timeout' => self::HTTP_TIMEOUT,
'max_redirects' => self::HTTP_MAX_REDIRECTS,
'header' => implode("\r\n", $headers),
'content' => json_encode($task)
)
));
if (strpos($url, '?') !== false) {
$url .= '&token='.$this->token;
}
else {
$url .= '?token='.$this->token;
}
@file_get_contents($url, false, $context);
}
}

View file

@ -4,7 +4,98 @@ namespace Schema;
use Core\Security; use Core\Security;
const VERSION = 21; const VERSION = 27;
function version_27($pdo)
{
$pdo->exec('CREATE UNIQUE INDEX users_username_idx ON users(username)');
}
function version_26($pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN default_columns VARCHAR(255) DEFAULT ''");
}
function version_25($pdo)
{
$pdo->exec("
CREATE TABLE task_has_events (
id INT NOT NULL AUTO_INCREMENT,
date_creation INT NOT NULL,
event_name TEXT NOT NULL,
creator_id INT,
project_id INT,
task_id INT,
data TEXT,
FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
PRIMARY KEY (id)
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE subtask_has_events (
id INT NOT NULL AUTO_INCREMENT,
date_creation INT NOT NULL,
event_name TEXT NOT NULL,
creator_id INT,
project_id INT,
subtask_id INT,
task_id INT,
data TEXT,
FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(subtask_id) REFERENCES task_has_subtasks(id) ON DELETE CASCADE,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
PRIMARY KEY (id)
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE comment_has_events (
id INT NOT NULL AUTO_INCREMENT,
date_creation INT NOT NULL,
event_name TEXT NOT NULL,
creator_id INT,
project_id INT,
comment_id INT,
task_id INT,
data TEXT,
FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(comment_id) REFERENCES comments(id) ON DELETE CASCADE,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
PRIMARY KEY (id)
) ENGINE=InnoDB CHARSET=utf8
");
}
function version_24($pdo)
{
$pdo->exec("ALTER TABLE projects ADD COLUMN is_public TINYINT(1) DEFAULT '0'");
}
function version_23($pdo)
{
$pdo->exec("ALTER TABLE users ADD COLUMN notifications_enabled TINYINT(1) DEFAULT '0'");
$pdo->exec("
CREATE TABLE user_has_notifications (
user_id INT,
project_id INT,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
UNIQUE(project_id, user_id)
);
");
}
function version_22($pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_modification VARCHAR(255)");
$pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_creation VARCHAR(255)");
}
function version_21($pdo) function version_21($pdo)
{ {
@ -19,7 +110,8 @@ function version_20($pdo)
function version_19($pdo) function version_19($pdo)
{ {
$pdo->exec("ALTER TABLE config ADD COLUMN api_token VARCHAR(255) DEFAULT '".Security::generateToken()."'"); $pdo->exec("ALTER TABLE config ADD COLUMN api_token VARCHAR(255) DEFAULT ''");
$pdo->exec("UPDATE config SET api_token='".Security::generateToken()."'");
} }
function version_18($pdo) function version_18($pdo)
@ -31,7 +123,7 @@ function version_18($pdo)
status INT DEFAULT 0, status INT DEFAULT 0,
time_estimated INT DEFAULT 0, time_estimated INT DEFAULT 0,
time_spent INT DEFAULT 0, time_spent INT DEFAULT 0,
task_id INT, task_id INT NOT NULL,
user_id INT, user_id INT,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
@ -119,52 +211,12 @@ function version_12($pdo)
); );
} }
function version_11($pdo)
{
}
function version_10($pdo)
{
}
function version_9($pdo)
{
}
function version_8($pdo)
{
}
function version_7($pdo)
{
}
function version_6($pdo)
{
}
function version_5($pdo)
{
}
function version_4($pdo)
{
}
function version_3($pdo)
{
}
function version_2($pdo)
{
}
function version_1($pdo) function version_1($pdo)
{ {
$pdo->exec(" $pdo->exec("
CREATE TABLE config ( CREATE TABLE config (
language CHAR(5) DEFAULT 'en_US', language CHAR(5) DEFAULT 'en_US',
webhooks_token VARCHAR(255), webhooks_token VARCHAR(255) DEFAULT '',
timezone VARCHAR(50) DEFAULT 'UTC' timezone VARCHAR(50) DEFAULT 'UTC'
) ENGINE=InnoDB CHARSET=utf8 ) ENGINE=InnoDB CHARSET=utf8
"); ");

View file

@ -4,7 +4,95 @@ namespace Schema;
use Core\Security; use Core\Security;
const VERSION = 2; const VERSION = 8;
function version_8($pdo)
{
$pdo->exec('CREATE UNIQUE INDEX users_username_idx ON users(username)');
}
function version_7($pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN default_columns VARCHAR(255) DEFAULT ''");
}
function version_6($pdo)
{
$pdo->exec("
CREATE TABLE task_has_events (
id SERIAL PRIMARY KEY,
date_creation INTEGER NOT NULL,
event_name TEXT NOT NULL,
creator_id INTEGER,
project_id INTEGER,
task_id INTEGER,
data TEXT,
FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
);
");
$pdo->exec("
CREATE TABLE subtask_has_events (
id SERIAL PRIMARY KEY,
date_creation INTEGER NOT NULL,
event_name TEXT NOT NULL,
creator_id INTEGER,
project_id INTEGER,
subtask_id INTEGER,
task_id INTEGER,
data TEXT,
FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(subtask_id) REFERENCES task_has_subtasks(id) ON DELETE CASCADE,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
);
");
$pdo->exec("
CREATE TABLE comment_has_events (
id SERIAL PRIMARY KEY,
date_creation INTEGER NOT NULL,
event_name TEXT NOT NULL,
creator_id INTEGER,
project_id INTEGER,
comment_id INTEGER,
task_id INTEGER,
data TEXT,
FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(comment_id) REFERENCES comments(id) ON DELETE CASCADE,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
);
");
}
function version_5($pdo)
{
$pdo->exec("ALTER TABLE projects ADD COLUMN is_public BOOLEAN DEFAULT '0'");
}
function version_4($pdo)
{
$pdo->exec("ALTER TABLE users ADD COLUMN notifications_enabled BOOLEAN DEFAULT '0'");
$pdo->exec("
CREATE TABLE user_has_notifications (
user_id INTEGER,
project_id INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
UNIQUE(project_id, user_id)
);
");
}
function version_3($pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_modification VARCHAR(255)");
$pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_creation VARCHAR(255)");
}
function version_2($pdo) function version_2($pdo)
{ {
@ -17,9 +105,9 @@ function version_1($pdo)
$pdo->exec(" $pdo->exec("
CREATE TABLE config ( CREATE TABLE config (
language CHAR(5) DEFAULT 'en_US', language CHAR(5) DEFAULT 'en_US',
webhooks_token VARCHAR(255), webhooks_token VARCHAR(255) DEFAULT '',
timezone VARCHAR(50) DEFAULT 'UTC', timezone VARCHAR(50) DEFAULT 'UTC',
api_token VARCHAR(255) api_token VARCHAR(255) DEFAULT ''
); );
CREATE TABLE users ( CREATE TABLE users (
@ -117,7 +205,7 @@ function version_1($pdo)
status SMALLINT DEFAULT 0, status SMALLINT DEFAULT 0,
time_estimated INTEGER DEFAULT 0, time_estimated INTEGER DEFAULT 0,
time_spent INTEGER DEFAULT 0, time_spent INTEGER DEFAULT 0,
task_id INTEGER, task_id INTEGER NOT NULL,
user_id INTEGER, user_id INTEGER,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
); );

View file

@ -4,7 +4,95 @@ namespace Schema;
use Core\Security; use Core\Security;
const VERSION = 21; const VERSION = 27;
function version_27($pdo)
{
$pdo->exec('CREATE UNIQUE INDEX users_username_idx ON users(username)');
}
function version_26($pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN default_columns TEXT DEFAULT ''");
}
function version_25($pdo)
{
$pdo->exec("
CREATE TABLE task_has_events (
id INTEGER PRIMARY KEY,
date_creation INTEGER NOT NULL,
event_name TEXT NOT NULL,
creator_id INTEGER,
project_id INTEGER,
task_id INTEGER,
data TEXT,
FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
);
");
$pdo->exec("
CREATE TABLE subtask_has_events (
id INTEGER PRIMARY KEY,
date_creation INTEGER NOT NULL,
event_name TEXT NOT NULL,
creator_id INTEGER,
project_id INTEGER,
subtask_id INTEGER,
task_id INTEGER,
data TEXT,
FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(subtask_id) REFERENCES task_has_subtasks(id) ON DELETE CASCADE,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
);
");
$pdo->exec("
CREATE TABLE comment_has_events (
id INTEGER PRIMARY KEY,
date_creation INTEGER NOT NULL,
event_name TEXT NOT NULL,
creator_id INTEGER,
project_id INTEGER,
comment_id INTEGER,
task_id INTEGER,
data TEXT,
FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(comment_id) REFERENCES comments(id) ON DELETE CASCADE,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
);
");
}
function version_24($pdo)
{
$pdo->exec('ALTER TABLE projects ADD COLUMN is_public INTEGER DEFAULT "0"');
}
function version_23($pdo)
{
$pdo->exec("ALTER TABLE users ADD COLUMN notifications_enabled INTEGER DEFAULT '0'");
$pdo->exec("
CREATE TABLE user_has_notifications (
user_id INTEGER,
project_id INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
UNIQUE(project_id, user_id)
);
");
}
function version_22($pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_modification TEXT");
$pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_creation TEXT");
}
function version_21($pdo) function version_21($pdo)
{ {
@ -19,7 +107,8 @@ function version_20($pdo)
function version_19($pdo) function version_19($pdo)
{ {
$pdo->exec("ALTER TABLE config ADD COLUMN api_token TEXT DEFAULT '".Security::generateToken()."'"); $pdo->exec("ALTER TABLE config ADD COLUMN api_token TEXT DEFAULT ''");
$pdo->exec("UPDATE config SET api_token='".Security::generateToken()."'");
} }
function version_18($pdo) function version_18($pdo)
@ -31,7 +120,7 @@ function version_18($pdo)
status INTEGER DEFAULT 0, status INTEGER DEFAULT 0,
time_estimated INTEGER DEFAULT 0, time_estimated INTEGER DEFAULT 0,
time_spent INTEGER DEFAULT 0, time_spent INTEGER DEFAULT 0,
task_id INTEGER, task_id INTEGER NOT NULL,
user_id INTEGER, user_id INTEGER,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
)" )"
@ -217,19 +306,6 @@ function version_4($pdo)
function version_3($pdo) function version_3($pdo)
{ {
$pdo->exec('ALTER TABLE projects ADD COLUMN token TEXT'); $pdo->exec('ALTER TABLE projects ADD COLUMN token TEXT');
// For each existing project, assign a different token
$rq = $pdo->prepare("SELECT id FROM projects WHERE token IS NULL");
$rq->execute();
$results = $rq->fetchAll(\PDO::FETCH_ASSOC);
if ($results !== false) {
foreach ($results as &$result) {
$rq = $pdo->prepare('UPDATE projects SET token=? WHERE id=?');
$rq->execute(array(Security::generateToken(), $result['id']));
}
}
} }
function version_2($pdo) function version_2($pdo)
@ -242,8 +318,8 @@ function version_1($pdo)
{ {
$pdo->exec(" $pdo->exec("
CREATE TABLE config ( CREATE TABLE config (
language TEXT, language TEXT DEFAULT 'en_US',
webhooks_token TEXT webhooks_token TEXT DEFAULT ''
) )
"); ");
@ -301,7 +377,7 @@ function version_1($pdo)
$pdo->exec(" $pdo->exec("
INSERT INTO config INSERT INTO config
(language, webhooks_token) (webhooks_token)
VALUES ('en_US', '".Security::generateToken()."') VALUES ('".Security::generateToken()."')
"); ");
} }

View file

@ -1,77 +1,70 @@
<section id="main"> <div class="page-header">
<div class="page-header"> <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2>
<h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2> </div>
<ul>
<li><a href="?controller=project"><?= t('All projects') ?></a></li> <?php if (! empty($actions)): ?>
</ul>
<h3><?= t('Defined actions') ?></h3>
<table>
<tr>
<th><?= t('Event name') ?></th>
<th><?= t('Action name') ?></th>
<th><?= t('Action parameters') ?></th>
<th><?= t('Action') ?></th>
</tr>
<?php foreach ($actions as $action): ?>
<tr>
<td><?= Helper\in_list($action['event_name'], $available_events) ?></td>
<td><?= Helper\in_list($action['action_name'], $available_actions) ?></td>
<td>
<ul>
<?php foreach ($action['params'] as $param): ?>
<li>
<?= Helper\in_list($param['name'], $available_params) ?> =
<strong>
<?php if (Helper\contains($param['name'], 'column_id')): ?>
<?= Helper\in_list($param['value'], $columns_list) ?>
<?php elseif (Helper\contains($param['name'], 'user_id')): ?>
<?= Helper\in_list($param['value'], $users_list) ?>
<?php elseif (Helper\contains($param['name'], 'project_id')): ?>
<?= Helper\in_list($param['value'], $projects_list) ?>
<?php elseif (Helper\contains($param['name'], 'color_id')): ?>
<?= Helper\in_list($param['value'], $colors_list) ?>
<?php elseif (Helper\contains($param['name'], 'category_id')): ?>
<?= Helper\in_list($param['value'], $categories_list) ?>
<?php endif ?>
</strong>
</li>
<?php endforeach ?>
</ul>
</td>
<td>
<a href="?controller=action&amp;action=confirm&amp;project_id=<?= $project['id'] ?>&amp;action_id=<?= $action['id'] ?>"><?= t('Remove') ?></a>
</td>
</tr>
<?php endforeach ?>
</table>
<?php endif ?>
<h3><?= t('Add an action') ?></h3>
<form method="post" action="?controller=action&amp;action=params&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_label(t('Event'), 'event_name') ?>
<?= Helper\form_select('event_name', $available_events, $values) ?><br/>
<?= Helper\form_label(t('Action'), 'action_name') ?>
<?= Helper\form_select('action_name', $available_actions, $values) ?><br/>
<div class="form-help">
<?= t('When the selected event occurs execute the corresponding action.') ?>
</div> </div>
<section>
<?php if (! empty($actions)): ?> <div class="form-actions">
<input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/>
<h3><?= t('Defined actions') ?></h3> </div>
<table> </form>
<tr>
<th><?= t('Event name') ?></th>
<th><?= t('Action name') ?></th>
<th><?= t('Action parameters') ?></th>
<th><?= t('Action') ?></th>
</tr>
<?php foreach ($actions as $action): ?>
<tr>
<td><?= Helper\in_list($action['event_name'], $available_events) ?></td>
<td><?= Helper\in_list($action['action_name'], $available_actions) ?></td>
<td>
<ul>
<?php foreach ($action['params'] as $param): ?>
<li>
<?= Helper\in_list($param['name'], $available_params) ?> =
<strong>
<?php if (Helper\contains($param['name'], 'column_id')): ?>
<?= Helper\in_list($param['value'], $columns_list) ?>
<?php elseif (Helper\contains($param['name'], 'user_id')): ?>
<?= Helper\in_list($param['value'], $users_list) ?>
<?php elseif (Helper\contains($param['name'], 'project_id')): ?>
<?= Helper\in_list($param['value'], $projects_list) ?>
<?php elseif (Helper\contains($param['name'], 'color_id')): ?>
<?= Helper\in_list($param['value'], $colors_list) ?>
<?php elseif (Helper\contains($param['name'], 'category_id')): ?>
<?= Helper\in_list($param['value'], $categories_list) ?>
<?php endif ?>
</strong>
</li>
<?php endforeach ?>
</ul>
</td>
<td>
<a href="?controller=action&amp;action=confirm&amp;action_id=<?= $action['id'] ?>"><?= t('Remove') ?></a>
</td>
</tr>
<?php endforeach ?>
</table>
<?php endif ?>
<h3><?= t('Add an action') ?></h3>
<form method="post" action="?controller=action&amp;action=params&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_label(t('Event'), 'event_name') ?>
<?= Helper\form_select('event_name', $available_events, $values) ?><br/>
<?= Helper\form_label(t('Action'), 'action_name') ?>
<?= Helper\form_select('action_name', $available_actions, $values) ?><br/>
<div class="form-help">
<?= t('When the selected event occurs execute the corresponding action.') ?>
</div>
<div class="form-actions">
<input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/>
</div>
</form>
</section>
</section>

View file

@ -1,43 +1,37 @@
<section id="main"> <div class="page-header">
<div class="page-header"> <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2>
<h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2> </div>
<ul> <section>
<li><a href="?controller=project"><?= t('All projects') ?></a></li>
</ul> <h3><?= t('Define action parameters') ?></h3>
<form method="post" action="?controller=action&amp;action=create&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_hidden('event_name', $values) ?>
<?= Helper\form_hidden('action_name', $values) ?>
<?php foreach ($action_params as $param_name => $param_desc): ?>
<?php if (Helper\contains($param_name, 'column_id')): ?>
<?= Helper\form_label($param_desc, $param_name) ?>
<?= Helper\form_select('params['.$param_name.']', $columns_list, $values) ?><br/>
<?php elseif (Helper\contains($param_name, 'user_id')): ?>
<?= Helper\form_label($param_desc, $param_name) ?>
<?= Helper\form_select('params['.$param_name.']', $users_list, $values) ?><br/>
<?php elseif (Helper\contains($param_name, 'project_id')): ?>
<?= Helper\form_label($param_desc, $param_name) ?>
<?= Helper\form_select('params['.$param_name.']', $projects_list, $values) ?><br/>
<?php elseif (Helper\contains($param_name, 'color_id')): ?>
<?= Helper\form_label($param_desc, $param_name) ?>
<?= Helper\form_select('params['.$param_name.']', $colors_list, $values) ?><br/>
<?php elseif (Helper\contains($param_name, 'category_id')): ?>
<?= Helper\form_label($param_desc, $param_name) ?>
<?= Helper\form_select('params['.$param_name.']', $categories_list, $values) ?><br/>
<?php endif ?>
<?php endforeach ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save this action') ?>" class="btn btn-blue"/>
<?= t('or') ?> <a href="?controller=action&amp;action=index&amp;project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a>
</div> </div>
<section> </form>
<h3><?= t('Define action parameters') ?></h3>
<form method="post" action="?controller=action&amp;action=create&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_hidden('event_name', $values) ?>
<?= Helper\form_hidden('action_name', $values) ?>
<?php foreach ($action_params as $param_name => $param_desc): ?>
<?php if (Helper\contains($param_name, 'column_id')): ?>
<?= Helper\form_label($param_desc, $param_name) ?>
<?= Helper\form_select('params['.$param_name.']', $columns_list, $values) ?><br/>
<?php elseif (Helper\contains($param_name, 'user_id')): ?>
<?= Helper\form_label($param_desc, $param_name) ?>
<?= Helper\form_select('params['.$param_name.']', $users_list, $values) ?><br/>
<?php elseif (Helper\contains($param_name, 'project_id')): ?>
<?= Helper\form_label($param_desc, $param_name) ?>
<?= Helper\form_select('params['.$param_name.']', $projects_list, $values) ?><br/>
<?php elseif (Helper\contains($param_name, 'color_id')): ?>
<?= Helper\form_label($param_desc, $param_name) ?>
<?= Helper\form_select('params['.$param_name.']', $colors_list, $values) ?><br/>
<?php elseif (Helper\contains($param_name, 'category_id')): ?>
<?= Helper\form_label($param_desc, $param_name) ?>
<?= Helper\form_select('params['.$param_name.']', $categories_list, $values) ?><br/>
<?php endif ?>
<?php endforeach ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save this action') ?>" class="btn btn-blue"/>
<?= t('or') ?> <a href="?controller=action&amp;action=index&amp;project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a>
</div>
</form>
</section>
</section>

View file

@ -1,16 +1,14 @@
<section id="main"> <div class="page-header">
<div class="page-header"> <h2><?= t('Remove an automatic action') ?></h2>
<h2><?= t('Remove an automatic action') ?></h2> </div>
</div>
<div class="confirm"> <div class="confirm">
<p class="alert alert-info"> <p class="alert alert-info">
<?= t('Do you really want to remove this action: "%s"?', Helper\in_list($action['event_name'], $available_events).'/'.Helper\in_list($action['action_name'], $available_actions)) ?> <?= t('Do you really want to remove this action: "%s"?', Helper\in_list($action['event_name'], $available_events).'/'.Helper\in_list($action['action_name'], $available_actions)) ?>
</p> </p>
<div class="form-actions"> <div class="form-actions">
<a href="?controller=action&amp;action=remove&amp;action_id=<?= $action['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> <a href="?controller=action&amp;action=remove&amp;action_id=<?= $action['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=action&amp;action=index&amp;project_id=<?= $action['project_id'] ?>"><?= t('cancel') ?></a> <?= t('or') ?> <a href="?controller=action&amp;action=index&amp;project_id=<?= $action['project_id'] ?>"><?= t('cancel') ?></a>
</div>
</div> </div>
</section> </div>

View file

@ -1,14 +1,12 @@
<section id="main"> <section id="main">
<div class="page-header board"> <div class="page-header board">
<h2> <h2><?= t('Project "%s"', $current_project_name) ?></h2>
<?= t('Project "%s"', $current_project_name) ?>
</h2>
</div> </div>
<section> <section>
<h3><?= t('Change assignee for the task "%s"', $values['title']) ?></h3> <h3><?= t('Change assignee for the task "%s"', $values['title']) ?></h3>
<form method="post" action="?controller=board&amp;action=assignTask" autocomplete="off"> <form method="post" action="?controller=board&amp;action=updateAssignee" autocomplete="off">
<?= Helper\form_csrf() ?> <?= Helper\form_csrf() ?>
<?= Helper\form_hidden('id', $values) ?> <?= Helper\form_hidden('id', $values) ?>
<?= Helper\form_hidden('project_id', $values) ?> <?= Helper\form_hidden('project_id', $values) ?>

View file

@ -0,0 +1,24 @@
<section id="main">
<div class="page-header board">
<h2><?= t('Project "%s"', $current_project_name) ?></h2>
</div>
<section>
<h3><?= t('Change category for the task "%s"', $values['title']) ?></h3>
<form method="post" action="?controller=board&amp;action=updateCategory" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('id', $values) ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_label(t('Category'), 'category_id') ?>
<?= Helper\form_select('category_id', $categories_list, $values, $errors) ?><br/>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
<?= t('or') ?> <a href="?controller=board&amp;action=show&amp;project_id=<?= $values['project_id'] ?>"><?= t('cancel') ?></a>
</div>
</form>
</section>
</section>

View file

@ -1,66 +1,58 @@
<section id="main"> <div class="page-header">
<div class="page-header"> <h2><?= t('Edit the board for "%s"', $project['name']) ?></h2>
<h2><?= t('Edit the board for "%s"', $project['name']) ?></h2> </div>
<ul> <section>
<li><a href="?controller=project"><?= t('All projects') ?></a></li>
</ul> <h3><?= t('Change columns') ?></h3>
<form method="post" action="?controller=board&amp;action=update&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?php $i = 0; ?>
<table>
<tr>
<th><?= t('Position') ?></th>
<th><?= t('Column title') ?></th>
<th><?= t('Task limit') ?></th>
<th><?= t('Actions') ?></th>
</tr>
<?php foreach ($columns as $column): ?>
<tr>
<td><?= Helper\form_label(t('Column %d', ++$i), 'title['.$column['id'].']', array('title="column_id='.$column['id'].'"')) ?></td>
<td><?= Helper\form_text('title['.$column['id'].']', $values, $errors, array('required')) ?></td>
<td><?= Helper\form_number('task_limit['.$column['id'].']', $values, $errors, array('placeholder="'.t('limit').'"')) ?></td>
<td>
<ul>
<?php if ($column['position'] != 1): ?>
<li>
<a href="?controller=board&amp;action=moveUp&amp;project_id=<?= $project['id'] ?>&amp;column_id=<?= $column['id'].Helper\param_csrf() ?>"><?= t('Move Up') ?></a>
</li>
<?php endif ?>
<?php if ($column['position'] != count($columns)): ?>
<li>
<a href="?controller=board&amp;action=moveDown&amp;project_id=<?= $project['id'] ?>&amp;column_id=<?= $column['id'].Helper\param_csrf() ?>"><?= t('Move Down') ?></a>
</li>
<?php endif ?>
<li>
<a href="?controller=board&amp;action=confirm&amp;project_id=<?= $project['id'] ?>&amp;column_id=<?= $column['id'] ?>"><?= t('Remove') ?></a>
</li>
</ul>
</td>
</tr>
<?php endforeach ?>
</table>
<div class="form-actions">
<input type="submit" value="<?= t('Update') ?>" class="btn btn-blue"/>
</div> </div>
<section> </form>
<h3><?= t('Change columns') ?></h3> <h3><?= t('Add a new column') ?></h3>
<form method="post" action="?controller=board&amp;action=update&amp;project_id=<?= $project['id'] ?>" autocomplete="off"> <form method="post" action="?controller=board&amp;action=add&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?> <?= Helper\form_csrf() ?>
<?php $i = 0; ?> <?= Helper\form_hidden('project_id', $values) ?>
<table> <?= Helper\form_label(t('Title'), 'title') ?>
<tr> <?= Helper\form_text('title', $values, $errors, array('required')) ?>
<th><?= t('Position') ?></th>
<th><?= t('Column title') ?></th>
<th><?= t('Task limit') ?></th>
<th><?= t('Actions') ?></th>
</tr>
<?php foreach ($columns as $column): ?>
<tr>
<td><?= Helper\form_label(t('Column %d', ++$i), 'title['.$column['id'].']', array('title="column_id='.$column['id'].'"')) ?></td>
<td><?= Helper\form_text('title['.$column['id'].']', $values, $errors, array('required')) ?></td>
<td><?= Helper\form_number('task_limit['.$column['id'].']', $values, $errors, array('placeholder="'.t('limit').'"')) ?></td>
<td>
<ul>
<?php if ($column['position'] != 1): ?>
<li>
<a href="?controller=board&amp;action=moveUp&amp;project_id=<?= $project['id'] ?>&amp;column_id=<?= $column['id'].Helper\param_csrf() ?>"><?= t('Move Up') ?></a>
</li>
<?php endif ?>
<?php if ($column['position'] != count($columns)): ?>
<li>
<a href="?controller=board&amp;action=moveDown&amp;project_id=<?= $project['id'] ?>&amp;column_id=<?= $column['id'].Helper\param_csrf() ?>"><?= t('Move Down') ?></a>
</li>
<?php endif ?>
<li>
<a href="?controller=board&amp;action=confirm&amp;project_id=<?= $project['id'] ?>&amp;column_id=<?= $column['id'] ?>"><?= t('Remove') ?></a>
</li>
</ul>
</td>
</tr>
<?php endforeach ?>
</table>
<div class="form-actions"> <div class="form-actions">
<input type="submit" value="<?= t('Update') ?>" class="btn btn-blue"/> <input type="submit" value="<?= t('Add this column') ?>" class="btn btn-blue"/>
<?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a> </div>
</div> </form>
</form>
<h3><?= t('Add a new column') ?></h3>
<form method="post" action="?controller=board&amp;action=add&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_label(t('Title'), 'title') ?>
<?= Helper\form_text('title', $values, $errors, array('required')) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Add this column') ?>" class="btn btn-blue"/>
<?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a>
</div>
</form>
</section>
</section>

View file

@ -19,6 +19,7 @@
<li><a href="#" id="filter-due-date"><?= t('Filter by due date') ?></a></li> <li><a href="#" id="filter-due-date"><?= t('Filter by due date') ?></a></li>
<li><a href="?controller=project&amp;action=search&amp;project_id=<?= $current_project_id ?>"><?= t('Search') ?></a></li> <li><a href="?controller=project&amp;action=search&amp;project_id=<?= $current_project_id ?>"><?= t('Search') ?></a></li>
<li><a href="?controller=project&amp;action=tasks&amp;project_id=<?= $current_project_id ?>"><?= t('Completed tasks') ?></a></li> <li><a href="?controller=project&amp;action=tasks&amp;project_id=<?= $current_project_id ?>"><?= t('Completed tasks') ?></a></li>
<li><a href="?controller=project&amp;action=activity&amp;project_id=<?= $current_project_id ?>"><?= t('Activity') ?></a></li>
</ul> </ul>
</div> </div>

View file

@ -21,7 +21,7 @@
<?php foreach ($column['tasks'] as $task): ?> <?php foreach ($column['tasks'] as $task): ?>
<div class="task-board task-<?= $task['color_id'] ?>"> <div class="task-board task-<?= $task['color_id'] ?>">
<?= Helper\template('board_task', array('task' => $task, 'categories' => $categories, 'not_editable' => true)) ?> <?= Helper\template('board_task', array('task' => $task, 'categories' => $categories, 'not_editable' => true, 'project' => $project)) ?>
</div> </div>
<?php endforeach ?> <?php endforeach ?>

View file

@ -1,17 +1,15 @@
<section id="main"> <div class="page-header">
<div class="page-header"> <h2><?= t('Remove a column') ?></h2>
<h2><?= t('Remove a column') ?></h2> </div>
</div>
<div class="confirm"> <div class="confirm">
<p class="alert alert-info"> <p class="alert alert-info">
<?= t('Do you really want to remove this column: "%s"?', $column['title']) ?> <?= t('Do you really want to remove this column: "%s"?', $column['title']) ?>
<?= t('This action will REMOVE ALL TASKS associated to this column!') ?> <?= t('This action will REMOVE ALL TASKS associated to this column!') ?>
</p> </p>
<div class="form-actions"> <div class="form-actions">
<a href="?controller=board&amp;action=remove&amp;column_id=<?= $column['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> <a href="?controller=board&amp;action=remove&amp;column_id=<?= $column['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=board&amp;action=edit&amp;project_id=<?= $column['project_id'] ?>"><?= t('cancel') ?></a> <?= t('or') ?> <a href="?controller=board&amp;action=edit&amp;project_id=<?= $column['project_id'] ?>"><?= t('cancel') ?></a>
</div>
</div> </div>
</section> </div>

View file

@ -32,7 +32,7 @@
data-task-limit="<?= $column['task_limit'] ?>" data-task-limit="<?= $column['task_limit'] ?>"
> >
<?php foreach ($column['tasks'] as $task): ?> <?php foreach ($column['tasks'] as $task): ?>
<div class="task-board draggable-item task-<?= $task['color_id'] ?>" <div class="task-board draggable-item task-<?= $task['color_id'] ?> <?= $task['date_modification'] > time() - RECENT_TASK_PERIOD ? 'task-board-recent' : '' ?>"
data-task-id="<?= $task['id'] ?>" data-task-id="<?= $task['id'] ?>"
data-owner-id="<?= $task['owner_id'] ?>" data-owner-id="<?= $task['owner_id'] ?>"
data-category-id="<?= $task['category_id'] ?>" data-category-id="<?= $task['category_id'] ?>"

View file

@ -1,10 +1,10 @@
<?php if (isset($not_editable)): ?> <?php if (isset($not_editable)): ?>
#<?= $task['id'] ?> - <a href="?controller=task&amp;action=readonly&amp;task_id=<?= $task['id'] ?>&amp;token=<?= $project['token'] ?>">#<?= $task['id'] ?></a> -
<span class="task-board-user"> <span class="task-board-user">
<?php if (! empty($task['owner_id'])): ?> <?php if (! empty($task['owner_id'])): ?>
<?= t('Assigned to %s', $task['username']) ?> <?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?>
<?php else: ?> <?php else: ?>
<span class="task-board-nobody"><?= t('Nobody assigned') ?></span> <span class="task-board-nobody"><?= t('Nobody assigned') ?></span>
<?php endif ?> <?php endif ?>
@ -15,7 +15,9 @@
<?php endif ?> <?php endif ?>
<div class="task-board-title"> <div class="task-board-title">
<?= Helper\escape($task['title']) ?> <a href="?controller=task&amp;action=readonly&amp;task_id=<?= $task['id'] ?>&amp;token=<?= $project['token'] ?>">
<?= Helper\escape($task['title']) ?>
</a>
</div> </div>
<?php else: ?> <?php else: ?>
@ -23,11 +25,13 @@
<a class="task-edit-popover" href="?controller=task&amp;action=edit&amp;task_id=<?= $task['id'] ?>" title="<?= t('Edit this task') ?>">#<?= $task['id'] ?></a> - <a class="task-edit-popover" href="?controller=task&amp;action=edit&amp;task_id=<?= $task['id'] ?>" title="<?= t('Edit this task') ?>">#<?= $task['id'] ?></a> -
<span class="task-board-user"> <span class="task-board-user">
<?php if (! empty($task['owner_id'])): ?> <a class="assignee-popover" href="?controller=board&amp;action=changeAssignee&amp;task_id=<?= $task['id'] ?>" title="<?= t('Change assignee') ?>">
<a class="assignee-popover" href="?controller=board&amp;action=assign&amp;task_id=<?= $task['id'] ?>" title="<?= t('Change assignee') ?>"><?= t('Assigned to %s', $task['username']) ?></a> <?php if (! empty($task['owner_id'])): ?>
<?php else: ?> <?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?></a>
<a class="assignee-popover" href="?controller=board&amp;action=assign&amp;task_id=<?= $task['id'] ?>" title="<?= t('Change assignee') ?>" class="task-board-nobody"><?= t('Nobody assigned') ?></a> <?php else: ?>
<?php endif ?> <?= t('Nobody assigned') ?>
<?php endif ?>
</a>
</span> </span>
<?php if ($task['score']): ?> <?php if ($task['score']): ?>
@ -44,32 +48,45 @@
<?php if ($task['category_id']): ?> <?php if ($task['category_id']): ?>
<div class="task-board-category-container"> <div class="task-board-category-container">
<span class="task-board-category"> <span class="task-board-category">
<?= Helper\in_list($task['category_id'], $categories) ?> <a class="category-popover" href="?controller=board&amp;action=changeCategory&amp;task_id=<?= $task['id'] ?>" title="<?= t('Change category') ?>">
<?= Helper\in_list($task['category_id'], $categories) ?>
</a>
</span> </span>
</div> </div>
<?php endif ?> <?php endif ?>
<?php if (! empty($task['date_due']) || ! empty($task['nb_files']) || ! empty($task['nb_comments']) || ! empty($task['description'])): ?> <?php if (! empty($task['date_due']) || ! empty($task['nb_files']) || ! empty($task['nb_comments']) || ! empty($task['description']) || ! empty($task['nb_subtasks'])): ?>
<div class="task-board-footer"> <div class="task-board-footer">
<?php if (! empty($task['date_due'])): ?> <?php if (! empty($task['date_due'])): ?>
<div class="task-board-date"> <div class="task-board-date <?= time() > $task['date_due'] ? 'task-board-date-overdue' : '' ?>">
<?= dt('%B %e, %G', $task['date_due']) ?> <?= dt('%B %e, %Y', $task['date_due']) ?>
</div> </div>
<?php endif ?> <?php endif ?>
<div class="task-board-icons"> <div class="task-board-icons">
<?php if (! empty($task['nb_subtasks'])): ?>
<span title="<?= t('Sub-Tasks') ?>"><?= $task['nb_completed_subtasks'].'/'.$task['nb_subtasks'] ?> <i class="fa fa-bars"></i></span>
<?php endif ?>
<?php if (! empty($task['nb_files'])): ?> <?php if (! empty($task['nb_files'])): ?>
<?= $task['nb_files'] ?> <i class="fa fa-paperclip" title="<?= t('Attachments') ?>"></i> <span title="<?= t('Attachments') ?>"><?= $task['nb_files'] ?> <i class="fa fa-paperclip"></i></span>
<?php endif ?> <?php endif ?>
<?php if (! empty($task['nb_comments'])): ?> <?php if (! empty($task['nb_comments'])): ?>
<?= $task['nb_comments'] ?> <i class="fa fa-comment-o" title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>"></i> <span title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>"><?= $task['nb_comments'] ?> <i class="fa fa-comment-o"></i></span>
<?php endif ?> <?php endif ?>
<?php if (! empty($task['description'])): ?> <?php if (! empty($task['description'])): ?>
<a class="task-board-popover" href='?controller=task&amp;action=editDescription&amp;task_id=<?= $task['id'] ?>'><i class="fa fa-file-text-o" title="<?= t('Description') ?>"></i></a> <span title="<?= t('Description') ?>">
<?php if (! isset($not_editable)): ?>
<a class="task-description-popover" href="?controller=task&amp;action=description&amp;task_id=<?= $task['id'] ?>"><i class="fa fa-file-text-o" data-href="?controller=task&amp;action=description&amp;task_id=<?= $task['id'] ?>"></i></a>
<?php else: ?>
<i class="fa fa-file-text-o"></i>
<?php endif ?>
</span>
<?php endif ?> <?php endif ?>
</div> </div>
</div> </div>

View file

@ -1,24 +1,16 @@
<section id="main"> <div class="page-header">
<div class="page-header"> <h2><?= t('Category modification for the project "%s"', $project['name']) ?></h2>
<h2><?= t('Category modification for the project "%s"', $project['name']) ?></h2> </div>
<ul>
<li><a href="?controller=project"><?= t('All projects') ?></a></li> <form method="post" action="?controller=category&amp;action=update&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
</ul> <?= Helper\form_csrf() ?>
<?= Helper\form_hidden('id', $values) ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_label(t('Category Name'), 'name') ?>
<?= Helper\form_text('name', $values, $errors, array('autofocus required')) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div> </div>
<section> </form>
<form method="post" action="?controller=category&amp;action=update&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('id', $values) ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_label(t('Category Name'), 'name') ?>
<?= Helper\form_text('name', $values, $errors, array('required')) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div>
</form>
</section>
</section>

View file

@ -1,49 +1,41 @@
<section id="main"> <div class="page-header">
<div class="page-header"> <h2><?= t('Categories') ?></h2>
<h2><?= t('Categories for the project "%s"', $project['name']) ?></h2> </div>
<ul>
<li><a href="?controller=project"><?= t('All projects') ?></a></li> <?php if (! empty($categories)): ?>
</ul> <table>
<tr>
<th><?= t('Category Name') ?></th>
<th><?= t('Actions') ?></th>
</tr>
<?php foreach ($categories as $category_id => $category_name): ?>
<tr>
<td><?= Helper\escape($category_name) ?></td>
<td>
<ul>
<li>
<a href="?controller=category&amp;action=edit&amp;project_id=<?= $project['id'] ?>&amp;category_id=<?= $category_id ?>"><?= t('Edit') ?></a>
</li>
<li>
<a href="?controller=category&amp;action=confirm&amp;project_id=<?= $project['id'] ?>&amp;category_id=<?= $category_id ?>"><?= t('Remove') ?></a>
</li>
</ul>
</td>
</tr>
<?php endforeach ?>
</table>
<?php endif ?>
<h3><?= t('Add a new category') ?></h3>
<form method="post" action="?controller=category&amp;action=save&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_label(t('Category Name'), 'name') ?>
<?= Helper\form_text('name', $values, $errors, array('autofocus required')) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div> </div>
<section> </form>
<?php if (! empty($categories)): ?>
<table>
<tr>
<th><?= t('Category Name') ?></th>
<th><?= t('Actions') ?></th>
</tr>
<?php foreach ($categories as $category_id => $category_name): ?>
<tr>
<td><?= Helper\escape($category_name) ?></td>
<td>
<ul>
<li>
<a href="?controller=category&amp;action=edit&amp;project_id=<?= $project['id'] ?>&amp;category_id=<?= $category_id ?>"><?= t('Edit') ?></a>
</li>
<li>
<a href="?controller=category&amp;action=confirm&amp;project_id=<?= $project['id'] ?>&amp;category_id=<?= $category_id ?>"><?= t('Remove') ?></a>
</li>
</ul>
</td>
</tr>
<?php endforeach ?>
</table>
<?php endif ?>
<h3><?= t('Add a new category') ?></h3>
<form method="post" action="?controller=category&amp;action=save&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_label(t('Category Name'), 'name') ?>
<?= Helper\form_text('name', $values, $errors, array('required')) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div>
</form>
</section>
</section>

View file

@ -6,6 +6,7 @@
<?= Helper\form_csrf() ?> <?= Helper\form_csrf() ?>
<?= Helper\form_hidden('id', $values) ?> <?= Helper\form_hidden('id', $values) ?>
<?= Helper\form_hidden('task_id', $values) ?>
<?= Helper\form_textarea('comment', $values, $errors, array('autofocus', 'required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?><br/> <?= Helper\form_textarea('comment', $values, $errors, array('autofocus', 'required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?><br/>
<div class="form-actions"> <div class="form-actions">

View file

@ -1,7 +1,7 @@
<div class="comment <?= isset($preview) ? 'comment-preview' : '' ?>" id="comment-<?= $comment['id'] ?>"> <div class="comment <?= isset($preview) ? 'comment-preview' : '' ?>" id="comment-<?= $comment['id'] ?>">
<p class="comment-title"> <p class="comment-title">
<span class="comment-username"><?= Helper\escape($comment['username']) ?></span> @ <span class="comment-date"><?= dt('%B %e, %G at %k:%M %p', $comment['date']) ?></span> <span class="comment-username"><?= Helper\escape($comment['name'] ?: $comment['username']) ?></span> @ <span class="comment-date"><?= dt('%B %e, %Y at %k:%M %p', $comment['date']) ?></span>
</p> </p>
<div class="comment-inner"> <div class="comment-inner">
@ -9,7 +9,7 @@
<?php if (! isset($preview)): ?> <?php if (! isset($preview)): ?>
<ul class="comment-actions"> <ul class="comment-actions">
<li><a href="#comment-<?= $comment['id'] ?>"><?= t('link') ?></a></li> <li><a href="#comment-<?= $comment['id'] ?>"><?= t('link') ?></a></li>
<?php if (Helper\is_admin() || Helper\is_current_user($comment['user_id'])): ?> <?php if ((! isset($not_editable) || ! $not_editable) && (Helper\is_admin() || Helper\is_current_user($comment['user_id']))): ?>
<li> <li>
<a href="?controller=comment&amp;action=confirm&amp;task_id=<?= $task['id'] ?>&amp;comment_id=<?= $comment['id'] ?>"><?= t('remove') ?></a> <a href="?controller=comment&amp;action=confirm&amp;task_id=<?= $task['id'] ?>&amp;comment_id=<?= $comment['id'] ?>"><?= t('remove') ?></a>
</li> </li>

View file

@ -1,126 +1,71 @@
<section id="main"> <section id="main">
<?php if ($user['is_admin']): ?> <div class="page-header">
<div class="page-header"> <h2><?= t('Application settings') ?></h2>
<h2><?= t('Application settings') ?></h2> </div>
<section>
<form method="post" action="?controller=config&amp;action=save" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_label(t('Language'), 'language') ?>
<?= Helper\form_select('language', $languages, $values, $errors) ?><br/>
<?= Helper\form_label(t('Timezone'), 'timezone') ?>
<?= Helper\form_select('timezone', $timezones, $values, $errors) ?><br/>
<?= Helper\form_label(t('Webhook URL for task creation'), 'webhooks_url_task_creation') ?>
<?= Helper\form_text('webhooks_url_task_creation', $values, $errors) ?><br/>
<?= Helper\form_label(t('Webhook URL for task modification'), 'webhooks_url_task_modification') ?>
<?= Helper\form_text('webhooks_url_task_modification', $values, $errors) ?><br/>
<?= Helper\form_label(t('Default columns for new projects (Comma-separated)'), 'default_columns') ?>
<?= Helper\form_text('default_columns', $values, $errors) ?><br/>
<p class="form-help"><?= t('Default values are "%s"', $default_columns) ?></p>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div> </div>
<section> </form>
<form method="post" action="?controller=config&amp;action=save" autocomplete="off"> </section>
<?= Helper\form_csrf() ?>
<?= Helper\form_label(t('Language'), 'language') ?>
<?= Helper\form_select('language', $languages, $values, $errors) ?><br/>
<?= Helper\form_label(t('Timezone'), 'timezone') ?>
<?= Helper\form_select('timezone', $timezones, $values, $errors) ?><br/>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div>
</form>
</section>
<?php endif ?>
<div class="page-header"> <div class="page-header">
<h2><?= t('User settings') ?></h2> <h2><?= t('More information') ?></h2>
</div> </div>
<section class="settings"> <section class="settings">
<ul> <ul>
<li><a href="?controller=config&amp;action=tokens<?= Helper\param_csrf() ?>"><?= t('Reset all tokens') ?></a></li>
<li> <li>
<strong><?= t('My default project:') ?> </strong> <?= t('Webhooks token:') ?>
<?= (isset($user['default_project_id']) && isset($projects[$user['default_project_id']])) ? Helper\escape($projects[$user['default_project_id']]) : t('None') ?>, <strong><?= Helper\escape($values['webhooks_token']) ?></strong>
<a href="?controller=user&amp;action=edit&amp;user_id=<?= $user['id'] ?>"><?= t('edit') ?></a> </li>
<li>
<?= t('API token:') ?>
<strong><?= Helper\escape($values['api_token']) ?></strong>
</li>
<?php if (DB_DRIVER === 'sqlite'): ?>
<li>
<?= t('Database size:') ?>
<strong><?= Helper\format_bytes($db_size) ?></strong>
</li>
<li>
<a href="?controller=config&amp;action=downloadDb<?= Helper\param_csrf() ?>"><?= t('Download the database') ?></a>
<?= t('(Gzip compressed Sqlite file)') ?>
</li>
<li>
<a href="?controller=config&amp;action=optimizeDb <?= Helper\param_csrf() ?>"><?= t('Optimize the database') ?></a>
<?= t('(VACUUM command)') ?>
</li>
<?php endif ?>
<li>
<?= t('Official website:') ?>
<a href="http://kanboard.net/" target="_blank" rel="noreferer">http://kanboard.net/</a>
</li>
<li>
<?= t('Application version:') ?>
<?= APP_VERSION ?>
</li> </li>
</ul> </ul>
</section> </section>
<?php if ($user['is_admin']): ?>
<div class="page-header">
<h2><?= t('More information') ?></h2>
</div>
<section class="settings">
<ul>
<li><a href="?controller=config&amp;action=tokens<?= Helper\param_csrf() ?>"><?= t('Reset all tokens') ?></a></li>
<li>
<?= t('Webhooks token:') ?>
<strong><?= Helper\escape($values['webhooks_token']) ?></strong>
</li>
<li>
<?= t('API token:') ?>
<strong><?= Helper\escape($values['api_token']) ?></strong>
</li>
<?php if (DB_DRIVER === 'sqlite'): ?>
<li>
<?= t('Database size:') ?>
<strong><?= Helper\format_bytes($db_size) ?></strong>
</li>
<li>
<a href="?controller=config&amp;action=downloadDb<?= Helper\param_csrf() ?>"><?= t('Download the database') ?></a>
<?= t('(Gzip compressed Sqlite file)') ?>
</li>
<li>
<a href="?controller=config&amp;action=optimizeDb <?= Helper\param_csrf() ?>"><?= t('Optimize the database') ?></a>
<?= t('(VACUUM command)') ?>
</li>
<?php endif ?>
<li>
<?= t('Official website:') ?>
<a href="http://kanboard.net/" target="_blank" rel="noreferer">http://kanboard.net/</a>
</li>
<li>
<?= t('Application version:') ?>
<?= APP_VERSION ?>
</li>
</ul>
</section>
<?php endif ?>
<div class="page-header" id="last-logins">
<h2><?= t('Last logins') ?></h2>
</div>
<?php if (! empty($last_logins)): ?>
<table class="table-small table-hover">
<tr>
<th><?= t('Login date') ?></th>
<th><?= t('Authentication method') ?></th>
<th><?= t('IP address') ?></th>
<th><?= t('User agent') ?></th>
</tr>
<?php foreach($last_logins as $login): ?>
<tr>
<td><?= dt('%B %e, %G at %k:%M %p', $login['date_creation']) ?></td>
<td><?= Helper\escape($login['auth_type']) ?></td>
<td><?= Helper\escape($login['ip']) ?></td>
<td><?= Helper\escape($login['user_agent']) ?></td>
</tr>
<?php endforeach ?>
</table>
<?php endif ?>
<div class="page-header" id="remember-me">
<h2><?= t('Persistent connections') ?></h2>
</div>
<?php if (empty($remember_me_sessions)): ?>
<p class="alert alert-info"><?= t('No session') ?></p>
<?php else: ?>
<table class="table-small table-hover">
<tr>
<th><?= t('Creation date') ?></th>
<th><?= t('Expiration date') ?></th>
<th><?= t('IP address') ?></th>
<th><?= t('User agent') ?></th>
<th><?= t('Action') ?></th>
</tr>
<?php foreach($remember_me_sessions as $session): ?>
<tr>
<td><?= dt('%B %e, %G at %k:%M %p', $session['date_creation']) ?></td>
<td><?= dt('%B %e, %G at %k:%M %p', $session['expiration']) ?></td>
<td><?= Helper\escape($session['ip']) ?></td>
<td><?= Helper\escape($session['user_agent']) ?></td>
<td><a href="?controller=config&amp;action=removeRememberMeToken&amp;id=<?= $session['id'].Helper\param_csrf() ?>"><?= t('Remove') ?></a></td>
</tr>
<?php endforeach ?>
</table>
<?php endif ?>
</section> </section>

View file

@ -0,0 +1,7 @@
<p class="activity-title">
<?= e('%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?>
</p>
<p class="activity-description">
<em><?= Helper\escape($task_title) ?></em><br/>
<div class="markdown"><?= Helper\parse($comment) ?></div>
</p>

View file

@ -0,0 +1,7 @@
<p class="activity-title">
<?= e('%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?>
</p>
<p class="activity-description">
<em><?= Helper\escape($task_title) ?></em><br/>
<div class="markdown"><?= Helper\parse($comment) ?></div>
</p>

View file

@ -0,0 +1,12 @@
<p class="activity-title">
<?= e('%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?>
</p>
<p class="activity-description">
<em><?= Helper\escape($task_title) ?></em><br/>
<p><?= Helper\escape($subtask_title) ?> <strong>(<?= Helper\in_list($subtask_status, $subtask_status_list) ?>)</strong></p>
<?php if ($subtask_assignee): ?>
<p><?= t('Assigned to %s with an estimate of %s/%sh', $subtask_assignee, $subtask_time_spent, $subtask_time_estimated) ?></p>
<?php else: ?>
<p><?= t('Not assigned, estimate of %sh', $subtask_time_estimated) ?></p>
<?php endif ?>
</p>

View file

@ -0,0 +1,12 @@
<p class="activity-title">
<?= e('%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?>
</p>
<p class="activity-description">
<em><?= Helper\escape($task_title) ?></em><br/>
<p><?= Helper\escape($subtask_title) ?> <strong>(<?= Helper\in_list($subtask_status, $subtask_status_list) ?>)</strong></p>
<?php if ($subtask_assignee): ?>
<p><?= t('Assigned to %s with an estimate of %s/%sh', $subtask_assignee, $subtask_time_spent, $subtask_time_estimated) ?></p>
<?php else: ?>
<p><?= t('Not assigned, estimate of %sh', $subtask_time_estimated) ?></p>
<?php endif ?>
</p>

Some files were not shown because too many files have changed in this diff Show more