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.10

This commit is contained in:
mbugeia 2014-12-22 19:15:38 +01:00
parent 2af3292971
commit 6250d201a8
1431 changed files with 151190 additions and 4870 deletions

View file

@ -2,8 +2,8 @@
namespace Action;
use Pimple\Container;
use Core\Listener;
use Core\Registry;
use Core\Tool;
/**
@ -15,7 +15,11 @@ use Core\Tool;
* @property \Model\Acl $acl
* @property \Model\Comment $comment
* @property \Model\Task $task
* @property \Model\TaskCreation $taskCreation
* @property \Model\TaskModification $taskModification
* @property \Model\TaskDuplication $taskDuplication
* @property \Model\TaskFinder $taskFinder
* @property \Model\TaskStatus $taskStatus
*/
abstract class Base implements Listener
{
@ -44,12 +48,12 @@ abstract class Base implements Listener
protected $event_name = '';
/**
* Registry instance
* Container instance
*
* @access protected
* @var \Core\Registry
* @var \Pimple\Container
*/
protected $registry;
protected $container;
/**
* Execute the action
@ -101,13 +105,13 @@ abstract class Base implements Listener
* Constructor
*
* @access public
* @param \Core\Registry $registry Regsitry instance
* @param integer $project_id Project id
* @param string $event_name Attached event name
* @param \Pimple\Container $container Container
* @param integer $project_id Project id
* @param string $event_name Attached event name
*/
public function __construct(Registry $registry, $project_id, $event_name)
public function __construct(Container $container, $project_id, $event_name)
{
$this->registry = $registry;
$this->container = $container;
$this->project_id = $project_id;
$this->event_name = $event_name;
}
@ -132,7 +136,7 @@ abstract class Base implements Listener
*/
public function __get($name)
{
return Tool::loadModel($this->registry, $name);
return Tool::loadModel($this->container, $name);
}
/**

View file

@ -61,7 +61,7 @@ class CommentCreation extends Base
*/
public function doAction(array $data)
{
return $this->comment->create(array(
return (bool) $this->comment->create(array(
'reference' => $data['reference'],
'comment' => $data['comment'],
'task_id' => $data['task_id'],

View file

@ -67,7 +67,7 @@ class TaskAssignCategoryColor extends Base
'category_id' => $this->getParam('category_id'),
);
return $this->task->update($values, false);
return $this->taskModification->update($values, false);
}
/**

View file

@ -67,7 +67,7 @@ class TaskAssignCategoryLabel extends Base
'category_id' => isset($data['category_id']) ? $data['category_id'] : $this->getParam('category_id'),
);
return $this->task->update($values, false);
return $this->taskModification->update($values, false);
}
/**

View file

@ -67,7 +67,7 @@ class TaskAssignColorCategory extends Base
'color_id' => $this->getParam('color_id'),
);
return $this->task->update($values, false);
return $this->taskModification->update($values, false);
}
/**

View file

@ -68,7 +68,7 @@ class TaskAssignColorUser extends Base
'color_id' => $this->getParam('color_id'),
);
return $this->task->update($values, false);
return $this->taskModification->update($values, false);
}
/**

View file

@ -67,7 +67,7 @@ class TaskAssignCurrentUser extends Base
'owner_id' => $this->acl->getUserId(),
);
return $this->task->update($values, false);
return $this->taskModification->update($values, false);
}
/**

View file

@ -68,7 +68,7 @@ class TaskAssignSpecificUser extends Base
'owner_id' => $this->getParam('user_id'),
);
return $this->task->update($values, false);
return $this->taskModification->update($values, false);
}
/**

View file

@ -64,7 +64,7 @@ class TaskAssignUser extends Base
'owner_id' => $data['owner_id'],
);
return $this->task->update($values, false);
return $this->taskModification->update($values, false);
}
/**

View file

@ -71,7 +71,7 @@ class TaskClose extends Base
*/
public function doAction(array $data)
{
return $this->task->close($data['task_id']);
return $this->taskStatus->close($data['task_id']);
}
/**

View file

@ -59,7 +59,7 @@ class TaskCreation extends Base
*/
public function doAction(array $data)
{
return $this->task->create(array(
return (bool) $this->taskCreation->create(array(
'project_id' => $data['project_id'],
'title' => $data['title'],
'reference' => $data['reference'],

View file

@ -64,9 +64,7 @@ class TaskDuplicateAnotherProject extends Base
*/
public function doAction(array $data)
{
$task = $this->taskFinder->getById($data['task_id']);
$this->task->duplicateToAnotherProject($this->getParam('project_id'), $task);
return true;
return (bool) $this->taskDuplication->duplicateToProject($data['task_id'], $this->getParam('project_id'));
}
/**

View file

@ -64,9 +64,7 @@ class TaskMoveAnotherProject extends Base
*/
public function doAction(array $data)
{
$task = $this->taskFinder->getById($data['task_id']);
$this->task->moveToAnotherProject($this->getParam('project_id'), $task);
return true;
return $this->taskDuplication->moveToProject($data['task_id'], $this->getParam('project_id'));
}
/**

View file

@ -56,7 +56,7 @@ class TaskOpen extends Base
*/
public function doAction(array $data)
{
return $this->task->open($data['task_id']);
return $this->taskStatus->open($data['task_id']);
}
/**

View file

@ -3,7 +3,7 @@
namespace Auth;
use Core\Tool;
use Core\Registry;
use Pimple\Container;
/**
* Base auth class
@ -26,34 +26,34 @@ abstract class Base
protected $db;
/**
* Registry instance
* Container instance
*
* @access protected
* @var \Core\Registry
* @var \Pimple\Container
*/
protected $registry;
protected $container;
/**
* Constructor
*
* @access public
* @param \Core\Registry $registry Registry instance
* @param \Pimple\Container $container
*/
public function __construct(Registry $registry)
public function __construct(Container $container)
{
$this->registry = $registry;
$this->db = $this->registry->shared('db');
$this->container = $container;
$this->db = $this->container['db'];
}
/**
* Load automatically models
*
* @access public
* @param string $name Model name
* @param string $name Model name
* @return mixed
*/
public function __get($name)
{
return Tool::loadModel($this->registry, $name);
return Tool::loadModel($this->container, $name);
}
}

View file

@ -2,8 +2,6 @@
namespace Auth;
require __DIR__.'/../../vendor/OAuth/bootstrap.php';
use Core\Request;
use OAuth\Common\Storage\Session;
use OAuth\Common\Consumer\Credentials;

View file

@ -2,8 +2,6 @@
namespace Auth;
require __DIR__.'/../../vendor/OAuth/bootstrap.php';
use Core\Request;
use OAuth\Common\Storage\Session;
use OAuth\Common\Consumer\Credentials;

View file

@ -104,7 +104,7 @@ class Ldap extends Base
{
$ldap = $this->connect();
if ($this->bind($ldap, $username, $password)) {
if (is_resource($ldap) && $this->bind($ldap, $username, $password)) {
return $this->search($ldap, $username, $password);
}
@ -136,6 +136,12 @@ class Ldap extends Base
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
ldap_set_option($ldap, LDAP_OPT_NETWORK_TIMEOUT, 1);
ldap_set_option($ldap, LDAP_OPT_TIMELIMIT, 1);
if (LDAP_START_TLS && ! @ldap_start_tls($ldap)) {
die('Unable to use ldap_start_tls()');
}
return $ldap;
}

View file

@ -4,7 +4,6 @@ namespace Auth;
use Core\Request;
use Core\Security;
use Core\Tool;
/**
* RememberMe model
@ -96,7 +95,7 @@ class RememberMe extends Base
// Update the sequence
$this->writeCookie(
$record['token'],
$this->update($record['token'], $record['sequence']),
$this->update($record['token']),
$record['expiration']
);
@ -137,7 +136,7 @@ class RememberMe extends Base
// Update the sequence
$this->writeCookie(
$record['token'],
$this->update($record['token'], $record['sequence']),
$this->update($record['token']),
$record['expiration']
);
}
@ -238,17 +237,15 @@ class RememberMe extends Base
*
* @access public
* @param string $token Session token
* @param string $sequence Sequence token
* @return string
*/
public function update($token, $sequence)
public function update($token)
{
$new_sequence = Security::generateToken();
$this->db
->table(self::TABLE)
->eq('token', $token)
->eq('sequence', $sequence)
->update(array('sequence' => $new_sequence));
return $new_sequence;
@ -311,7 +308,7 @@ class RememberMe extends Base
$expiration,
BASE_URL_DIRECTORY,
null,
Tool::isHTTPS(),
Request::isHTTPS(),
true
);
}
@ -344,7 +341,7 @@ class RememberMe extends Base
time() - 3600,
BASE_URL_DIRECTORY,
null,
Tool::isHTTPS(),
Request::isHTTPS(),
true
);
}

View file

@ -0,0 +1,57 @@
<?php
namespace Console;
use Core\Tool;
use Pimple\Container;
use Symfony\Component\Console\Command\Command;
/**
* Base command class
*
* @package console
* @author Frederic Guillot
*
* @property \Model\Notification $notification
* @property \Model\Project $project
* @property \Model\ProjectPermission $projectPermission
* @property \Model\ProjectAnalytic $projectAnalytic
* @property \Model\ProjectDailySummary $projectDailySummary
* @property \Model\Task $task
* @property \Model\TaskExport $taskExport
* @property \Model\TaskFinder $taskFinder
*/
abstract class Base extends Command
{
/**
* Container instance
*
* @access protected
* @var \Pimple\Container
*/
protected $container;
/**
* Constructor
*
* @access public
* @param \Pimple\Container $container
*/
public function __construct(Container $container)
{
parent::__construct();
$this->container = $container;
}
/**
* Load automatically models
*
* @access public
* @param string $name Model name
* @return mixed
*/
public function __get($name)
{
return Tool::loadModel($this->container, $name);
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Console;
use Model\Project;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class ProjectDailySummaryCalculation extends Base
{
protected function configure()
{
$this
->setName('projects:daily-summary')
->setDescription('Calculate daily summary data for all projects');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$projects = $this->project->getAllByStatus(Project::ACTIVE);
foreach ($projects as $project) {
$output->writeln('Run calculation for '.$project['name']);
$this->projectDailySummary->updateTotals($project['id'], date('Y-m-d'));
}
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Console;
use Core\Tool;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class ProjectDailySummaryExport extends Base
{
protected function configure()
{
$this
->setName('export:daily-project-summary')
->setDescription('Daily project summary CSV export (number of tasks per column and per day)')
->addArgument('project_id', InputArgument::REQUIRED, 'Project id')
->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)')
->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$data = $this->projectDailySummary->getAggregatedMetrics(
$input->getArgument('project_id'),
$input->getArgument('start_date'),
$input->getArgument('end_date')
);
if (is_array($data)) {
Tool::csv($data);
}
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Console;
use Core\Tool;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class TaskExport extends Base
{
protected function configure()
{
$this
->setName('export:tasks')
->setDescription('Tasks CSV export')
->addArgument('project_id', InputArgument::REQUIRED, 'Project id')
->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)')
->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$data = $this->taskExport->export(
$input->getArgument('project_id'),
$input->getArgument('start_date'),
$input->getArgument('end_date')
);
if (is_array($data)) {
Tool::csv($data);
}
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Console;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class TaskOverdueNotification extends Base
{
protected function configure()
{
$this
->setName('notification:overdue-tasks')
->setDescription('Send notifications for overdue tasks')
->addOption('show', null, InputOption::VALUE_NONE, 'Show sent overdue tasks');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$projects = array();
$tasks = $this->taskFinder->getOverdueTasks();
// Group tasks by project
foreach ($tasks as $task) {
$projects[$task['project_id']][] = $task;
}
// Send notifications for each project
foreach ($projects as $project_id => $project_tasks) {
$users = $this->notification->getUsersList($project_id);
$this->notification->sendEmails(
'task_due',
$users,
array('tasks' => $project_tasks, 'project' => $project_tasks[0]['project_name'])
);
}
if ($input->getOption('show')) {
$this->showTable($output, $tasks);
}
}
public function showTable(OutputInterface $output, array $tasks)
{
$rows = array();
foreach ($tasks as $task) {
$rows[] = array(
$task['id'],
$task['title'],
date('Y-m-d', $task['date_due']),
$task['project_id'],
$task['project_name'],
$task['assignee_name'] ?: $task['assignee_username'],
);
}
$table = new Table($output);
$table
->setHeaders(array('Id', 'Title', 'Due date', 'Project Id', 'Project name', 'Assignee'))
->setRows($rows)
->render();
}
}

View file

@ -19,7 +19,7 @@ class Action extends Base
{
$project = $this->getProjectManagement();
$this->response->html($this->projectLayout('action_index', array(
$this->response->html($this->projectLayout('action/index', array(
'values' => array('project_id' => $project['id']),
'project' => $project,
'actions' => $this->action->getAllByProject($project['id']),
@ -27,11 +27,10 @@ class Action extends Base
'available_events' => $this->action->getAvailableEvents(),
'available_params' => $this->action->getAllActionParameters(),
'columns_list' => $this->board->getColumnsList($project['id']),
'users_list' => $this->projectPermission->getUsersList($project['id']),
'users_list' => $this->projectPermission->getMemberList($project['id']),
'projects_list' => $this->project->getList(false),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
'menu' => 'projects',
'title' => t('Automatic actions')
)));
}
@ -50,11 +49,10 @@ class Action extends Base
$this->response->redirect('?controller=action&action=index&project_id='.$project['id']);
}
$this->response->html($this->projectLayout('action_event', array(
$this->response->html($this->projectLayout('action/event', array(
'values' => $values,
'project' => $project,
'events' => $this->action->getCompatibleEvents($values['action_name']),
'menu' => 'projects',
'title' => t('Automatic actions')
)));
}
@ -83,16 +81,15 @@ class Action extends Base
$projects_list = $this->project->getList(false);
unset($projects_list[$project['id']]);
$this->response->html($this->projectLayout('action_params', array(
$this->response->html($this->projectLayout('action/params', array(
'values' => $values,
'action_params' => $action_params,
'columns_list' => $this->board->getColumnsList($project['id']),
'users_list' => $this->projectPermission->getUsersList($project['id']),
'users_list' => $this->projectPermission->getMemberList($project['id']),
'projects_list' => $projects_list,
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
'project' => $project,
'menu' => 'projects',
'title' => t('Automatic actions')
)));
}
@ -140,12 +137,11 @@ class Action extends Base
{
$project = $this->getProjectManagement();
$this->response->html($this->projectLayout('action_remove', array(
$this->response->html($this->projectLayout('action/remove', array(
'action' => $this->action->getById($this->request->getIntegerParam('action_id')),
'available_events' => $this->action->getAvailableEvents(),
'available_actions' => $this->action->getAvailableActions(),
'project' => $project,
'menu' => 'projects',
'title' => t('Remove an action')
)));
}

View file

@ -0,0 +1,128 @@
<?php
namespace Controller;
/**
* Project Anaytic controller
*
* @package controller
* @author Frederic Guillot
*/
class Analytic extends Base
{
/**
* Common layout for analytic views
*
* @access private
* @param string $template Template name
* @param array $params Template parameters
* @return string
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->acl->getUserId());
$params['analytic_content_for_layout'] = $this->template->load($template, $params);
return $this->template->layout('analytic/layout', $params);
}
/**
* Show tasks distribution graph
*
* @access public
*/
public function tasks()
{
$project = $this->getProject();
$metrics = $this->projectAnalytic->getTaskRepartition($project['id']);
if ($this->request->isAjax()) {
$this->response->json(array(
'metrics' => $metrics,
'labels' => array(
'column_title' => t('Column'),
'nb_tasks' => t('Number of tasks'),
)
));
}
else {
$this->response->html($this->layout('analytic/tasks', array(
'project' => $project,
'metrics' => $metrics,
'title' => t('Task repartition for "%s"', $project['name']),
)));
}
}
/**
* Show users repartition
*
* @access public
*/
public function users()
{
$project = $this->getProject();
$metrics = $this->projectAnalytic->getUserRepartition($project['id']);
if ($this->request->isAjax()) {
$this->response->json(array(
'metrics' => $metrics,
'labels' => array(
'user' => t('User'),
'nb_tasks' => t('Number of tasks'),
)
));
}
else {
$this->response->html($this->layout('analytic/users', array(
'project' => $project,
'metrics' => $metrics,
'title' => t('User repartition for "%s"', $project['name']),
)));
}
}
/**
* Show cumulative flow diagram
*
* @access public
*/
public function cfd()
{
$project = $this->getProject();
$values = $this->request->getValues();
$from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week')));
$to = $this->request->getStringParam('to', date('Y-m-d'));
if (! empty($values)) {
$from = $values['from'];
$to = $values['to'];
}
if ($this->request->isAjax()) {
$this->response->json(array(
'columns' => array_values($this->board->getColumnsList($project['id'])),
'metrics' => $this->projectDailySummary->getRawMetrics($project['id'], $from, $to),
'labels' => array(
'column' => t('Column'),
'day' => t('Date'),
'total' => t('Tasks'),
)
));
}
else {
$this->response->html($this->layout('analytic/cfd', array(
'values' => array(
'from' => $from,
'to' => $to,
),
'display_graph' => $this->projectDailySummary->countDays($project['id'], $from, $to) >= 2,
'project' => $project,
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
'title' => t('Cumulative flow diagram for "%s"', $project['name']),
)));
}
}
}

View file

@ -3,6 +3,8 @@
namespace Controller;
use Model\Project as ProjectModel;
use Model\SubTask as SubTaskModel;
use Helper;
/**
* Application controller
@ -12,6 +14,16 @@ use Model\Project as ProjectModel;
*/
class App extends Base
{
/**
* Check if the user is connected
*
* @access public
*/
public function status()
{
$this->response->text('OK');
}
/**
* Dashboard for the current user
*
@ -19,15 +31,155 @@ class App extends Base
*/
public function index()
{
$user_id = $this->acl->getUserId();
$projects = $this->projectPermission->getAllowedProjects($user_id);
$paginate = $this->request->getStringParam('paginate', 'userTasks');
$offset = $this->request->getIntegerParam('offset', 0);
$direction = $this->request->getStringParam('direction');
$order = $this->request->getStringParam('order');
$this->response->html($this->template->layout('app_index', array(
'board_selector' => $projects,
'events' => $this->projectActivity->getProjects(array_keys($projects), 10),
'tasks' => $this->taskFinder->getAllTasksByUser($user_id),
'menu' => 'dashboard',
$user_id = $this->acl->getUserId();
$projects = $this->projectPermission->getMemberProjects($user_id);
$project_ids = array_keys($projects);
$params = array(
'title' => t('Dashboard'),
)));
'board_selector' => $this->projectPermission->getAllowedProjects($user_id),
'events' => $this->projectActivity->getProjects($project_ids, 10),
);
$params += $this->getTaskPagination($user_id, $paginate, $offset, $order, $direction);
$params += $this->getSubtaskPagination($user_id, $paginate, $offset, $order, $direction);
$params += $this->getProjectPagination($project_ids, $paginate, $offset, $order, $direction);
$this->response->html($this->template->layout('app/dashboard', $params));
}
/**
* Get tasks pagination
*
* @access public
*/
private function getTaskPagination($user_id, $paginate, $offset, $order, $direction)
{
$limit = 10;
if (! in_array($order, array('tasks.id', 'project_name', 'title', 'date_due'))) {
$order = 'tasks.id';
$direction = 'ASC';
}
if ($paginate === 'userTasks') {
$tasks = $this->taskPaginator->userTasks($user_id, $offset, $limit, $order, $direction);
}
else {
$offset = 0;
$tasks = $this->taskPaginator->userTasks($user_id, $offset, $limit);
}
return array(
'tasks' => $tasks,
'task_pagination' => array(
'controller' => 'app',
'action' => 'index',
'params' => array('paginate' => 'userTasks'),
'direction' => $direction,
'order' => $order,
'total' => $this->taskPaginator->countUserTasks($user_id),
'offset' => $offset,
'limit' => $limit,
)
);
}
/**
* Get subtasks pagination
*
* @access public
*/
private function getSubtaskPagination($user_id, $paginate, $offset, $order, $direction)
{
$status = array(SubTaskModel::STATUS_TODO, SubTaskModel::STATUS_INPROGRESS);
$limit = 10;
if (! in_array($order, array('tasks.id', 'project_name', 'status', 'title'))) {
$order = 'tasks.id';
$direction = 'ASC';
}
if ($paginate === 'userSubtasks') {
$subtasks = $this->subtaskPaginator->userSubtasks($user_id, $status, $offset, $limit, $order, $direction);
}
else {
$offset = 0;
$subtasks = $this->subtaskPaginator->userSubtasks($user_id, $status, $offset, $limit);
}
return array(
'subtasks' => $subtasks,
'subtask_pagination' => array(
'controller' => 'app',
'action' => 'index',
'params' => array('paginate' => 'userSubtasks'),
'direction' => $direction,
'order' => $order,
'total' => $this->subtaskPaginator->countUserSubtasks($user_id, $status),
'offset' => $offset,
'limit' => $limit,
)
);
}
/**
* Get projects pagination
*
* @access public
*/
private function getProjectPagination($project_ids, $paginate, $offset, $order, $direction)
{
$limit = 5;
if (! in_array($order, array('id', 'name'))) {
$order = 'name';
$direction = 'ASC';
}
if ($paginate === 'projectSummaries') {
$projects = $this->projectPaginator->projectSummaries($project_ids, $offset, $limit, $order, $direction);
}
else {
$offset = 0;
$projects = $this->projectPaginator->projectSummaries($project_ids, $offset, $limit);
}
return array(
'projects' => $projects,
'project_pagination' => array(
'controller' => 'app',
'action' => 'index',
'params' => array('paginate' => 'projectSummaries'),
'direction' => $direction,
'order' => $order,
'total' => count($project_ids),
'offset' => $offset,
'limit' => $limit,
)
);
}
/**
* Render Markdown Text and reply with the HTML Code
*
* @access public
*/
public function preview()
{
$payload = $this->request->getJson();
if (empty($payload['text'])) {
$this->response->html('<p>'.t('Nothing to preview...').'</p>');
}
else {
$this->response->html(Helper\markdown($payload['text']));
}
}
}

View file

@ -2,9 +2,13 @@
namespace Controller;
use Pimple\Container;
use Core\Tool;
use Core\Registry;
use Core\Security;
use Core\Request;
use Core\Response;
use Core\Template;
use Core\Session;
use Model\LastLogin;
/**
@ -13,57 +17,65 @@ use Model\LastLogin;
* @package controller
* @author Frederic Guillot
*
* @property \Model\Acl $acl
* @property \Model\Authentication $authentication
* @property \Model\Action $action
* @property \Model\Board $board
* @property \Model\Category $category
* @property \Model\Color $color
* @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\ProjectPermission $projectPermission
* @property \Model\SubTask $subTask
* @property \Model\Task $task
* @property \Model\TaskHistory $taskHistory
* @property \Model\TaskExport $taskExport
* @property \Model\TaskFinder $taskFinder
* @property \Model\TaskPermission $taskPermission
* @property \Model\TaskValidator $taskValidator
* @property \Model\CommentHistory $commentHistory
* @property \Model\SubtaskHistory $subtaskHistory
* @property \Model\TimeTracking $timeTracking
* @property \Model\User $user
* @property \Model\Webhook $webhook
* @property \Model\Acl $acl
* @property \Model\Authentication $authentication
* @property \Model\Action $action
* @property \Model\Board $board
* @property \Model\Category $category
* @property \Model\Color $color
* @property \Model\Comment $comment
* @property \Model\Config $config
* @property \Model\DateParser $dateParser
* @property \Model\File $file
* @property \Model\LastLogin $lastLogin
* @property \Model\Notification $notification
* @property \Model\Project $project
* @property \Model\ProjectPermission $projectPermission
* @property \Model\ProjectAnalytic $projectAnalytic
* @property \Model\ProjectDailySummary $projectDailySummary
* @property \Model\SubTask $subTask
* @property \Model\Task $task
* @property \Model\TaskCreation $taskCreation
* @property \Model\TaskModification $taskModification
* @property \Model\TaskDuplication $taskDuplication
* @property \Model\TaskHistory $taskHistory
* @property \Model\TaskExport $taskExport
* @property \Model\TaskFinder $taskFinder
* @property \Model\TaskPosition $taskPosition
* @property \Model\TaskPermission $taskPermission
* @property \Model\TaskStatus $taskStatus
* @property \Model\TaskValidator $taskValidator
* @property \Model\CommentHistory $commentHistory
* @property \Model\SubtaskHistory $subtaskHistory
* @property \Model\TimeTracking $timeTracking
* @property \Model\User $user
* @property \Model\Webhook $webhook
*/
abstract class Base
{
/**
* Request instance
*
* @accesss public
* @accesss protected
* @var \Core\Request
*/
public $request;
protected $request;
/**
* Response instance
*
* @accesss public
* @accesss protected
* @var \Core\Response
*/
public $response;
protected $response;
/**
* Template instance
*
* @accesss public
* @accesss protected
* @var \Core\Template
*/
public $template;
protected $template;
/**
* Session instance
@ -71,37 +83,53 @@ abstract class Base
* @accesss public
* @var \Core\Session
*/
public $session;
protected $session;
/**
* Registry instance
* Container instance
*
* @access private
* @var \Core\Registry
* @var \Pimple\Container
*/
private $registry;
private $container;
/**
* Constructor
*
* @access public
* @param \Core\Registry $registry Registry instance
* @param \Pimple\Container $container
*/
public function __construct(Registry $registry)
public function __construct(Container $container)
{
$this->registry = $registry;
$this->container = $container;
$this->request = new Request;
$this->response = new Response;
$this->session = new Session;
$this->template = new Template;
}
/**
* Destructor
*
* @access public
*/
public function __destruct()
{
// foreach ($this->container['db']->getLogMessages() as $message) {
// $this->container['logger']->addDebug($message);
// }
}
/**
* Load automatically models
*
* @access public
* @param string $name Model name
* @param string $name Model name
* @return mixed
*/
public function __get($name)
{
return Tool::loadModel($this->registry, $name);
return Tool::loadModel($this->container, $name);
}
/**
@ -112,7 +140,7 @@ abstract class Base
public function beforeAction($controller, $action)
{
// Start the session
$this->session->open(BASE_URL_DIRECTORY, SESSION_SAVE_PATH);
$this->session->open(BASE_URL_DIRECTORY);
// HTTP secure headers
$this->response->csp(array('style-src' => "'self' 'unsafe-inline'"));
@ -133,6 +161,11 @@ abstract class Base
// Authentication
if (! $this->authentication->isAuthenticated($controller, $action)) {
if ($this->request->isAjax()) {
$this->response->text('Not Authorized', 401);
}
$this->response->redirect('?controller=user&action=login&redirect_query='.urlencode($this->request->getQueryString()));
}
@ -154,6 +187,7 @@ abstract class Base
{
$models = array(
'projectActivity', // Order is important
'projectDailySummary',
'action',
'project',
'webhook',
@ -173,7 +207,7 @@ abstract class Base
*/
public function notfound($no_layout = false)
{
$this->response->html($this->template->layout('app_notfound', array(
$this->response->html($this->template->layout('app/notfound', array(
'title' => t('Page not found'),
'no_layout' => $no_layout,
)));
@ -187,7 +221,7 @@ abstract class Base
*/
public function forbidden($no_layout = false)
{
$this->response->html($this->template->layout('app_forbidden', array(
$this->response->html($this->template->layout('app/forbidden', array(
'title' => t('Access Forbidden'),
'no_layout' => $no_layout,
)));
@ -245,8 +279,10 @@ abstract class Base
$content = $this->template->load($template, $params);
$params['task_content_for_layout'] = $content;
$params['title'] = $params['task']['project_name'].' &gt; '.$params['task']['title'];
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->acl->getUserId());
return $this->template->layout('task_layout', $params);
return $this->template->layout('task/layout', $params);
}
/**
@ -261,9 +297,10 @@ abstract class Base
{
$content = $this->template->load($template, $params);
$params['project_content_for_layout'] = $content;
$params['menu'] = 'projects';
$params['title'] = $params['project']['name'] === $params['title'] ? $params['title'] : $params['project']['name'].' &gt; '.$params['title'];
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->acl->getUserId());
return $this->template->layout('project_layout', $params);
return $this->template->layout('project/layout', $params);
}
/**

View file

@ -42,27 +42,12 @@ class Board extends Base
{
$task = $this->getTask();
$project = $this->project->getById($task['project_id']);
$projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId());
$params = array(
'errors' => array(),
$this->response->html($this->template->load('board/assignee', array(
'values' => $task,
'users_list' => $this->projectPermission->getUsersList($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_assignee', $params));
}
else {
$this->response->html($this->template->layout('board_assignee', $params + array(
'menu' => 'boards',
'title' => t('Change assignee').' - '.$task['title'],
)));
}
'users_list' => $this->projectPermission->getMemberList($project['id']),
'project' => $project,
)));
}
/**
@ -77,7 +62,7 @@ class Board extends Base
list($valid,) = $this->taskValidator->validateAssigneeModification($values);
if ($valid && $this->task->update($values)) {
if ($valid && $this->taskModification->update($values)) {
$this->session->flash(t('Task updated successfully.'));
}
else {
@ -96,27 +81,12 @@ class Board extends Base
{
$task = $this->getTask();
$project = $this->project->getById($task['project_id']);
$projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId());
$params = array(
'errors' => array(),
$this->response->html($this->template->load('board/category', 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'],
)));
}
'project' => $project,
)));
}
/**
@ -131,7 +101,7 @@ class Board extends Base
list($valid,) = $this->taskValidator->validateCategoryModification($values);
if ($valid && $this->task->update($values)) {
if ($valid && $this->taskModification->update($values)) {
$this->session->flash(t('Task updated successfully.'));
}
else {
@ -158,7 +128,7 @@ class Board extends Base
}
// Display the board with a specific layout
$this->response->html($this->template->layout('board_public', array(
$this->response->html($this->template->layout('board/public', array(
'project' => $project,
'columns' => $this->board->get($project['id']),
'categories' => $this->category->getList($project['id'], false),
@ -214,15 +184,12 @@ class Board extends Base
$this->user->storeLastSeenProjectId($project['id']);
$this->response->html($this->template->layout('board_index', array(
'users' => $this->projectPermission->getUsersList($project['id'], true, true),
'filters' => array('user_id' => UserModel::EVERYBODY_ID),
$this->response->html($this->template->layout('board/index', array(
'users' => $this->projectPermission->getMemberList($project['id'], true, true),
'projects' => $projects,
'current_project_id' => $project['id'],
'current_project_name' => $project['name'],
'project' => $project,
'board' => $this->board->get($project['id']),
'categories' => $this->category->getList($project['id'], true, true),
'menu' => 'boards',
'title' => $project['name'],
'board_selector' => $board_selector,
'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
@ -246,12 +213,11 @@ class Board extends Base
$values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null;
}
$this->response->html($this->projectLayout('board_edit', array(
$this->response->html($this->projectLayout('board/edit', array(
'errors' => array(),
'values' => $values + array('project_id' => $project['id']),
'columns' => $columns,
'project' => $project,
'menu' => 'projects',
'title' => t('Edit board')
)));
}
@ -287,12 +253,11 @@ class Board extends Base
}
}
$this->response->html($this->projectLayout('board_edit', array(
$this->response->html($this->projectLayout('board/edit', array(
'errors' => $errors,
'values' => $values + array('project_id' => $project['id']),
'columns' => $columns,
'project' => $project,
'menu' => 'projects',
'title' => t('Edit board')
)));
}
@ -326,12 +291,11 @@ class Board extends Base
}
}
$this->response->html($this->projectLayout('board_edit', array(
$this->response->html($this->projectLayout('board/edit', array(
'errors' => $errors,
'values' => $values + $data,
'columns' => $columns,
'project' => $project,
'menu' => 'projects',
'title' => t('Edit board')
)));
}
@ -359,10 +323,9 @@ class Board extends Base
$this->response->redirect('?controller=board&action=edit&project_id='.$project['id']);
}
$this->response->html($this->projectLayout('board_remove', array(
$this->response->html($this->projectLayout('board/remove', array(
'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')),
'project' => $project,
'menu' => 'projects',
'title' => t('Remove a column from a board')
)));
}
@ -379,16 +342,16 @@ class Board extends Base
if ($project_id > 0 && $this->request->isAjax()) {
if (! $this->projectPermission->isUserAllowed($project_id, $this->acl->getUserId())) {
$this->response->status(401);
$this->response->text('Forbidden', 403);
}
$values = $this->request->getValues();
$values = $this->request->getJson();
if ($this->task->movePosition($project_id, $values['task_id'], $values['column_id'], $values['position'])) {
if ($this->taskPosition->movePosition($project_id, $values['task_id'], $values['column_id'], $values['position'])) {
$this->response->html(
$this->template->load('board_show', array(
'current_project_id' => $project_id,
$this->template->load('board/show', array(
'project' => $this->project->getById($project_id),
'board' => $this->board->get($project_id),
'categories' => $this->category->getList($project_id, false),
'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
@ -403,7 +366,7 @@ class Board extends Base
}
}
else {
$this->response->status(401);
$this->response->status(403);
}
}
@ -420,13 +383,13 @@ class Board extends Base
$timestamp = $this->request->getIntegerParam('timestamp');
if ($project_id > 0 && ! $this->projectPermission->isUserAllowed($project_id, $this->acl->getUserId())) {
$this->response->text('Not Authorized', 401);
$this->response->text('Forbidden', 403);
}
if ($this->project->isModifiedSince($project_id, $timestamp)) {
$this->response->html(
$this->template->load('board_show', array(
'current_project_id' => $project_id,
$this->template->load('board/show', array(
'project' => $this->project->getById($project_id),
'board' => $this->board->get($project_id),
'categories' => $this->category->getList($project_id, false),
'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
@ -439,7 +402,77 @@ class Board extends Base
}
}
else {
$this->response->status(401);
$this->response->status(403);
}
}
/**
* Get subtasks on mouseover
*
* @access public
*/
public function subtasks()
{
$task = $this->getTask();
$this->response->html($this->template->load('board/subtasks', array(
'subtasks' => $this->subTask->getAll($task['id'])
)));
}
/**
* Change the status of a subtask from the mouseover
*
* @access public
*/
public function toggleSubtask()
{
$task = $this->getTask();
$this->subTask->toggleStatus($this->request->getIntegerParam('subtask_id'));
$this->response->html($this->template->load('board/subtasks', array(
'subtasks' => $this->subTask->getAll($task['id'])
)));
}
/**
* Display all attachments during the task mouseover
*
* @access public
*/
public function attachments()
{
$task = $this->getTask();
$this->response->html($this->template->load('board/files', array(
'files' => $this->file->getAll($task['id'])
)));
}
/**
* Display comments during a task mouseover
*
* @access public
*/
public function comments()
{
$task = $this->getTask();
$this->response->html($this->template->load('board/comments', array(
'comments' => $this->comment->getAll($task['id'])
)));
}
/**
* Display the description
*
* @access public
*/
public function description()
{
$task = $this->getTask();
$this->response->html($this->template->load('board/description', array(
'task' => $task
)));
}
}

View file

@ -34,16 +34,15 @@ class Category extends Base
*
* @access public
*/
public function index()
public function index(array $values = array(), array $errors = array())
{
$project = $this->getProjectManagement();
$this->response->html($this->projectLayout('category_index', array(
$this->response->html($this->projectLayout('category/index', array(
'categories' => $this->category->getList($project['id'], false),
'values' => array('project_id' => $project['id']),
'errors' => array(),
'values' => $values + array('project_id' => $project['id']),
'errors' => $errors,
'project' => $project,
'menu' => 'projects',
'title' => t('Categories')
)));
}
@ -71,14 +70,7 @@ class Category extends Base
}
}
$this->response->html($this->projectLayout('category_index', array(
'categories' => $this->category->getList($project['id'], false),
'values' => $values,
'errors' => $errors,
'project' => $project,
'menu' => 'projects',
'title' => t('Categories')
)));
$this->index($values, $errors);
}
/**
@ -86,16 +78,15 @@ class Category extends Base
*
* @access public
*/
public function edit()
public function edit(array $values = array(), array $errors = array())
{
$project = $this->getProjectManagement();
$category = $this->getCategory($project['id']);
$this->response->html($this->projectLayout('category_edit', array(
'values' => $category,
'errors' => array(),
$this->response->html($this->projectLayout('category/edit', array(
'values' => empty($values) ? $category : $values,
'errors' => $errors,
'project' => $project,
'menu' => 'projects',
'title' => t('Categories')
)));
}
@ -123,13 +114,7 @@ class Category extends Base
}
}
$this->response->html($this->projectLayout('category_edit', array(
'values' => $values,
'errors' => $errors,
'project' => $project,
'menu' => 'projects',
'title' => t('Categories')
)));
$this->edit($values, $errors);
}
/**
@ -142,10 +127,9 @@ class Category extends Base
$project = $this->getProjectManagement();
$category = $this->getCategory($project['id']);
$this->response->html($this->projectLayout('category_remove', array(
$this->response->html($this->projectLayout('category/remove', array(
'project' => $project,
'category' => $category,
'menu' => 'projects',
'title' => t('Remove a category')
)));
}

View file

@ -25,8 +25,7 @@ class Comment extends Base
}
if (! $this->acl->isAdminUser() && $comment['user_id'] != $this->acl->getUserId()) {
$this->response->html($this->template->layout('comment_forbidden', array(
'menu' => 'tasks',
$this->response->html($this->template->layout('comment/forbidden', array(
'title' => t('Access Forbidden')
)));
}
@ -39,18 +38,21 @@ class Comment extends Base
*
* @access public
*/
public function create()
public function create(array $values = array(), array $errors = array())
{
$task = $this->getTask();
$this->response->html($this->taskLayout('comment_create', array(
'values' => array(
if (empty($values)) {
$values = array(
'user_id' => $this->acl->getUserId(),
'task_id' => $task['id'],
),
'errors' => array(),
);
}
$this->response->html($this->taskLayout('comment/create', array(
'values' => $values,
'errors' => $errors,
'task' => $task,
'menu' => 'tasks',
'title' => t('Add a comment')
)));
}
@ -79,13 +81,7 @@ class Comment extends Base
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comments');
}
$this->response->html($this->taskLayout('comment_create', array(
'values' => $values,
'errors' => $errors,
'task' => $task,
'menu' => 'tasks',
'title' => t('Add a comment')
)));
$this->create($values, $errors);
}
/**
@ -93,17 +89,16 @@ class Comment extends Base
*
* @access public
*/
public function edit()
public function edit(array $values = array(), array $errors = array())
{
$task = $this->getTask();
$comment = $this->getComment();
$this->response->html($this->taskLayout('comment_edit', array(
'values' => $comment,
'errors' => array(),
$this->response->html($this->taskLayout('comment/edit', array(
'values' => empty($values) ? $comment : $values,
'errors' => $errors,
'comment' => $comment,
'task' => $task,
'menu' => 'tasks',
'title' => t('Edit a comment')
)));
}
@ -133,14 +128,7 @@ class Comment extends Base
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comment-'.$comment['id']);
}
$this->response->html($this->taskLayout('comment_edit', array(
'values' => $values,
'errors' => $errors,
'comment' => $comment,
'task' => $task,
'menu' => 'tasks',
'title' => t('Edit a comment')
)));
$this->edit($values, $errors);
}
/**
@ -153,10 +141,9 @@ class Comment extends Base
$task = $this->getTask();
$comment = $this->getComment();
$this->response->html($this->taskLayout('comment_remove', array(
$this->response->html($this->taskLayout('comment/remove', array(
'comment' => $comment,
'task' => $task,
'menu' => 'tasks',
'title' => t('Remove a comment')
)));
}

View file

@ -20,12 +20,12 @@ class Config extends Base
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->acl->getUserId());
$params['values'] = $this->config->getAll();
$params['errors'] = array();
$params['menu'] = 'config';
$params['config_content_for_layout'] = $this->template->load($template, $params);
return $this->template->layout('config_layout', $params);
return $this->template->layout('config/layout', $params);
}
/**
@ -59,9 +59,9 @@ class Config extends Base
*/
public function index()
{
$this->response->html($this->layout('config_about', array(
$this->response->html($this->layout('config/about', array(
'db_size' => $this->config->getDatabaseSize(),
'title' => t('About'),
'title' => t('Settings').' &gt; '.t('About'),
)));
}
@ -74,11 +74,11 @@ class Config extends Base
{
$this->common('application');
$this->response->html($this->layout('config_application', array(
'title' => t('Application settings'),
$this->response->html($this->layout('config/application', array(
'languages' => $this->config->getLanguages(),
'timezones' => $this->config->getTimezones(),
'date_formats' => $this->dateParser->getAvailableFormats(),
'title' => t('Settings').' &gt; '.t('Application settings'),
)));
}
@ -91,9 +91,9 @@ class Config extends Base
{
$this->common('board');
$this->response->html($this->layout('config_board', array(
'title' => t('Board settings'),
$this->response->html($this->layout('config/board', array(
'default_columns' => implode(', ', $this->board->getDefaultColumns()),
'title' => t('Settings').' &gt; '.t('Board settings'),
)));
}
@ -106,8 +106,8 @@ class Config extends Base
{
$this->common('webhook');
$this->response->html($this->layout('config_webhook', array(
'title' => t('Webhook settings'),
$this->response->html($this->layout('config/webhook', array(
'title' => t('Settings').' &gt; '.t('Webhook settings'),
)));
}
@ -118,8 +118,8 @@ class Config extends Base
*/
public function api()
{
$this->response->html($this->layout('config_api', array(
'title' => t('API'),
$this->response->html($this->layout('config/api', array(
'title' => t('Settings').' &gt; '.t('API'),
)));
}

View file

@ -21,11 +21,9 @@ class File extends Base
{
$task = $this->getTask();
$this->response->html($this->taskLayout('file_new', array(
$this->response->html($this->taskLayout('file/new', array(
'task' => $task,
'menu' => 'tasks',
'max_size' => ini_get('upload_max_filesize'),
'title' => t('Attach a document')
)));
}
@ -77,7 +75,7 @@ class File extends Base
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
if ($file['task_id'] == $task['id']) {
$this->response->html($this->template->load('file_open', array(
$this->response->html($this->template->load('file/open', array(
'file' => $file
)));
}
@ -134,11 +132,9 @@ class File extends Base
$task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
$this->response->html($this->taskLayout('file_remove', array(
$this->response->html($this->taskLayout('file/remove', array(
'task' => $task,
'file' => $file,
'menu' => 'tasks',
'title' => t('Remove a file')
)));
}
}

View file

@ -33,11 +33,11 @@ class Project extends Base
}
}
$this->response->html($this->template->layout('project_index', array(
$this->response->html($this->template->layout('project/index', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->acl->getUserId()),
'active_projects' => $active_projects,
'inactive_projects' => $inactive_projects,
'nb_projects' => $nb_projects,
'menu' => 'projects',
'title' => t('Projects').' ('.$nb_projects.')'
)));
}
@ -51,7 +51,7 @@ class Project extends Base
{
$project = $this->getProject();
$this->response->html($this->projectLayout('project_show', array(
$this->response->html($this->projectLayout('project/show', array(
'project' => $project,
'stats' => $this->project->getStats($project['id']),
'webhook_token' => $this->config->get('webhook_token'),
@ -64,7 +64,7 @@ class Project extends Base
*
* @access public
*/
public function export()
public function exportTasks()
{
$project = $this->getProjectManagement();
$from = $this->request->getStringParam('from');
@ -72,14 +72,14 @@ class Project extends Base
if ($from && $to) {
$data = $this->taskExport->export($project['id'], $from, $to);
$this->response->forceDownload('Export_'.date('Y_m_d_H_i_S').'.csv');
$this->response->forceDownload('Tasks_'.date('Y_m_d_H_i').'.csv');
$this->response->csv($data);
}
$this->response->html($this->projectLayout('project_export', array(
$this->response->html($this->projectLayout('project/export_tasks', array(
'values' => array(
'controller' => 'project',
'action' => 'export',
'action' => 'exportTasks',
'project_id' => $project['id'],
'from' => $from,
'to' => $to,
@ -92,6 +92,39 @@ class Project extends Base
)));
}
/**
* Daily project summary export
*
* @access public
*/
public function exportDailyProjectSummary()
{
$project = $this->getProjectManagement();
$from = $this->request->getStringParam('from');
$to = $this->request->getStringParam('to');
if ($from && $to) {
$data = $this->projectDailySummary->getAggregatedMetrics($project['id'], $from, $to);
$this->response->forceDownload('Daily_Summary_'.date('Y_m_d_H_i').'.csv');
$this->response->csv($data);
}
$this->response->html($this->projectLayout('project/export_daily_summary', array(
'values' => array(
'controller' => 'project',
'action' => 'exportDailyProjectSummary',
'project_id' => $project['id'],
'from' => $from,
'to' => $to,
),
'errors' => array(),
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
'project' => $project,
'title' => t('Daily project summary export')
)));
}
/**
* Public access management
*
@ -115,7 +148,7 @@ class Project extends Base
$this->response->redirect('?controller=project&action=share&project_id='.$project['id']);
}
$this->response->html($this->projectLayout('project_share', array(
$this->response->html($this->projectLayout('project/share', array(
'project' => $project,
'title' => t('Public access'),
)));
@ -126,13 +159,13 @@ class Project extends Base
*
* @access public
*/
public function edit()
public function edit(array $values = array(), array $errors = array())
{
$project = $this->getProjectManagement();
$this->response->html($this->projectLayout('project_edit', array(
'errors' => array(),
'values' => $project,
$this->response->html($this->projectLayout('project/edit', array(
'values' => empty($values) ? $project : $values,
'errors' => $errors,
'project' => $project,
'title' => t('Edit project')
)));
@ -146,7 +179,7 @@ class Project extends Base
public function update()
{
$project = $this->getProjectManagement();
$values = $this->request->getValues() + array('is_active' => 0);
$values = $this->request->getValues();
list($valid, $errors) = $this->project->validateModification($values);
if ($valid) {
@ -160,12 +193,7 @@ class Project extends Base
}
}
$this->response->html($this->projectLayout('project_edit', array(
'errors' => $errors,
'values' => $values,
'project' => $project,
'title' => t('Edit Project')
)));
$this->edit($values, $errors);
}
/**
@ -177,7 +205,7 @@ class Project extends Base
{
$project = $this->getProjectManagement();
$this->response->html($this->projectLayout('project_users', array(
$this->response->html($this->projectLayout('project/users', array(
'project' => $project,
'users' => $this->projectPermission->getAllUsers($project['id']),
'title' => t('Edit project access list')
@ -282,7 +310,7 @@ class Project extends Base
$this->response->redirect('?controller=project');
}
$this->response->html($this->projectLayout('project_remove', array(
$this->response->html($this->projectLayout('project/remove', array(
'project' => $project,
'title' => t('Remove project')
)));
@ -311,7 +339,7 @@ class Project extends Base
$this->response->redirect('?controller=project');
}
$this->response->html($this->projectLayout('project_duplicate', array(
$this->response->html($this->projectLayout('project/duplicate', array(
'project' => $project,
'title' => t('Clone this project')
)));
@ -339,7 +367,7 @@ class Project extends Base
$this->response->redirect('?controller=project&action=show&project_id='.$project['id']);
}
$this->response->html($this->projectLayout('project_disable', array(
$this->response->html($this->projectLayout('project/disable', array(
'project' => $project,
'title' => t('Project activation')
)));
@ -367,7 +395,7 @@ class Project extends Base
$this->response->redirect('?controller=project&action=show&project_id='.$project['id']);
}
$this->response->html($this->projectLayout('project_enable', array(
$this->response->html($this->projectLayout('project/enable', array(
'project' => $project,
'title' => t('Project activation')
)));
@ -388,7 +416,7 @@ class Project extends Base
$this->forbidden(true);
}
$this->response->xml($this->template->load('project_feed', array(
$this->response->xml($this->template->load('project/feed', array(
'events' => $this->projectActivity->getProject($project['id']),
'project' => $project,
)));
@ -403,9 +431,9 @@ class Project extends Base
{
$project = $this->getProject();
$this->response->html($this->template->layout('project_activity', array(
$this->response->html($this->template->layout('project/activity', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->acl->getUserId()),
'events' => $this->projectActivity->getProject($project['id']),
'menu' => 'projects',
'project' => $project,
'title' => t('%s\'s activity', $project['name'])
)));
@ -428,11 +456,12 @@ class Project extends Base
$limit = 25;
if ($search !== '') {
$tasks = $this->taskFinder->search($project['id'], $search, $offset, $limit, $order, $direction);
$nb_tasks = $this->taskFinder->countSearch($project['id'], $search);
$tasks = $this->taskPaginator->searchTasks($project['id'], $search, $offset, $limit, $order, $direction);
$nb_tasks = $this->taskPaginator->countSearchTasks($project['id'], $search);
}
$this->response->html($this->template->layout('project_search', array(
$this->response->html($this->template->layout('project/search', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->acl->getUserId()),
'tasks' => $tasks,
'nb_tasks' => $nb_tasks,
'pagination' => array(
@ -452,10 +481,9 @@ class Project extends Base
'project_id' => $project['id'],
),
'project' => $project,
'menu' => 'projects',
'columns' => $this->board->getColumnsList($project['id']),
'categories' => $this->category->getList($project['id'], false),
'title' => $project['name'].($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '')
'title' => t('Search in the project "%s"', $project['name']).($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '')
)));
}
@ -472,10 +500,11 @@ class Project extends Base
$offset = $this->request->getIntegerParam('offset', 0);
$limit = 25;
$tasks = $this->taskFinder->getClosedTasks($project['id'], $offset, $limit, $order, $direction);
$nb_tasks = $this->taskFinder->countByProjectId($project['id'], array(TaskModel::STATUS_CLOSED));
$tasks = $this->taskPaginator->closedTasks($project['id'], $offset, $limit, $order, $direction);
$nb_tasks = $this->taskPaginator->countClosedTasks($project['id']);
$this->response->html($this->template->layout('project_tasks', array(
$this->response->html($this->template->layout('project/tasks', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->acl->getUserId()),
'pagination' => array(
'controller' => 'project',
'action' => 'tasks',
@ -487,12 +516,11 @@ class Project extends Base
'limit' => $limit,
),
'project' => $project,
'menu' => 'projects',
'columns' => $this->board->getColumnsList($project['id']),
'categories' => $this->category->getList($project['id'], false),
'tasks' => $tasks,
'nb_tasks' => $nb_tasks,
'title' => $project['name'].' ('.$nb_tasks.')'
'title' => t('Completed tasks for "%s"', $project['name']).' ('.$nb_tasks.')'
)));
}
@ -501,14 +529,15 @@ class Project extends Base
*
* @access public
*/
public function create()
public function create(array $values = array(), array $errors = array())
{
$this->response->html($this->template->layout('project_new', array(
'errors' => array(),
'values' => array(
'is_private' => $this->request->getIntegerParam('private', $this->acl->isRegularUser()),
),
'title' => t('New project')
$is_private = $this->request->getIntegerParam('private', $this->acl->isRegularUser());
$this->response->html($this->template->layout('project/new', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->acl->getUserId()),
'values' => empty($values) ? array('is_private' => $is_private) : $values,
'errors' => $errors,
'title' => $is_private ? t('New private project') : t('New project'),
)));
}
@ -524,19 +553,17 @@ class Project extends Base
if ($valid) {
if ($this->project->create($values, $this->acl->getUserId())) {
$project_id = $this->project->create($values, $this->acl->getUserId(), true);
if ($project_id) {
$this->session->flash(t('Your project have been created successfully.'));
$this->response->redirect('?controller=project');
$this->response->redirect('?controller=project&action=show&project_id='.$project_id);
}
else {
$this->session->flashError(t('Unable to create your project.'));
}
}
$this->response->html($this->template->layout('project_new', array(
'errors' => $errors,
'values' => $values,
'title' => t('New Project')
)));
$this->create($values, $errors);
}
}

View file

@ -32,20 +32,22 @@ class Subtask extends Base
*
* @access public
*/
public function create()
public function create(array $values = array(), array $errors = array())
{
$task = $this->getTask();
$this->response->html($this->taskLayout('subtask_create', array(
'values' => array(
if (empty($values)) {
$values = array(
'task_id' => $task['id'],
'another_subtask' => $this->request->getIntegerParam('another_subtask', 0)
),
'errors' => array(),
'users_list' => $this->projectPermission->getUsersList($task['project_id']),
);
}
$this->response->html($this->taskLayout('subtask/create', array(
'values' => $values,
'errors' => $errors,
'users_list' => $this->projectPermission->getMemberList($task['project_id']),
'task' => $task,
'menu' => 'tasks',
'title' => t('Add a sub-task')
)));
}
@ -77,14 +79,7 @@ class Subtask extends Base
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks');
}
$this->response->html($this->taskLayout('subtask_create', array(
'values' => $values,
'errors' => $errors,
'users_list' => $this->projectPermission->getUsersList($task['project_id']),
'task' => $task,
'menu' => 'tasks',
'title' => t('Add a sub-task')
)));
$this->create($values, $errors);
}
/**
@ -92,20 +87,18 @@ class Subtask extends Base
*
* @access public
*/
public function edit()
public function edit(array $values = array(), array $errors = array())
{
$task = $this->getTask();
$subtask = $this->getSubTask();
$this->response->html($this->taskLayout('subtask_edit', array(
'values' => $subtask,
'errors' => array(),
'users_list' => $this->projectPermission->getUsersList($task['project_id']),
$this->response->html($this->taskLayout('subtask/edit', array(
'values' => empty($values) ? $subtask : $values,
'errors' => $errors,
'users_list' => $this->projectPermission->getMemberList($task['project_id']),
'status_list' => $this->subTask->getStatusList(),
'subtask' => $subtask,
'task' => $task,
'menu' => 'tasks',
'title' => t('Edit a sub-task')
)));
}
@ -134,16 +127,7 @@ class Subtask extends Base
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks');
}
$this->response->html($this->taskLayout('subtask_edit', array(
'values' => $values,
'errors' => $errors,
'users_list' => $this->projectPermission->getUsersList($task['project_id']),
'status_list' => $this->subTask->getStatusList(),
'subtask' => $subtask,
'task' => $task,
'menu' => 'tasks',
'title' => t('Edit a sub-task')
)));
$this->edit($values, $errors);
}
/**
@ -156,11 +140,9 @@ class Subtask extends Base
$task = $this->getTask();
$subtask = $this->getSubtask();
$this->response->html($this->taskLayout('subtask_remove', array(
$this->response->html($this->taskLayout('subtask/remove', array(
'subtask' => $subtask,
'task' => $task,
'menu' => 'tasks',
'title' => t('Remove a sub-task')
)));
}
@ -193,15 +175,9 @@ class Subtask extends Base
public function toggleStatus()
{
$task = $this->getTask();
$subtask = $this->getSubtask();
$subtask_id = $this->request->getIntegerParam('subtask_id');
$value = array(
'id' => $subtask['id'],
'status' => ($subtask['status'] + 1) % 3,
'task_id' => $task['id'],
);
if (! $this->subTask->update($value)) {
if (! $this->subTask->toggleStatus($subtask_id)) {
$this->session->flashError(t('Unable to update your sub-task.'));
}

View file

@ -32,7 +32,7 @@ class Task extends Base
$this->notfound(true);
}
$this->response->html($this->template->layout('task_public', array(
$this->response->html($this->template->layout('task/public', array(
'project' => $project,
'comments' => $this->comment->getAll($task['id']),
'subtasks' => $this->subTask->getAll($task['id']),
@ -65,7 +65,7 @@ class Task extends Base
$this->dateParser->format($values, array('date_started'));
$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']),
'comments' => $this->comment->getAll($task['id']),
@ -77,8 +77,7 @@ class Task extends Base
'colors_list' => $this->color->getList(),
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
'menu' => 'tasks',
'title' => $task['title'],
'title' => $task['project_name'].' &gt; '.$task['title'],
)));
}
@ -87,29 +86,33 @@ class Task extends Base
*
* @access public
*/
public function create()
public function create(array $values = array(), array $errors = array())
{
$project_id = $this->request->getIntegerParam('project_id');
$this->checkProjectPermissions($project_id);
$project = $this->getProject();
$method = $this->request->isAjax() ? 'load' : 'layout';
$this->response->html($this->template->layout('task_new', array(
'errors' => array(),
'values' => array(
'project_id' => $project_id,
if (empty($values)) {
$values = array(
'column_id' => $this->request->getIntegerParam('column_id'),
'color_id' => $this->request->getStringParam('color_id'),
'owner_id' => $this->request->getIntegerParam('owner_id'),
'another_task' => $this->request->getIntegerParam('another_task'),
),
);
}
$this->response->html($this->template->$method('task/new', array(
'ajax' => $this->request->isAjax(),
'errors' => $errors,
'values' => $values + array('project_id' => $project['id']),
'projects_list' => $this->project->getListByStatus(ProjectModel::ACTIVE),
'columns_list' => $this->board->getColumnsList($project_id),
'users_list' => $this->projectPermission->getUsersList($project_id),
'columns_list' => $this->board->getColumnsList($project['id']),
'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project_id),
'categories_list' => $this->category->getList($project['id']),
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
'menu' => 'tasks',
'title' => t('New task')
'title' => $project['name'].' &gt; '.t('New task')
)));
}
@ -120,16 +123,17 @@ class Task extends Base
*/
public function save()
{
$project = $this->getProject();
$values = $this->request->getValues();
$values['creator_id'] = $this->acl->getUserId();
$this->checkProjectPermissions($values['project_id']);
$this->checkProjectPermissions($project['id']);
list($valid, $errors) = $this->taskValidator->validateCreation($values);
if ($valid) {
if ($this->task->create($values)) {
if ($this->taskCreation->create($values)) {
$this->session->flash(t('Task created successfully.'));
if (isset($values['another_task']) && $values['another_task'] == 1) {
@ -146,19 +150,7 @@ class Task extends Base
}
}
$this->response->html($this->template->layout('task_new', array(
'errors' => $errors,
'values' => $values,
'projects_list' => $this->project->getListByStatus(ProjectModel::ACTIVE),
'columns_list' => $this->board->getColumnsList($values['project_id']),
'users_list' => $this->projectPermission->getUsersList($values['project_id']),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($values['project_id']),
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
'menu' => 'tasks',
'title' => t('New task')
)));
$this->create($values, $errors);
}
/**
@ -177,21 +169,19 @@ class Task extends Base
'values' => $task,
'errors' => array(),
'task' => $task,
'users_list' => $this->projectPermission->getUsersList($task['project_id']),
'users_list' => $this->projectPermission->getMemberList($task['project_id']),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($task['project_id']),
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
'ajax' => $ajax,
'menu' => 'tasks',
'title' => t('Edit a task')
);
if ($ajax) {
$this->response->html($this->template->load('task_edit', $params));
$this->response->html($this->template->load('task/edit', $params));
}
else {
$this->response->html($this->taskLayout('task_edit', $params));
$this->response->html($this->taskLayout('task/edit', $params));
}
}
@ -209,7 +199,7 @@ class Task extends Base
if ($valid) {
if ($this->task->update($values)) {
if ($this->taskModification->update($values)) {
$this->session->flash(t('Task updated successfully.'));
if ($this->request->getIntegerParam('ajax')) {
@ -224,18 +214,16 @@ class Task extends Base
}
}
$this->response->html($this->taskLayout('task_edit', array(
$this->response->html($this->taskLayout('task/edit', array(
'values' => $values,
'errors' => $errors,
'task' => $task,
'columns_list' => $this->board->getColumnsList($values['project_id']),
'users_list' => $this->projectPermission->getUsersList($values['project_id']),
'users_list' => $this->projectPermission->getMemberList($values['project_id']),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($values['project_id']),
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
'menu' => 'tasks',
'title' => t('Edit a task'),
'ajax' => $this->request->isAjax(),
)));
}
@ -250,9 +238,9 @@ class Task extends Base
$task = $this->getTask();
$values = $this->request->getValues();
list($valid, $errors) = $this->taskValidator->validateTimeModification($values);
list($valid,) = $this->taskValidator->validateTimeModification($values);
if ($valid && $this->task->update($values)) {
if ($valid && $this->taskModification->update($values)) {
$this->session->flash(t('Task updated successfully.'));
}
else {
@ -275,7 +263,7 @@ class Task extends Base
$this->checkCSRFParam();
if ($this->task->close($task['id'])) {
if ($this->taskStatus->close($task['id'])) {
$this->session->flash(t('Task closed successfully.'));
} else {
$this->session->flashError(t('Unable to close this task.'));
@ -284,10 +272,8 @@ class Task extends Base
$this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
}
$this->response->html($this->taskLayout('task_close', array(
$this->response->html($this->taskLayout('task/close', array(
'task' => $task,
'menu' => 'tasks',
'title' => t('Close a task')
)));
}
@ -304,7 +290,7 @@ class Task extends Base
$this->checkCSRFParam();
if ($this->task->open($task['id'])) {
if ($this->taskStatus->open($task['id'])) {
$this->session->flash(t('Task opened successfully.'));
} else {
$this->session->flashError(t('Unable to open this task.'));
@ -313,10 +299,8 @@ class Task extends Base
$this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
}
$this->response->html($this->taskLayout('task_open', array(
$this->response->html($this->taskLayout('task/open', array(
'task' => $task,
'menu' => 'tasks',
'title' => t('Open a task')
)));
}
@ -346,10 +330,8 @@ class Task extends Base
$this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
}
$this->response->html($this->taskLayout('task_remove', array(
$this->response->html($this->taskLayout('task/remove', array(
'task' => $task,
'menu' => 'tasks',
'title' => t('Remove a task')
)));
}
@ -365,7 +347,7 @@ class Task extends Base
if ($this->request->getStringParam('confirmation') === 'yes') {
$this->checkCSRFParam();
$task_id = $this->task->duplicateToSameProject($task);
$task_id = $this->taskDuplication->duplicate($task['id']);
if ($task_id) {
$this->session->flash(t('Task created successfully.'));
@ -376,10 +358,8 @@ class Task extends Base
}
}
$this->response->html($this->taskLayout('task_duplicate', array(
$this->response->html($this->taskLayout('task/duplicate', array(
'task' => $task,
'menu' => 'tasks',
'title' => t('Duplicate a task')
)));
}
@ -401,7 +381,7 @@ class Task extends Base
if ($valid) {
if ($this->task->update($values)) {
if ($this->taskModification->update($values)) {
$this->session->flash(t('Task updated successfully.'));
}
else {
@ -426,15 +406,13 @@ class Task extends Base
'errors' => $errors,
'task' => $task,
'ajax' => $ajax,
'menu' => 'tasks',
'title' => t('Edit the description'),
);
if ($ajax) {
$this->response->html($this->template->load('task_edit_description', $params));
$this->response->html($this->template->load('task/edit_description', $params));
}
else {
$this->response->html($this->taskLayout('task_edit_description', $params));
$this->response->html($this->taskLayout('task/edit_description', $params));
}
}
@ -444,31 +422,11 @@ class Task extends Base
* @access public
*/
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();
$values = $task;
$errors = array();
$projects_list = $this->projectPermission->getAllowedProjects($this->acl->getUserId());
$projects_list = $this->projectPermission->getMemberProjects($this->acl->getUserId());
unset($projects_list[$task['project_id']]);
@ -478,7 +436,46 @@ class Task extends Base
list($valid, $errors) = $this->taskValidator->validateProjectModification($values);
if ($valid) {
$task_id = $this->task->{$action.'ToAnotherProject'}($values['project_id'], $task);
if ($this->taskDuplication->moveToProject($task['id'], $values['project_id'])) {
$this->session->flash(t('Task updated successfully.'));
$this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
}
else {
$this->session->flashError(t('Unable to update your task.'));
}
}
}
$this->response->html($this->taskLayout('task/move_project', array(
'values' => $values,
'errors' => $errors,
'task' => $task,
'projects_list' => $projects_list,
)));
}
/**
* Duplicate a task to another project
*
* @access public
*/
public function copy()
{
$task = $this->getTask();
$values = $task;
$errors = array();
$projects_list = $this->projectPermission->getMemberProjects($this->acl->getUserId());
unset($projects_list[$task['project_id']]);
if ($this->request->isPost()) {
$values = $this->request->getValues();
list($valid, $errors) = $this->taskValidator->validateProjectModification($values);
if ($valid) {
$task_id = $this->taskDuplication->duplicateToProject($task['id'], $values['project_id']);
if ($task_id) {
$this->session->flash(t('Task created successfully.'));
$this->response->redirect('?controller=task&action=show&task_id='.$task_id);
@ -489,13 +486,11 @@ class Task extends Base
}
}
$this->response->html($this->taskLayout('task_'.$action.'_project', array(
$this->response->html($this->taskLayout('task/duplicate_project', array(
'values' => $values,
'errors' => $errors,
'task' => $task,
'projects_list' => $projects_list,
'menu' => 'tasks',
'title' => t(ucfirst($action).' the task to another project')
)));
}
}

View file

@ -28,15 +28,15 @@ class User extends Base
*
* @access public
*/
public function login()
public function login(array $values = array(), array $errors = array())
{
if ($this->acl->isLogged()) {
$this->response->redirect('?controller=app');
}
$this->response->html($this->template->layout('user_login', array(
'errors' => array(),
'values' => array(),
$this->response->html($this->template->layout('user/login', array(
'errors' => $errors,
'values' => $values,
'no_layout' => true,
'redirect_query' => $this->request->getStringParam('redirect_query'),
'title' => t('Login')
@ -63,13 +63,7 @@ class User extends Base
}
}
$this->response->html($this->template->layout('user_login', array(
'errors' => $errors,
'values' => $values,
'no_layout' => true,
'redirect_query' => $redirect_query,
'title' => t('Login')
)));
$this->login($values, $errors);
}
/**
@ -84,13 +78,13 @@ class User extends Base
{
$content = $this->template->load($template, $params);
$params['user_content_for_layout'] = $content;
$params['menu'] = 'users';
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->acl->getUserId());
if (isset($params['user'])) {
$params['title'] = $params['user']['name'] ?: $params['user']['username'];
$params['title'] = ($params['user']['name'] ?: $params['user']['username']).' (#'.$params['user']['id'].')';
}
return $this->template->layout('user_layout', $params);
return $this->template->layout('user/layout', $params);
}
/**
@ -130,11 +124,11 @@ class User extends Base
$nb_users = $this->user->count();
$this->response->html(
$this->template->layout('user_index', array(
$this->template->layout('user/index', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->acl->getUserId()),
'projects' => $this->project->getList(),
'nb_users' => $nb_users,
'users' => $users,
'menu' => 'users',
'title' => t('Users').' ('.$nb_users.')',
'pagination' => array(
'controller' => 'user',
@ -154,13 +148,13 @@ class User extends Base
*
* @access public
*/
public function create()
public function create(array $values = array(), array $errors = array())
{
$this->response->html($this->template->layout('user_new', array(
$this->response->html($this->template->layout('user/new', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->acl->getUserId()),
'projects' => $this->project->getList(),
'errors' => array(),
'values' => array(),
'menu' => 'users',
'errors' => $errors,
'values' => $values,
'title' => t('New user')
)));
}
@ -186,13 +180,7 @@ class User extends Base
}
}
$this->response->html($this->template->layout('user_new', array(
'projects' => $this->project->getList(),
'errors' => $errors,
'values' => $values,
'menu' => 'users',
'title' => t('New user')
)));
$this->create($values, $errors);
}
/**
@ -203,7 +191,7 @@ class User extends Base
public function show()
{
$user = $this->getUser();
$this->response->html($this->layout('user_show', array(
$this->response->html($this->layout('user/show', array(
'projects' => $this->projectPermission->getAllowedProjects($user['id']),
'user' => $user,
)));
@ -217,7 +205,7 @@ class User extends Base
public function last()
{
$user = $this->getUser();
$this->response->html($this->layout('user_last', array(
$this->response->html($this->layout('user/last', array(
'last_logins' => $this->lastLogin->getAll($user['id']),
'user' => $user,
)));
@ -231,7 +219,7 @@ class User extends Base
public function sessions()
{
$user = $this->getUser();
$this->response->html($this->layout('user_sessions', array(
$this->response->html($this->layout('user/sessions', array(
'sessions' => $this->authentication->backend('rememberMe')->getAll($user['id']),
'user' => $user,
)));
@ -266,7 +254,7 @@ class User extends Base
$this->response->redirect('?controller=user&action=notifications&user_id='.$user['id']);
}
$this->response->html($this->layout('user_notifications', array(
$this->response->html($this->layout('user/notifications', array(
'projects' => $this->projectPermission->getAllowedProjects($user['id']),
'notifications' => $this->notification->readSettings($user['id']),
'user' => $user,
@ -281,7 +269,7 @@ class User extends Base
public function external()
{
$user = $this->getUser();
$this->response->html($this->layout('user_external', array(
$this->response->html($this->layout('user/external', array(
'last_logins' => $this->lastLogin->getAll($user['id']),
'user' => $user,
)));
@ -316,7 +304,7 @@ class User extends Base
}
}
$this->response->html($this->layout('user_password', array(
$this->response->html($this->layout('user/password', array(
'values' => $values,
'errors' => $errors,
'user' => $user,
@ -365,7 +353,7 @@ class User extends Base
}
}
$this->response->html($this->layout('user_edit', array(
$this->response->html($this->layout('user/edit', array(
'values' => $values,
'errors' => $errors,
'projects' => $this->projectPermission->filterProjects($this->project->getList(), $user['id']),
@ -395,7 +383,7 @@ class User extends Base
$this->response->redirect('?controller=user');
}
$this->response->html($this->layout('user_remove', array(
$this->response->html($this->layout('user/remove', array(
'user' => $user,
)));
}
@ -431,7 +419,7 @@ class User extends Base
$this->response->redirect('?controller=app');
}
else {
$this->response->html($this->template->layout('user_login', array(
$this->response->html($this->template->layout('user/login', array(
'errors' => array('login' => t('Google authentication failed')),
'values' => array(),
'no_layout' => true,
@ -493,7 +481,7 @@ class User extends Base
$this->response->redirect('?controller=app');
}
else {
$this->response->html($this->template->layout('user_login', array(
$this->response->html($this->template->layout('user/login', array(
'errors' => array('login' => t('GitHub authentication failed')),
'values' => array(),
'no_layout' => true,

View file

@ -35,7 +35,7 @@ class Webhook extends Base
list($valid,) = $this->taskValidator->validateCreation($values);
if ($valid && $this->task->create($values)) {
if ($valid && $this->taskCreation->create($values)) {
$this->response->text('OK');
}
@ -57,7 +57,7 @@ class Webhook extends Base
$result = $this->githubWebhook->parsePayload(
$this->request->getHeader('X-Github-Event'),
$this->request->getBody()
$this->request->getJson()
);
echo $result ? 'PARSED' : 'IGNORED';

View file

@ -1,75 +0,0 @@
<?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

@ -69,7 +69,7 @@ class Event
{
if (! $this->isEventTriggered($eventName)) {
$this->events[] = $eventName;
$this->events[$eventName] = $data;
if (isset($this->listeners[$eventName])) {
@ -118,6 +118,17 @@ class Event
return $this->events;
}
/**
* Get a list of triggered events
*
* @access public
* @return array
*/
public function getEventData($eventName)
{
return isset($this->events[$eventName]) ? $this->events[$eventName] : array();
}
/**
* Check if an event have been triggered
*
@ -127,7 +138,7 @@ class Event
*/
public function isEventTriggered($eventName)
{
return in_array($eventName, $this->events);
return isset($this->events[$eventName]);
}
/**

View file

@ -1,62 +0,0 @@
<?php
namespace Core;
/**
* Loader class
*
* @package core
* @author Frederic Guillot
*/
class Loader
{
/**
* List of paths
*
* @access private
* @var array
*/
private $paths = array();
/**
* Load the missing class
*
* @access public
* @param string $class Class name with namespace
*/
public function load($class)
{
foreach ($this->paths as $path) {
$filename = $path.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php';
if (file_exists($filename)) {
require $filename;
break;
}
}
}
/**
* Register the autoloader
*
* @access public
*/
public function execute()
{
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,83 +0,0 @@
<?php
namespace Core;
use RuntimeException;
/**
* The registry class is a dependency injection container
*
* @property mixed db
* @property mixed event
* @package core
* @author Frederic Guillot
*/
class Registry
{
/**
* Contains all dependencies
*
* @access private
* @var array
*/
private $container = array();
/**
* Contains all instances
*
* @access private
* @var array
*/
private $instances = array();
/**
* Set a dependency
*
* @access public
* @param string $name Unique identifier for the service/parameter
* @param mixed $value The value of the parameter or a closure to define an object
*/
public function __set($name, $value)
{
$this->container[$name] = $value;
}
/**
* Get a dependency
*
* @access public
* @param string $name Unique identifier for the service/parameter
* @return mixed The value of the parameter or an object
* @throws RuntimeException If the identifier is not found
*/
public function __get($name)
{
if (isset($this->container[$name])) {
if (is_callable($this->container[$name])) {
return $this->container[$name]();
}
else {
return $this->container[$name];
}
}
throw new \RuntimeException('Identifier not found in the registry: '.$name);
}
/**
* Return a shared instance of a dependency
*
* @access public
* @param string $name Unique identifier for the service/parameter
* @return mixed Same object instance of the dependency
*/
public function shared($name)
{
if (! isset($this->instances[$name])) {
$this->instances[$name] = $this->$name;
}
return $this->instances[$name];
}
}

View file

@ -75,6 +75,17 @@ class Request
return file_get_contents('php://input');
}
/**
* Get the Json request body
*
* @access public
* @return array
*/
public function getJson()
{
return json_decode($this->getBody(), true);
}
/**
* Get the content of an uploaded file
*
@ -113,6 +124,20 @@ class Request
return $this->getHeader('X-Requested-With') === 'XMLHttpRequest';
}
/**
* 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';
}
/**
* Return a HTTP header value
*

View file

@ -246,7 +246,7 @@ class Response
*/
public function hsts()
{
if (Tool::isHTTPS()) {
if (Request::isHTTPS()) {
header('Strict-Transport-Security: max-age=31536000');
}
}

View file

@ -2,6 +2,8 @@
namespace Core;
use Pimple\Container;
/**
* Router class
*
@ -27,24 +29,24 @@ class Router
private $action = '';
/**
* Registry instance
* Container instance
*
* @access private
* @var \Core\Registry
* @var \Pimple\Container
*/
private $registry;
private $container;
/**
* Constructor
*
* @access public
* @param Registry $registry Registry instance
* @param string $controller Controller name
* @param string $action Action name
* @param \Pimple\Container $container Container instance
* @param string $controller Controller name
* @param string $action Action name
*/
public function __construct(Registry $registry, $controller = '', $action = '')
public function __construct(Container $container, $controller = '', $action = '')
{
$this->registry = $registry;
$this->container = $container;
$this->controller = empty($_GET['controller']) ? $controller : $_GET['controller'];
$this->action = empty($_GET['action']) ? $action : $_GET['action'];
}
@ -81,11 +83,7 @@ class Router
return false;
}
$instance = new $class($this->registry);
$instance->request = new Request;
$instance->response = new Response;
$instance->session = new Session;
$instance->template = new Template;
$instance = new $class($this->container);
$instance->beforeAction($this->controller, $this->action);
$instance->$method();

View file

@ -36,32 +36,30 @@ class Session
*
* @access public
* @param string $base_path Cookie path
* @param string $save_path Custom session save path
*/
public function open($base_path = '/', $save_path = '')
public function open($base_path = '/')
{
if ($save_path !== '') {
session_save_path($save_path);
}
// HttpOnly and secure flags for session cookie
session_set_cookie_params(
self::SESSION_LIFETIME,
$base_path ?: '/',
null,
Tool::isHTTPS(),
Request::isHTTPS(),
true
);
// Avoid session id in the URL
ini_set('session.use_only_cookies', '1');
// Enable strict mode
ini_set('session.use_strict_mode', '1');
// Ensure session ID integrity
ini_set('session.entropy_file', '/dev/urandom');
ini_set('session.entropy_length', '32');
ini_set('session.hash_bits_per_character', 6);
// If session was autostarted with session.auto_start = 1 in php.ini destroy it, otherwise we cannot login
// If session was autostarted with session.auto_start = 1 in php.ini destroy it
if (isset($_SESSION)) {
session_destroy();
}

View file

@ -2,6 +2,8 @@
namespace Core;
use Pimple\Container;
/**
* Tool class
*
@ -37,31 +39,17 @@ class Tool
*
* @static
* @access public
* @param Core\Registry $registry DPI container
* @param string $name Model name
* @param Pimple\Container $container Container instance
* @param string $name Model name
* @return mixed
*/
public static function loadModel(Registry $registry, $name)
public static function loadModel(Container $container, $name)
{
if (! isset($registry->$name)) {
if (! isset($container[$name])) {
$class = '\Model\\'.ucfirst($name);
$registry->$name = new $class($registry);
$container[$name] = new $class($container);
}
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';
return $container[$name];
}
}

View file

@ -2,8 +2,8 @@
namespace Event;
use Pimple\Container;
use Core\Listener;
use Core\Registry;
use Core\Tool;
/**
@ -22,22 +22,22 @@ use Core\Tool;
abstract class Base implements Listener
{
/**
* Registry instance
* Container instance
*
* @access protected
* @var \Core\Registry
* @var \Pimple\Container
*/
protected $registry;
protected $container;
/**
* Constructor
*
* @access public
* @param \Core\Registry $registry Regsitry instance
* @param \Pimple\Container $container
*/
public function __construct(Registry $registry)
public function __construct(Container $container)
{
$this->registry = $registry;
$this->container = $container;
}
/**
@ -60,7 +60,7 @@ abstract class Base implements Listener
*/
public function __get($name)
{
return Tool::loadModel($this->registry, $name);
return Tool::loadModel($this->container, $name);
}
/**
@ -73,7 +73,7 @@ abstract class Base implements Listener
*/
public function getEventNamespace()
{
$event_name = $this->registry->event->getLastTriggeredEvent();
$event_name = $this->container['event']->getLastTriggeredEvent();
return substr($event_name, 0, strpos($event_name, '.'));
}
}

View file

@ -27,7 +27,7 @@ class ProjectActivityListener extends Base
$values['task']['project_id'],
$values['task']['id'],
$this->acl->getUserId(),
$this->registry->event->getLastTriggeredEvent(),
$this->container['event']->getLastTriggeredEvent(),
$values
);
}

View file

@ -0,0 +1,28 @@
<?php
namespace Event;
/**
* Project daily summary listener
*
* @package event
* @author Frederic Guillot
*/
class ProjectDailySummaryListener extends Base
{
/**
* 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)
{
if (isset($data['project_id'])) {
return $this->projectDailySummary->updateTotals($data['project_id'], date('Y-m-d'));
}
return false;
}
}

View file

@ -187,6 +187,7 @@ return array(
'Complexity' => 'Kompleksitet',
'limit' => 'Begrænsning',
'Task limit' => 'Opgave begrænsning',
// 'Task count' => '',
'This value must be greater than %d' => 'Denne værdi skal være større end %d',
'Edit project access list' => 'Rediger adgangstilladelser for projektet',
'Edit users access' => 'Rediger brugertilladelser',
@ -558,4 +559,47 @@ return array(
// 'Help on Github webhook' => '',
// 'Create a comment from an external provider' => '',
// 'Github issue comment created' => '',
// 'Configure' => '',
// 'Project management' => '',
// 'My projects' => '',
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
// 'Subtask' => '',
// 'My subtasks' => '',
// 'User repartition' => '',
// 'User repartition for "%s"' => '',
// 'Clone this project' => '',
// 'Column removed successfully.' => '',
// 'Edit Project' => '',
// 'Github Issue' => '',
// 'Not enough data to show the graph.' => '',
// 'Previous' => '',
// 'The id must be an integer' => '',
// 'The project id must be an integer' => '',
// 'The status must be an integer' => '',
// 'The subtask id is required' => '',
// 'The subtask id must be an integer' => '',
// 'The task id is required' => '',
// 'The task id must be an integer' => '',
// 'The user id must be an integer' => '',
// 'This value is required' => '',
// 'This value must be numeric' => '',
// 'Unable to create this task.' => '',
// 'Cumulative flow diagram' => '',
// 'Cumulative flow diagram for "%s"' => '',
// 'Daily project summary' => '',
// 'Daily project summary export' => '',
// 'Daily project summary export for "%s"' => '',
// 'Exports' => '',
// 'This export contains the number of tasks per column grouped per day.' => '',
// 'Nothing to preview...' => '',
// 'Preview' => '',
// 'Write' => '',
);

View file

@ -57,7 +57,7 @@ return array(
'Column %d' => 'Spalte %d',
'Add this column' => 'Diese Spalte hinzufügen',
'%d tasks on the board' => '%d Aufgaben auf dieser Pinnwand',
'%d tasks in total' => '%d Aufgaben gesamt',
'%d tasks in total' => '%d Aufgaben insgesamt',
'Unable to update this board.' => 'Ändern dieser Pinnwand nicht möglich.',
'Edit board' => 'Pinnwand bearbeiten',
'Disable' => 'Deaktivieren',
@ -94,7 +94,7 @@ return array(
'User settings' => 'Benutzereinstellungen',
'My default project:' => 'Standardprojekt:',
'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 geschlossen werden: "%s"?',
'Edit a task' => 'Aufgabe bearbeiten',
'Column' => 'Spalte',
'Color' => 'Farbe',
@ -108,8 +108,8 @@ return array(
'There is nobody assigned' => 'Die Aufgabe wurde niemandem zugewiesen',
'Column on the board:' => 'Spalte:',
'Status is open' => 'Status ist geöffnet',
'Status is closed' => 'Status ist abgeschlossen',
'Close this task' => 'Aufgabe abschließen',
'Status is closed' => 'Status ist geschlossen',
'Close this task' => 'Aufgabe schließen',
'Open this task' => 'Aufgabe wieder öffnen',
'There is no description.' => 'Keine Beschreibung vorhanden.',
'Add a new task' => 'Neue Aufgabe hinzufügen',
@ -147,9 +147,9 @@ return array(
'Project disabled successfully.' => 'Projekt erfolgreich deaktiviert.',
'Unable to disable this project.' => 'Deaktivieren des Projekts 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 geöffnet.',
'Unable to close this task.' => 'Abschließen der Aufgabe nicht möglich.',
'Task closed successfully.' => 'Aufgabe erfolgreich abgeschlossen.',
'Task closed successfully.' => 'Aufgabe erfolgreich geschlossen.',
'Unable to update your task.' => 'Aktualisieren der Aufgabe nicht möglich.',
'Task updated successfully.' => 'Aufgabe erfolgreich aktualisiert.',
'Unable to create your task.' => 'Erstellen der Aufgabe nicht möglich.',
@ -187,6 +187,7 @@ return array(
'Complexity' => 'Komplexität',
'limit' => 'Limit',
'Task limit' => 'Maximale Anzahl von Aufgaben',
'Task count' => 'Aufgabenanzahl',
'This value must be greater than %d' => 'Dieser Wert muss größer sein als %d',
'Edit project access list' => 'Zugriffsberechtigungen des Projektes bearbeiten',
'Edit users access' => 'Benutzerzugriff ändern',
@ -342,27 +343,27 @@ return array(
'Time tracking' => 'Zeiterfassung',
'Estimate:' => 'Geschätzt:',
'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 Teilaufgabe wirklich gelöscht werden: "%s"?',
'Remaining:' => 'Verbleibend:',
'hours' => 'Stunden',
'spent' => 'aufgewendet',
'estimated' => 'geschätzt',
'Sub-Tasks' => 'Unteraufgaben',
'Add a sub-task' => 'Unteraufgabe anlegen',
'Sub-Tasks' => 'Teilaufgaben',
'Add a sub-task' => 'Teilaufgabe anlegen',
'Original estimate' => 'Geschätzter Aufwand',
'Create another sub-task' => 'Weitere Unteraufgabe anlegen',
'Create another sub-task' => 'Weitere Teilaufgabe anlegen',
'Time spent' => 'Aufgewendete Zeit',
'Edit a sub-task' => 'Unteraufgabe bearbeiten',
'Remove a sub-task' => 'Unteraufgabe löschen',
'Edit a sub-task' => 'Teilaufgabe bearbeiten',
'Remove a sub-task' => 'Teilaufgabe löschen',
'The time must be a numeric value' => 'Zeit nur als nummerische Angabe',
'Todo' => 'Nicht gestartet',
'In progress' => 'In Bearbeitung',
'Sub-task removed successfully.' => 'Unteraufgabe erfolgreich gelöscht.',
'Unable to remove this sub-task.' => 'Löschen der Unteraufgabe nicht möglich.',
'Sub-task updated successfully.' => 'Unteraufgabe erfolgreich aktualisiert.',
'Unable to update your sub-task.' => 'Aktualisieren der Unteraufgabe nicht möglich.',
'Unable to create your sub-task.' => 'Erstellen der Unteraufgabe nicht möglich.',
'Sub-task added successfully.' => 'Unteraufgabe erfolgreich angelegt.',
'Sub-task removed successfully.' => 'Teilaufgabe erfolgreich gelöscht.',
'Unable to remove this sub-task.' => 'Löschen der Teilaufgabe nicht möglich.',
'Sub-task updated successfully.' => 'Teilaufgabe erfolgreich aktualisiert.',
'Unable to update your sub-task.' => 'Aktualisieren der Teilaufgabe nicht möglich.',
'Unable to create your sub-task.' => 'Erstellen der Teilaufgabe nicht möglich.',
'Sub-task added successfully.' => 'Teilaufgabe erfolgreich angelegt.',
'Maximum size: ' => 'Maximalgröße: ',
'Unable to upload the file.' => 'Hochladen der Datei nicht möglich.',
'Display another project' => 'Zu Projekt wechseln...',
@ -396,12 +397,12 @@ return array(
'Task position:' => 'Position der Aufgabe',
'The task #%d have been opened.' => 'Die Aufgabe #%d wurde geöffnet.',
'The task #%d have been closed.' => 'Die Aufgabe #%d wurde geschlossen.',
'Sub-task updated' => 'Unteraufgabe aktualisiert',
'Sub-task updated' => 'Teilaufgabe aktualisiert',
'Title:' => 'Titel',
'Status:' => 'Status',
'Assignee:' => 'Zuständigkeit:',
'Time tracking:' => 'Zeittracking',
'New sub-task' => 'Neue Unteraufgabe',
'New sub-task' => 'Neue Teilaufgabe',
'New attachment added "%s"' => 'Neuer Anhang "%s" wurde hinzugefügt.',
'Comment updated' => 'Kommentar wurde aktualisiert',
'New comment posted by %s' => 'Neuer Kommentar verfasst durch %s',
@ -409,8 +410,8 @@ return array(
'[%s][New attachment] %s (#%d)' => '[%s][Neuer Anhang] %s (#%d)',
'[%s][New comment] %s (#%d)' => '[%s][Neuer Kommentar] %s (#%d)',
'[%s][Comment updated] %s (#%d)' => '[%s][Kommentar aktualisisiert] %s (#%d)',
'[%s][New subtask] %s (#%d)' => '[%s][Neue Unteraufgabe] %s (#%d)',
'[%s][Subtask updated] %s (#%d)' => '[%s][Unteraufgabe aktualisisert] %s (#%d)',
'[%s][New subtask] %s (#%d)' => '[%s][Neue Teilaufgabe] %s (#%d)',
'[%s][Subtask updated] %s (#%d)' => '[%s][Teilaufgabe aktualisisert] %s (#%d)',
'[%s][New task] %s (#%d)' => '[%s][Neue Aufgabe] %s (#%d)',
'[%s][Task updated] %s (#%d)' => '[%s][Aufgabe aktualisiert] %s (#%d)',
'[%s][Task closed] %s (#%d)' => '[%s][Aufgabe geschlossen] %s (#%d)',
@ -473,8 +474,8 @@ return array(
'%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '%s hat die Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> in die Spalte "%s" verschoben',
'%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s hat die Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> angelegt',
'%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s hat die Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> geschlossen',
'%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s hat eine Unteraufgabe für die Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> angelegt',
'%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s hat eine Unteraufgabe der Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> verändert',
'%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s hat eine Teilaufgabe für die Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> angelegt',
'%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s hat eine Teilaufgabe der Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> verändert',
'Assigned to %s with an estimate of %s/%sh' => 'An %s zugewiesen mit einer Schätzung von %s/%s Stunden',
'Not assigned, estimate of %sh' => 'Nicht zugewiesen, Schätzung von %s Stunden',
'%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s hat einen Kommentat der Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> aktualisiert',
@ -484,8 +485,8 @@ return array(
'RSS feed' => 'RSS Feed',
'%s updated a comment on the task #%d' => '%s hat einen Kommentar der Aufgabe #%d aktualisiert',
'%s commented on the task #%d' => '%s hat die Aufgabe #%d kommentiert',
'%s updated a subtask for the task #%d' => '%s hat eine Unteraufgabe der Aufgabe #%d aktualisiert',
'%s created a subtask for the task #%d' => '%s hat eine Unteraufgabe der Aufgabe #%d angelegt',
'%s updated a subtask for the task #%d' => '%s hat eine Teilaufgabe der Aufgabe #%d aktualisiert',
'%s created a subtask for the task #%d' => '%s hat eine Teilaufgabe der Aufgabe #%d angelegt',
'%s updated the task #%d' => '%s hat die Aufgabe #%d aktualisiert',
'%s created the task #%d' => '%s hat die Aufgabe #%d angelegt',
'%s closed the task #%d' => '%s hat die Aufgabe #%d geschlossen',
@ -537,7 +538,7 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO Format wird immer akzeptiert, z.B.: "%s" und "%s"',
'New private project' => 'Neues privates Projekt',
'This project is private' => 'Dieses Projekt ist privat',
'Type here to create a new sub-task' => 'Hier tippen, um eine neue Unteraufgabe zu erstellen',
'Type here to create a new sub-task' => 'Hier tippen, um eine neue Teilaufgabe zu erstellen',
'Add' => 'Hinzufügen',
'Estimated time: %s hours' => 'Geplante Zeit: %s Stunden',
'Time spent: %s hours' => 'Aufgewendete Zeit: %s Stunden',
@ -558,4 +559,47 @@ return array(
'Help on Github webhook' => 'Hilfe bei einem Github Webhook',
'Create a comment from an external provider' => 'Kommentar eines externen Providers hinzufügen',
'Github issue comment created' => 'Github Fehler Kommentar hinzugefügt',
'Configure' => 'konfigurieren',
'Project management' => 'Projektmanagement',
'My projects' => 'Meine Projekte',
'Columns' => 'Spalten',
'Task' => 'Aufgabe',
'Your are not member of any project.' => 'Sie sind nicht Mitglied eines Projekts.',
'Percentage' => 'Prozentsatz',
'Number of tasks' => 'Anzahl an Aufgaben',
'Task distribution' => 'Aufgabenverteilung',
'Reportings' => 'Berichte',
'Task repartition for "%s"' => 'Aufgabenzuweisung für "%s"',
'Analytics' => 'Analyse',
'Subtask' => 'Teilaufgabe',
'My subtasks' => 'Meine Teilaufgaben',
'User repartition' => 'Benutzerverteilung',
'User repartition for "%s"' => 'Benutzerverteilung für "%s"',
'Clone this project' => 'Projekt kopieren',
'Column removed successfully.' => 'Spalte erfolgreich entfernt.',
'Edit Project' => 'Projekt bearbeiten',
'Github Issue' => 'Github Issue',
'Not enough data to show the graph.' => 'Nicht genügend Daten, um die Grafik zu zeigen.',
'Previous' => 'Vorherige',
'The id must be an integer' => 'Die Id muss eine ganze Zahl sein',
'The project id must be an integer' => 'Der Projektid muss eine ganze Zahl sein',
'The status must be an integer' => 'Der Status muss eine ganze Zahl sein',
'The subtask id is required' => 'Die Teilaufgabenid ist benötigt',
'The subtask id must be an integer' => 'Die Teilaufgabenid muss eine ganze Zahl sein',
'The task id is required' => 'Die Aufgabenid ist benötigt',
'The task id must be an integer' => 'Die Aufgabenid muss eine ganze Zahl sein',
'The user id must be an integer' => 'Die Userid muss eine ganze Zahl sein',
'This value is required' => 'Dieser Wert ist erforderlich',
'This value must be numeric' => 'Dieser Wert muss numerisch sein',
'Unable to create this task.' => 'Diese Aufgabe kann nicht erstellt werden',
'Cumulative flow diagram' => 'Kumulatives Flussdiagramm',
'Cumulative flow diagram for "%s"' => 'Kumulatives Flussdiagramm für "%s"',
'Daily project summary' => 'Tägliche Projektzusammenfassung',
'Daily project summary export' => 'Export der täglichen Projektzusammenfassung',
'Daily project summary export for "%s"' => 'Export der täglichen Projektzusammenfassung für "%s"',
'Exports' => 'Exporte',
'This export contains the number of tasks per column grouped per day.' => 'Dieser Export enthält die Anzahl der Aufgaben pro Spalte nach Tagen gruppiert.',
'Nothing to preview...' => 'Nichts in der Vorschau anzuzeigen ...',
'Preview' => 'Vorschau',
'Write' => 'Ändern',
);

View file

@ -187,6 +187,7 @@ return array(
'Complexity' => 'Complejidad',
'limit' => 'límite',
'Task limit' => 'Número máximo de tareas',
// 'Task count' => '',
'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 users access' => 'Editar los permisos de usuario',
@ -558,4 +559,47 @@ return array(
// 'Help on Github webhook' => '',
// 'Create a comment from an external provider' => '',
// 'Github issue comment created' => '',
// 'Configure' => '',
// 'Project management' => '',
// 'My projects' => '',
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
// 'Subtask' => '',
// 'My subtasks' => '',
// 'User repartition' => '',
// 'User repartition for "%s"' => '',
// 'Clone this project' => '',
// 'Column removed successfully.' => '',
// 'Edit Project' => '',
// 'Github Issue' => '',
// 'Not enough data to show the graph.' => '',
// 'Previous' => '',
// 'The id must be an integer' => '',
// 'The project id must be an integer' => '',
// 'The status must be an integer' => '',
// 'The subtask id is required' => '',
// 'The subtask id must be an integer' => '',
// 'The task id is required' => '',
// 'The task id must be an integer' => '',
// 'The user id must be an integer' => '',
// 'This value is required' => '',
// 'This value must be numeric' => '',
// 'Unable to create this task.' => '',
// 'Cumulative flow diagram' => '',
// 'Cumulative flow diagram for "%s"' => '',
// 'Daily project summary' => '',
// 'Daily project summary export' => '',
// 'Daily project summary export for "%s"' => '',
// 'Exports' => '',
// 'This export contains the number of tasks per column grouped per day.' => '',
// 'Nothing to preview...' => '',
// 'Preview' => '',
// 'Write' => '',
);

View file

@ -187,6 +187,7 @@ return array(
'Complexity' => 'Monimutkaisuus',
'limit' => 'raja',
'Task limit' => 'Tehtävien maksimimäärä',
// 'Task count' => '',
'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ä',
@ -558,4 +559,47 @@ return array(
// 'Help on Github webhook' => '',
// 'Create a comment from an external provider' => '',
// 'Github issue comment created' => '',
// 'Configure' => '',
// 'Project management' => '',
// 'My projects' => '',
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
// 'Subtask' => '',
// 'My subtasks' => '',
// 'User repartition' => '',
// 'User repartition for "%s"' => '',
// 'Clone this project' => '',
// 'Column removed successfully.' => '',
// 'Edit Project' => '',
// 'Github Issue' => '',
// 'Not enough data to show the graph.' => '',
// 'Previous' => '',
// 'The id must be an integer' => '',
// 'The project id must be an integer' => '',
// 'The status must be an integer' => '',
// 'The subtask id is required' => '',
// 'The subtask id must be an integer' => '',
// 'The task id is required' => '',
// 'The task id must be an integer' => '',
// 'The user id must be an integer' => '',
// 'This value is required' => '',
// 'This value must be numeric' => '',
// 'Unable to create this task.' => '',
// 'Cumulative flow diagram' => '',
// 'Cumulative flow diagram for "%s"' => '',
// 'Daily project summary' => '',
// 'Daily project summary export' => '',
// 'Daily project summary export for "%s"' => '',
// 'Exports' => '',
// 'This export contains the number of tasks per column grouped per day.' => '',
// 'Nothing to preview...' => '',
// 'Preview' => '',
// 'Write' => '',
);

View file

@ -187,6 +187,7 @@ return array(
'Complexity' => 'Complexité',
'limit' => 'limite',
'Task limit' => 'Nombre maximum de tâches',
'Task count' => 'Nombre de tâches',
'This value must be greater than %d' => 'Cette valeur doit être plus grande que %d',
'Edit project access list' => 'Modifier l\'accès au projet',
'Edit users access' => 'Modifier les utilisateurs autorisés',
@ -456,7 +457,7 @@ return array(
'Edit profile' => 'Modifier le profil',
'Change password' => 'Changer le mot de passe',
'Password modification' => 'Changement de mot de passe',
'External authentications' => 'Authentifications externe',
'External authentications' => 'Authentifications externes',
'Google Account' => 'Compte Google',
'Github Account' => 'Compte Github',
'Never connected.' => 'Jamais connecté.',
@ -558,4 +559,47 @@ return array(
'Help on Github webhook' => 'Aide sur les webhooks Github',
'Create a comment from an external provider' => 'Créer un commentaire depuis un fournisseur externe',
'Github issue comment created' => 'Commentaire créé sur un ticket Github',
'Configure' => 'Configurer',
'Project management' => 'Gestion des projets',
'My projects' => 'Mes projets',
'Columns' => 'Colonnes',
'Task' => 'Tâche',
'Your are not member of any project.' => 'Vous n\'êtes membre d\'aucun projet.',
'Percentage' => 'Pourcentage',
'Number of tasks' => 'Nombre de tâches',
'Task distribution' => 'Répartition des tâches',
'Reportings' => 'Rapports',
'Task repartition for "%s"' => 'Répartition des tâches pour « %s »',
'Analytics' => 'Analytique',
'Subtask' => 'Sous-tâche',
'My subtasks' => 'Mes sous-tâches',
'User repartition' => 'Répartition des utilisateurs',
'User repartition for "%s"' => 'Répartition des utilisateurs pour « %s »',
'Clone this project' => 'Cloner ce projet',
'Column removed successfully.' => 'Colonne supprimée avec succès.',
'Edit Project' => 'Modifier le projet',
'Github Issue' => 'Ticket Github',
'Not enough data to show the graph.' => 'Pas assez de données pour afficher le graphique.',
'Previous' => 'Précédent',
'The id must be an integer' => 'L\'id doit être un entier',
'The project id must be an integer' => 'L\'id du projet doit être un entier',
'The status must be an integer' => 'Le status doit être un entier',
'The subtask id is required' => 'L\'id de la sous-tâche est obligatoire',
'The subtask id must be an integer' => 'L\'id de la sous-tâche doit être en entier',
'The task id is required' => 'L\'id de la tâche est obligatoire',
'The task id must be an integer' => 'L\'id de la tâche doit être en entier',
'The user id must be an integer' => 'L\'id de l\'utilisateur doit être en entier',
'This value is required' => 'Cette valeur est obligatoire',
'This value must be numeric' => 'Cette valeur doit être numérique',
'Unable to create this task.' => 'Impossible de créer cette tâche',
'Cumulative flow diagram' => 'Diagramme de flux cumulé',
'Cumulative flow diagram for "%s"' => 'Diagramme de flux cumulé pour « %s »',
'Daily project summary' => 'Résumé journalier du projet',
'Daily project summary export' => 'Export du résumé journalier du projet',
'Daily project summary export for "%s"' => 'Export du résumé quotidien du projet pour « %s »',
'Exports' => 'Exports',
'This export contains the number of tasks per column grouped per day.' => 'Cet export contient le nombre de tâches par colonne groupé par jour.',
'Nothing to preview...' => 'Rien à prévisualiser...',
'Preview' => 'Prévisualiser',
'Write' => 'Écrire',
);

View file

@ -187,6 +187,7 @@ return array(
// 'Complexity' => '',
'limit' => 'limite',
'Task limit' => 'Numero massimo di compiti',
// 'Task count' => '',
'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',
@ -558,4 +559,47 @@ return array(
// 'Help on Github webhook' => '',
// 'Create a comment from an external provider' => '',
// 'Github issue comment created' => '',
// 'Configure' => '',
// 'Project management' => '',
// 'My projects' => '',
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
// 'Subtask' => '',
// 'My subtasks' => '',
// 'User repartition' => '',
// 'User repartition for "%s"' => '',
// 'Clone this project' => '',
// 'Column removed successfully.' => '',
// 'Edit Project' => '',
// 'Github Issue' => '',
// 'Not enough data to show the graph.' => '',
// 'Previous' => '',
// 'The id must be an integer' => '',
// 'The project id must be an integer' => '',
// 'The status must be an integer' => '',
// 'The subtask id is required' => '',
// 'The subtask id must be an integer' => '',
// 'The task id is required' => '',
// 'The task id must be an integer' => '',
// 'The user id must be an integer' => '',
// 'This value is required' => '',
// 'This value must be numeric' => '',
// 'Unable to create this task.' => '',
// 'Cumulative flow diagram' => '',
// 'Cumulative flow diagram for "%s"' => '',
// 'Daily project summary' => '',
// 'Daily project summary export' => '',
// 'Daily project summary export for "%s"' => '',
// 'Exports' => '',
// 'This export contains the number of tasks per column grouped per day.' => '',
// 'Nothing to preview...' => '',
// 'Preview' => '',
// 'Write' => '',
);

View file

@ -187,6 +187,7 @@ return array(
'Complexity' => '複雑さ',
'limit' => '制限',
'Task limit' => 'タスク数制限',
// 'Task count' => '',
'This value must be greater than %d' => '%d より大きな値を入力してください',
'Edit project access list' => 'プロジェクトのアクセス許可を変更',
'Edit users access' => 'ユーザのアクセス許可を変更',
@ -558,4 +559,47 @@ return array(
// 'Help on Github webhook' => '',
// 'Create a comment from an external provider' => '',
// 'Github issue comment created' => '',
// 'Configure' => '',
// 'Project management' => '',
// 'My projects' => '',
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
// 'Subtask' => '',
// 'My subtasks' => '',
// 'User repartition' => '',
// 'User repartition for "%s"' => '',
// 'Clone this project' => '',
// 'Column removed successfully.' => '',
// 'Edit Project' => '',
// 'Github Issue' => '',
// 'Not enough data to show the graph.' => '',
// 'Previous' => '',
// 'The id must be an integer' => '',
// 'The project id must be an integer' => '',
// 'The status must be an integer' => '',
// 'The subtask id is required' => '',
// 'The subtask id must be an integer' => '',
// 'The task id is required' => '',
// 'The task id must be an integer' => '',
// 'The user id must be an integer' => '',
// 'This value is required' => '',
// 'This value must be numeric' => '',
// 'Unable to create this task.' => '',
// 'Cumulative flow diagram' => '',
// 'Cumulative flow diagram for "%s"' => '',
// 'Daily project summary' => '',
// 'Daily project summary export' => '',
// 'Daily project summary export for "%s"' => '',
// 'Exports' => '',
// 'This export contains the number of tasks per column grouped per day.' => '',
// 'Nothing to preview...' => '',
// 'Preview' => '',
// 'Write' => '',
);

View file

@ -187,6 +187,7 @@ return array(
'Complexity' => 'Poziom trudności',
'limit' => 'limit',
'Task limit' => 'Limit zadań',
// 'Task count' => '',
'This value must be greater than %d' => 'Wartość musi być większa niż %d',
'Edit project access list' => 'Edycja list dostępu dla projektu',
'Edit users access' => 'Edytuj dostęp',
@ -558,4 +559,47 @@ return array(
// 'Help on Github webhook' => '',
// 'Create a comment from an external provider' => '',
// 'Github issue comment created' => '',
// 'Configure' => '',
// 'Project management' => '',
// 'My projects' => '',
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
// 'Subtask' => '',
// 'My subtasks' => '',
// 'User repartition' => '',
// 'User repartition for "%s"' => '',
// 'Clone this project' => '',
// 'Column removed successfully.' => '',
// 'Edit Project' => '',
// 'Github Issue' => '',
// 'Not enough data to show the graph.' => '',
// 'Previous' => '',
// 'The id must be an integer' => '',
// 'The project id must be an integer' => '',
// 'The status must be an integer' => '',
// 'The subtask id is required' => '',
// 'The subtask id must be an integer' => '',
// 'The task id is required' => '',
// 'The task id must be an integer' => '',
// 'The user id must be an integer' => '',
// 'This value is required' => '',
// 'This value must be numeric' => '',
// 'Unable to create this task.' => '',
// 'Cumulative flow diagram' => '',
// 'Cumulative flow diagram for "%s"' => '',
// 'Daily project summary' => '',
// 'Daily project summary export' => '',
// 'Daily project summary export for "%s"' => '',
// 'Exports' => '',
// 'This export contains the number of tasks per column grouped per day.' => '',
// 'Nothing to preview...' => '',
// 'Preview' => '',
// 'Write' => '',
);

View file

@ -108,8 +108,8 @@ return array(
'There is nobody assigned' => 'Não há ninguém designado',
'Column on the board:' => 'Coluna no quadro:',
'Status is open' => 'Status está aberto',
'Status is closed' => 'Status está fechado',
'Close this task' => 'Fechar esta tarefa',
'Status is closed' => 'Status está encerrado',
'Close this task' => 'Encerrar esta tarefa',
'Open this task' => 'Abrir esta tarefa',
'There is no description.' => 'Não há descrição.',
'Add a new task' => 'Adicionar uma nova tarefa',
@ -187,6 +187,7 @@ return array(
'Complexity' => 'Complexidade',
'limit' => 'limite',
'Task limit' => 'Limite da tarefa',
'Task count' => 'Número de tarefas',
'This value must be greater than %d' => 'Este valor deve ser maior que %d',
'Edit project access list' => 'Editar lista de acesso ao projeto',
'Edit users access' => 'Editar acesso de usuários',
@ -196,7 +197,7 @@ return array(
'revoke' => 'revogar',
'List of authorized users' => 'Lista de usuários autorizados',
'User' => 'Usuário',
// 'Nobody have access to this project.' => '',
'Nobody have access to this project.' => 'Ninguém tem acesso a este projeto.',
'You are not allowed to access to this project.' => 'Você não está autorizado a acessar este projeto.',
'Comments' => 'Comentários',
'Post comment' => 'Postar comentário',
@ -216,7 +217,7 @@ return array(
'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.',
'Remove an action' => 'Remover uma ação',
'Unable to remove this action.' => 'Impossível remover esta ação',
'Unable to remove this action.' => 'Impossível remover esta ação.',
'Action removed successfully.' => 'Ação removida com sucesso.',
'Automatic actions for the project "%s"' => 'Ações automáticas para o projeto "%s"',
'Defined actions' => 'Ações definidas',
@ -226,13 +227,13 @@ return array(
'Action parameters' => 'Parâmetros da ação',
'Action' => 'Ação',
'Event' => 'Evento',
'When the selected event occurs execute the corresponding action.' => 'Quando o evento selecionado ocorrer, execute a ação correspondente',
'When the selected event occurs execute the corresponding action.' => 'Quando o evento selecionado ocorrer, execute a ação correspondente.',
'Next step' => 'Próximo passo',
'Define action parameters' => 'Definir parêmetros da ação',
'Save this action' => 'Salvar esta ação',
'Do you really want to remove this action: "%s"?' => 'Você quer realmente remover esta ação: "%s"?',
'Remove an automatic action' => 'Remove uma ação automática',
'Close the task' => 'Fechar tarefa',
'Close the task' => 'Encerrar tarefa',
'Assign the task to a specific user' => 'Designar a tarefa para um usuário específico',
'Assign the task to the person who does the action' => 'Designar a tarefa para a pessoa que executa a ação',
'Duplicate the task to another project' => 'Duplicar a tarefa para um outro projeto',
@ -240,8 +241,8 @@ return array(
'Move a task to another position in the same column' => 'Mover a tarefa para outra posição, na mesma coluna',
'Task modification' => 'Modificação de tarefa',
'Task creation' => 'Criação de tarefa',
'Open a closed task' => 'Reabrir uma tarefa fechada',
'Closing a task' => 'Fechando uma tarefa',
'Open a closed task' => 'Reabrir uma tarefa encerrada',
'Closing a task' => 'Encerrando uma tarefa',
'Assign a color to a specific user' => 'Designar uma cor para um usuário específico',
'Column title' => 'Título da coluna',
'Position' => 'Posição',
@ -253,9 +254,9 @@ return array(
'Update this comment' => 'Atualizar este comentário',
'Comment updated successfully.' => 'Comentário atualizado com sucesso.',
'Unable to update your comment.' => 'Impossível atualizar seu comentário.',
'Remove a comment' => 'Remover um comentário.',
'Remove a comment' => 'Remover um comentário',
'Comment removed successfully.' => 'Comentário removido com sucesso.',
'Unable to remove this comment.' => 'Impossível remover este comentário',
'Unable to remove this comment.' => 'Impossível remover este comentário.',
'Do you really want to remove this comment?' => 'Você tem certeza de que quer remover este comentário?',
'Only administrators or the creator of the comment can access to this page.' => 'Somente administradores ou o criator deste comentário tem acesso a esta página.',
'Details' => 'Detalhes',
@ -263,7 +264,7 @@ return array(
'The current password is required' => 'A senha atual é obrigatória',
'Wrong password' => 'Senha errada',
'Reset all tokens' => 'Reiniciar todos os tokens',
'All tokens have been regenerated.' => 'Todos os tokens foram gerados novamente',
'All tokens have been regenerated.' => 'Todos os tokens foram gerados novamente.',
'Unknown' => 'Desconhecido',
'Last logins' => 'Últimos logins',
'Login date' => 'Data de login',
@ -279,7 +280,7 @@ return array(
'Filter by due date' => 'Filtrar por data de vencimento',
'Everybody' => 'Todos',
'Open' => 'Abrir',
'Closed' => 'Fechado',
'Closed' => 'Encerrado',
'Search' => 'Pesquisar',
'Nothing found.' => 'Não encontrado.',
'Search in the project "%s"' => 'Procure no projeto "%s"',
@ -391,21 +392,21 @@ return array(
'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"' => '',
'Email notifications' => 'Notificações por email',
'Enable email notifications' => 'Habilitar notificações por email',
'Task position:' => 'Posição da tarefa:',
'The task #%d have been opened.' => 'A tarefa #%d foi aberta.',
'The task #%d have been closed.' => 'A tarefa #%d foi encerrada.',
'Sub-task updated' => 'Subtarefa atualizada',
'Title:' => 'Título:',
'Status:' => 'Status:',
'Assignee:' => 'Designado:',
'Time tracking:' => 'Controle de tempo:',
'New sub-task' => 'Nova subtarefa',
'New attachment added "%s"' => 'Novo anexo adicionado "%s"',
'Comment updated' => 'Comentário atualizado',
'New comment posted by %s' => 'Novo comentário postado por %s',
'List of due tasks for the project "%s"' => 'Lista de tarefas pendentes para o projeto "%s"',
// '[%s][New attachment] %s (#%d)' => '',
// '[%s][New comment] %s (#%d)' => '',
// '[%s][Comment updated] %s (#%d)' => '',
@ -416,107 +417,107 @@ return array(
// '[%s][Task closed] %s (#%d)' => '',
// '[%s][Task opened] %s (#%d)' => '',
// '[%s][Due tasks]' => '',
// '[Kanboard] Notification' => '',
// 'I want to receive notifications only for those projects:' => '',
// 'view the task on Kanboard' => '',
// 'Public access' => '',
// 'Category management' => '',
// 'User management' => '',
// 'Active tasks' => '',
// 'Disable public access' => '',
// 'Enable public access' => '',
// 'Active projects' => '',
// 'Inactive projects' => '',
// 'Public access disabled' => '',
// 'Do you really want to disable this project: "%s"?' => '',
// 'Do you really want to duplicate this project: "%s"?' => '',
// 'Do you really want to enable this project: "%s"?' => '',
// 'Project activation' => '',
// 'Move the task to another project' => '',
// 'Move to another project' => '',
// 'Do you really want to duplicate this task?' => '',
// 'Duplicate a task' => '',
// 'External accounts' => '',
// 'Account type' => '',
'[Kanboard] Notification' => '[Kanboard] Notificação',
'I want to receive notifications only for those projects:' => 'Quero receber notificações somente para estes projetos:',
'view the task on Kanboard' => 'ver a tarefa no Kanboard',
'Public access' => 'Acesso público',
'Category management' => 'Gerenciamento de categorias',
'User management' => 'Gerenciamento de usuários',
'Active tasks' => 'Tarefas ativas',
'Disable public access' => 'Desabilitar o acesso público',
'Enable public access' => 'Habilitar o acesso público',
'Active projects' => 'Projetos ativos',
'Inactive projects' => 'Projetos inativos',
'Public access disabled' => 'Acesso público desabilitado',
'Do you really want to disable this project: "%s"?' => 'Deseja ralmente desabilitar este projeto: "%s"?',
'Do you really want to duplicate this project: "%s"?' => 'Deseja realmente duplicar este projeto: "%s"?',
'Do you really want to enable this project: "%s"?' => 'Deseja realmente habilitar este projeto: "%s"?',
'Project activation' => 'Avaliação do projeto',
'Move the task to another project' => 'Mover a tarefa para outro projeto',
'Move to another project' => 'Mover para outro projeto',
'Do you really want to duplicate this task?' => 'Deseja realmente duplicar esta tarefa?',
'Duplicate a task' => 'Duplicar tarefa',
'External accounts' => 'Contas externas',
'Account type' => 'Tipo de conta',
// 'Local' => '',
// 'Remote' => '',
// 'Enabled' => '',
// 'Disabled' => '',
// 'Google account linked' => '',
// 'Github account linked' => '',
// 'Username:' => '',
// 'Name:' => '',
'Remote' => 'Remoto',
'Enabled' => 'Habilitado',
'Disabled' => 'Desabilitado',
'Google account linked' => 'Conta do Google associada',
'Github account linked' => 'Conta do Github associada',
'Username:' => 'Usuário:',
'Name:' => 'Nome:',
// 'Email:' => '',
// 'Default project:' => '',
// 'Notifications:' => '',
// 'Notifications' => '',
// 'Group:' => '',
// 'Regular user' => '',
// 'Account type:' => '',
// 'Edit profile' => '',
// 'Change password' => '',
// 'Password modification' => '',
// 'External authentications' => '',
// 'Google Account' => '',
// 'Github Account' => '',
// 'Never connected.' => '',
// 'No account linked.' => '',
// 'Account linked.' => '',
// 'No external authentication enabled.' => '',
// 'Password modified successfully.' => '',
// 'Unable to change the password.' => '',
// 'Change category for the task "%s"' => '',
// 'Change category' => '',
'Default project:' => 'Projeto padrão:',
'Notifications:' => 'Notificações:',
'Notifications' => 'Notificações',
'Group:' => 'Groupo:',
'Regular user' => 'Usuário habitual',
'Account type:' => 'Tipo de conta:',
'Edit profile' => 'Editar perfil',
'Change password' => 'Alterar senha',
'Password modification' => 'Alteração de senha',
'External authentications' => 'Autenticação externa',
'Google Account' => 'Conta do Google',
'Github Account' => 'Conta do Github',
'Never connected.' => 'Nunca conectado.',
'No account linked.' => 'Nenhuma conta associada.',
'Account linked.' => 'Conta associada.',
'No external authentication enabled.' => 'Nenhuma autenticação externa permitida.',
'Password modified successfully.' => 'Senha alterada com sucesso.',
'Unable to change the password.' => 'Não foi possível alterar a senha.',
'Change category for the task "%s"' => 'Mudar categoria para a tarefa "%s"',
'Change category' => 'Mudar categoria',
// '%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.' => '',
'%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s criou uma sub-tarefa para a tarefa <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 atualizou uma sub-tarefa da tarefa <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'Assigned to %s with an estimate of %s/%sh' => 'Designado para %s com tempo estimado de %s/%sh',
'Not assigned, estimate of %sh' => 'Não designado, estimado em %sh',
'%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s atualizou o comentário na tarefa <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 comentou a tarefa <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
'%s\'s activity' => 'Atividades de%s',
'No activity.' => 'Sem atividade.',
// 'RSS feed' => '',
// '%s updated a comment on the task #%d' => '',
// '%s commented on the task #%d' => '',
// '%s updated a subtask for the task #%d' => '',
// '%s created a subtask for the task #%d' => '',
// '%s updated the task #%d' => '',
// '%s created the task #%d' => '',
// '%s closed the task #%d' => '',
// '%s open the task #%d' => '',
// '%s moved the task #%d to the column "%s"' => '',
// '%s moved the task #%d to the position %d in the column "%s"' => '',
// 'Activity' => '',
// 'Default values are "%s"' => '',
// 'Default columns for new projects (Comma-separated)' => '',
// 'Task assignee change' => '',
// '%s change the assignee of the task #%d to %s' => '',
// '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to %s' => '',
'%s updated a comment on the task #%d' => '%s atualizou um comentário na tarefa #%d',
'%s commented on the task #%d' => '%s comentou na tarefa #%d',
'%s updated a subtask for the task #%d' => '%s atualizou uma sub-tarefa para a tarefa #%d',
'%s created a subtask for the task #%d' => '%s criou uma sub-tarefa para a tarefa #%d',
'%s updated the task #%d' => '%s atualizou a tarefa #%d',
'%s created the task #%d' => '%s criou a tarefa #%d',
'%s closed the task #%d' => '%s encerrou a tarefa #%d',
'%s open the task #%d' => '%s abriu a tarefa #%d',
'%s moved the task #%d to the column "%s"' => '%s moveu a tarefa #%d para a coluna "%s"',
'%s moved the task #%d to the position %d in the column "%s"' => '%s moveu a tarefa #%d para a posição %d na coluna "%s"',
'Activity' => 'Atividade',
'Default values are "%s"' => 'Os valores padrão são "%s"',
'Default columns for new projects (Comma-separated)' => 'Colunas padrão para novos projetos (Separado por vírgula)',
'Task assignee change' => 'Mudar designação da tarefa',
'%s change the assignee of the task #%d to %s' => '%s mudou a designação da tarefa #%d para %s',
'%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to %s' => '%s mudou a designação da tarefa <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> para %s',
// '[%s][Column Change] %s (#%d)' => '',
// '[%s][Position Change] %s (#%d)' => '',
// '[%s][Assignee Change] %s (#%d)' => '',
// 'New password for the user "%s"' => '',
// 'Choose an event' => '',
'New password for the user "%s"' => 'Novo password para o usuário "%s"',
'Choose an event' => 'Escolher um evento',
// 'Github commit received' => '',
// 'Github issue opened' => '',
// 'Github issue closed' => '',
// 'Github issue reopened' => '',
// 'Github issue assignee change' => '',
// 'Github issue label change' => '',
// 'Create a task from an external provider' => '',
// 'Change the assignee based on an external username' => '',
// 'Change the category based on an external label' => '',
// 'Reference' => '',
// 'Reference: %s' => '',
// 'Label' => '',
// 'Database' => '',
// 'About' => '',
'Create a task from an external provider' => 'Criar uma tarefa a partir de um provedor externo',
'Change the assignee based on an external username' => 'Alterar designação com vase em um usuário externo!',
'Change the category based on an external label' => 'Alterar categoria com base em um rótulo externo',
'Reference' => 'Referencia',
'Reference: %s' => 'Referencia: %s',
'Label' => 'Rótulo',
'Database' => 'Banco de dados',
'About' => 'Sobre',
// 'Database driver:' => '',
// 'Board settings' => '',
// 'URL and token' => '',
@ -530,32 +531,75 @@ return array(
// 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '',
// 'Frequency in second (60 seconds by default)' => '',
// 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '',
// 'Application URL' => '',
'Application URL' => 'URL da Aplicação',
// 'Example: http://example.kanboard.net/ (used by email notifications)' => '',
// 'Token regenerated.' => '',
// 'Date format' => '',
// 'ISO format is always accepted, example: "%s" and "%s"' => '',
// 'New private project' => '',
// 'This project is private' => '',
// 'Type here to create a new sub-task' => '',
// 'Add' => '',
// 'Estimated time: %s hours' => '',
// 'Time spent: %s hours' => '',
// 'Started on %B %e, %Y' => '',
// 'Start date' => '',
// 'Time estimated' => '',
// 'There is nothing assigned to you.' => '',
// 'My tasks' => '',
'Date format' => 'Formato de data',
'ISO format is always accepted, example: "%s" and "%s"' => 'O formato ISO é sempre aceito, exemplo: "%s" e "%s"',
'New private project' => 'Novo projeto privado',
'This project is private' => 'Este projeto é privado',
'Type here to create a new sub-task' => 'Digite aqui para criar uma nova sub-tarefa',
'Add' => 'Adicionar',
'Estimated time: %s hours' => 'Tempo estimado: %s horas',
'Time spent: %s hours' => 'Tempo gasto: %s horas',
'Started on %B %e, %Y' => 'Iniciado em %B %e, %Y',
'Start date' => 'Data de início',
'Time estimated' => 'Tempo estimado',
'There is nothing assigned to you.' => 'Não há nada designado para você.',
'My tasks' => 'Minhas tarefas',
// 'Activity stream' => '',
// 'Dashboard' => '',
// 'Confirmation' => '',
// 'Allow everybody to access to this project' => '',
// 'Everybody have access to this project.' => '',
'Confirmation' => 'Confirmação',
'Allow everybody to access to this project' => 'Permitir que todos acessem este projeto',
'Everybody have access to this project.' => 'Todos possuem acesso a este projeto.',
// 'Webhooks' => '',
// 'API' => '',
// 'Integration' => '',
'Integration' => 'Integração',
// 'Github webhook' => '',
// 'Help on Github webhook' => '',
// 'Create a comment from an external provider' => '',
'Help on Github webhook' => 'Ajuda para o Github webhook',
'Create a comment from an external provider' => 'Criar um comentário de um provedor externo',
// 'Github issue comment created' => '',
'Configure' => 'Configurar',
'Project management' => 'Gerenciamento de projetos',
'My projects' => 'Meus projetos',
'Columns' => 'Colunas',
'Task' => 'Tarefas',
'Your are not member of any project.' => 'Você não é menmbro de nenhum projeto.',
'Percentage' => 'Porcentagem',
'Number of tasks' => 'Número de tarefas',
'Task distribution' => 'Distribuição de tarefas',
'Reportings' => 'Relatórios',
// 'Task repartition for "%s"' => '',
'Analytics' => 'Estatísticas',
'Subtask' => 'Sub-tarefa',
'My subtasks' => 'Minhas sub-tarefas',
'User repartition' => 'Repartição de usuário',
'User repartition for "%s"' => 'Repartição de usuário para "%s"',
'Clone this project' => 'Clonar o projeto',
'Column removed successfully.' => 'Coluna removida com sucesso.',
'Edit Project' => 'Editar projeto',
// 'Github Issue' => '',
'Not enough data to show the graph.' => 'Dados insuficientes para exibir o gráfico.',
'Previous' => 'Anterior',
'The id must be an integer' => 'A ID deve ser um inteiro',
'The project id must be an integer' => 'A ID do projeto deve ser um inteiro',
'The status must be an integer' => 'O status deve ser um inteiro',
'The subtask id is required' => 'A ID da sub-tarefa é requerida',
'The subtask id must be an integer' => 'A ID da sub-tarefa deve ser um inteiro',
'The task id is required' => 'A ID da tarefa é requerida',
'The task id must be an integer' => 'A ID da tarefa deve ser um inteiro',
'The user id must be an integer' => 'A ID de usuário deve ser um inteiro',
'This value is required' => 'Este valor é requerido',
'This value must be numeric' => 'Este valor deve ser numérico',
'Unable to create this task.' => 'Não foi possível criar esta tarefa.',
// 'Cumulative flow diagram' => '',
// 'Cumulative flow diagram for "%s"' => '',
// 'Daily project summary' => '',
// 'Daily project summary export' => '',
// 'Daily project summary export for "%s"' => '',
// 'Exports' => '',
// 'This export contains the number of tasks per column grouped per day.' => '',
'Nothing to preview...' => 'Nada para pré-visualizar...',
'Preview' => 'Pré-visualizar',
// 'Write' => '',
);

View file

@ -187,6 +187,7 @@ return array(
'Complexity' => 'Сложность',
'limit' => 'лимит',
'Task limit' => 'Лимит задач',
// 'Task count' => '',
'This value must be greater than %d' => 'Это значение должно быть больше %d',
'Edit project access list' => 'Изменить доступ к проекту',
'Edit users access' => 'Изменить доступ пользователей',
@ -558,4 +559,47 @@ return array(
// 'Help on Github webhook' => '',
// 'Create a comment from an external provider' => '',
// 'Github issue comment created' => '',
// 'Configure' => '',
// 'Project management' => '',
// 'My projects' => '',
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
// 'Subtask' => '',
// 'My subtasks' => '',
// 'User repartition' => '',
// 'User repartition for "%s"' => '',
// 'Clone this project' => '',
// 'Column removed successfully.' => '',
// 'Edit Project' => '',
// 'Github Issue' => '',
// 'Not enough data to show the graph.' => '',
// 'Previous' => '',
// 'The id must be an integer' => '',
// 'The project id must be an integer' => '',
// 'The status must be an integer' => '',
// 'The subtask id is required' => '',
// 'The subtask id must be an integer' => '',
// 'The task id is required' => '',
// 'The task id must be an integer' => '',
// 'The user id must be an integer' => '',
// 'This value is required' => '',
// 'This value must be numeric' => '',
// 'Unable to create this task.' => '',
// 'Cumulative flow diagram' => '',
// 'Cumulative flow diagram for "%s"' => '',
// 'Daily project summary' => '',
// 'Daily project summary export' => '',
// 'Daily project summary export for "%s"' => '',
// 'Exports' => '',
// 'This export contains the number of tasks per column grouped per day.' => '',
// 'Nothing to preview...' => '',
// 'Preview' => '',
// 'Write' => '',
);

View file

@ -187,6 +187,7 @@ return array(
'Complexity' => 'Ungefärligt antal timmar',
'limit' => 'max',
'Task limit' => 'Uppgiftsbegränsning',
// 'Task count' => '',
'This value must be greater than %d' => 'Värdet måste vara större än %d',
'Edit project access list' => 'Ändra projektåtkomst lista',
'Edit users access' => 'Användaråtkomst',
@ -558,4 +559,47 @@ return array(
// 'Help on Github webhook' => '',
// 'Create a comment from an external provider' => '',
// 'Github issue comment created' => '',
// 'Configure' => '',
// 'Project management' => '',
// 'My projects' => '',
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
// 'Subtask' => '',
// 'My subtasks' => '',
// 'User repartition' => '',
// 'User repartition for "%s"' => '',
// 'Clone this project' => '',
// 'Column removed successfully.' => '',
// 'Edit Project' => '',
// 'Github Issue' => '',
// 'Not enough data to show the graph.' => '',
// 'Previous' => '',
// 'The id must be an integer' => '',
// 'The project id must be an integer' => '',
// 'The status must be an integer' => '',
// 'The subtask id is required' => '',
// 'The subtask id must be an integer' => '',
// 'The task id is required' => '',
// 'The task id must be an integer' => '',
// 'The user id must be an integer' => '',
// 'This value is required' => '',
// 'This value must be numeric' => '',
// 'Unable to create this task.' => '',
// 'Cumulative flow diagram' => '',
// 'Cumulative flow diagram for "%s"' => '',
// 'Daily project summary' => '',
// 'Daily project summary export' => '',
// 'Daily project summary export for "%s"' => '',
// 'Exports' => '',
// 'This export contains the number of tasks per column grouped per day.' => '',
// 'Nothing to preview...' => '',
// 'Preview' => '',
// 'Write' => '',
);

View file

@ -187,6 +187,7 @@ return array(
'Complexity' => 'ความซับซ้อน',
'limit' => 'จำกัด',
'Task limit' => 'จำกัดงาน',
// 'Task count' => '',
'This value must be greater than %d' => 'ค่าต้องมากกว่า %d',
'Edit project access list' => 'แก้ไขการเข้าถึงรายชื่อโปรเจค',
'Edit users access' => 'แก้ไขการเข้าถึงผู้ใช้',
@ -558,4 +559,47 @@ return array(
// 'Help on Github webhook' => '',
// 'Create a comment from an external provider' => '',
// 'Github issue comment created' => '',
// 'Configure' => '',
// 'Project management' => '',
// 'My projects' => '',
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
// 'Subtask' => '',
// 'My subtasks' => '',
// 'User repartition' => '',
// 'User repartition for "%s"' => '',
// 'Clone this project' => '',
// 'Column removed successfully.' => '',
// 'Edit Project' => '',
// 'Github Issue' => '',
// 'Not enough data to show the graph.' => '',
// 'Previous' => '',
// 'The id must be an integer' => '',
// 'The project id must be an integer' => '',
// 'The status must be an integer' => '',
// 'The subtask id is required' => '',
// 'The subtask id must be an integer' => '',
// 'The task id is required' => '',
// 'The task id must be an integer' => '',
// 'The user id must be an integer' => '',
// 'This value is required' => '',
// 'This value must be numeric' => '',
// 'Unable to create this task.' => '',
// 'Cumulative flow diagram' => '',
// 'Cumulative flow diagram for "%s"' => '',
// 'Daily project summary' => '',
// 'Daily project summary export' => '',
// 'Daily project summary export for "%s"' => '',
// 'Exports' => '',
// 'This export contains the number of tasks per column grouped per day.' => '',
// 'Nothing to preview...' => '',
// 'Preview' => '',
// 'Write' => '',
);

View file

@ -187,6 +187,7 @@ return array(
'Complexity' => '复杂度',
'limit' => '限制',
'Task limit' => '任务限制',
// 'Task count' => '',
'This value must be greater than %d' => '该数值必须大于%d',
'Edit project access list' => '编辑项目存取列表',
'Edit users access' => '编辑用户存取权限',
@ -288,13 +289,13 @@ return array(
'Description' => '描述',
'%d comments' => '%d个评论',
'%d comment' => '%d个评论',
'Email address invalid' => 'Email地址无效',
'Email address invalid' => '电子邮件地址无效',
'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',
'Email' => '电子邮件',
'Link my Google Account' => '关联我的google帐号',
'Unlink my Google Account' => '去除我的google帐号关联',
'Login with my Google Account' => '用我的google帐号登录',
@ -549,13 +550,56 @@ return array(
'Activity stream' => '活动流',
'Dashboard' => '面板',
'Confirmation' => '确认',
// 'Allow everybody to access to this project' => '',
// 'Everybody have access to this project.' => '',
// 'Webhooks' => '',
// 'API' => '',
// 'Integration' => '',
// 'Github webhook' => '',
// 'Help on Github webhook' => '',
// 'Create a comment from an external provider' => '',
// 'Github issue comment created' => '',
'Allow everybody to access to this project' => '允许所有人访问此项目',
'Everybody have access to this project.' => '所有人都可以访问此项目',
'Webhooks' => '网络钩子',
'API' => '应用程序接口',
'Integration' => '整合',
'Github webhook' => 'Github 网络钩子',
'Help on Github webhook' => 'Github 网络钩子帮助',
'Create a comment from an external provider' => '从外部创建一个评论',
'Github issue comment created' => '已经创建了Github问题评论',
'Configure' => '配置',
'Project management' => '项目管理',
'My projects' => '我的项目',
'Columns' => '栏目',
'Task' => '任务',
'Your are not member of any project.' => '您尚未加入任何项目',
'Percentage' => '百分比',
'Number of tasks' => '任务数',
'Task distribution' => '任务分布',
'Reportings' => '报告',
'Task repartition for "%s"' => '"%s"的任务分析',
'Analytics' => '分析',
'Subtask' => '子任务',
'My subtasks' => '我的子任务',
'User repartition' => '用户分析',
'User repartition for "%s"' => '"%s"的用户分析',
'Clone this project' => '复制此项目',
'Column removed successfully.' => '成功删除了栏目。',
'Edit Project' => '编辑项目',
'Github Issue' => 'Github 任务报告',
'Not enough data to show the graph.' => '数据不足,无法绘图。',
'Previous' => '后退',
'The id must be an integer' => '编号必须为整数',
'The project id must be an integer' => '项目编号必须为整数',
'The status must be an integer' => '状态必须为整数',
'The subtask id is required' => '必须提供子任务编号',
'The subtask id must be an integer' => '子任务编号必须为整数',
'The task id is required' => '需要任务编号',
'The task id must be an integer' => '任务编号必须为整数',
'The user id must be an integer' => '用户编号必须为整数',
'This value is required' => '必须给出这个值',
'This value must be numeric' => '这个值必须为数字',
'Unable to create this task.' => '无法创建此任务。',
'Cumulative flow diagram' => '累积流图表',
'Cumulative flow diagram for "%s"' => '"%s"的累积流图表',
'Daily project summary' => '每日项目汇总',
'Daily project summary export' => '导出每日项目汇总',
'Daily project summary export for "%s"' => '导出项目"%s"的每日汇总',
'Exports' => '导出',
'This export contains the number of tasks per column grouped per day.' => '此导出包含每列的任务数,按天分组',
'Nothing to preview...' => '没有需要预览的内容',
'Preview' => '预览',
'Write' => '书写',
);

View file

@ -31,9 +31,9 @@ class Acl extends Base
* @var array
*/
private $user_actions = array(
'app' => array('index'),
'board' => array('index', 'show', 'save', 'check', 'changeassignee', 'updateassignee', 'changecategory', 'updatecategory', 'movecolumn', 'edit', 'update', 'add', 'confirm', 'remove'),
'project' => array('index', 'show', 'export', 'share', 'edit', 'update', 'users', 'remove', 'duplicate', 'disable', 'enable', 'activity', 'search', 'tasks', 'create', 'save'),
'app' => array('index', 'preview', 'status'),
'project' => array('index', 'show', 'exporttasks', 'exportdaily', 'share', 'edit', 'update', 'users', 'remove', 'duplicate', 'disable', 'enable', 'activity', 'search', 'tasks', 'create', 'save'),
'board' => array('index', 'show', 'save', 'check', 'changeassignee', 'updateassignee', 'changecategory', 'updatecategory', 'movecolumn', 'edit', 'update', 'add', 'confirm', 'remove', 'subtasks', 'togglesubtask', 'attachments', 'comments', 'description'),
'user' => array('edit', 'forbidden', 'logout', 'show', 'external', 'unlinkgoogle', 'unlinkgithub', 'sessions', 'removesession', 'last', 'notifications', 'password'),
'comment' => array('create', 'save', 'confirm', 'remove', 'update', 'edit', 'forbidden'),
'file' => array('create', 'save', 'download', 'confirm', 'remove', 'open', 'image'),
@ -41,6 +41,7 @@ class Acl extends Base
'task' => array('show', 'create', 'save', 'edit', 'update', 'close', 'open', 'duplicate', 'remove', 'description', 'move', 'copy', 'time'),
'category' => array('index', 'save', 'edit', 'update', 'confirm', 'remove'),
'action' => array('index', 'event', 'params', 'create', 'confirm', 'remove'),
'analytic' => array('tasks', 'users', 'cfd'),
);
/**

View file

@ -264,7 +264,7 @@ class Action extends Base
public function load($name, $project_id, $event)
{
$className = '\Action\\'.$name;
return new $className($this->registry, $project_id, $event);
return new $className($this->container, $project_id, $event);
}
/**

View file

@ -24,12 +24,12 @@ class Authentication extends Base
*/
public function backend($name)
{
if (! isset($this->registry->$name)) {
if (! isset($this->container[$name])) {
$class = '\Auth\\'.ucfirst($name);
$this->registry->$name = new $class($this->registry);
$this->container[$name] = new $class($this->container);
}
return $this->registry->shared($name);
return $this->container[$name];
}
/**

View file

@ -4,7 +4,7 @@ namespace Model;
use Core\Event;
use Core\Tool;
use Core\Registry;
use Pimple\Container;
use PicoDb\Database;
/**
@ -31,9 +31,11 @@ use PicoDb\Database;
* @property \Model\SubTask $subTask
* @property \Model\SubtaskHistory $subtaskHistory
* @property \Model\Task $task
* @property \Model\TaskCreation $taskCreation
* @property \Model\TaskExport $taskExport
* @property \Model\TaskFinder $taskFinder
* @property \Model\TaskHistory $taskHistory
* @property \Model\TaskPosition $taskPosition
* @property \Model\TaskValidator $taskValidator
* @property \Model\TimeTracking $timeTracking
* @property \Model\User $user
@ -58,24 +60,24 @@ abstract class Base
public $event;
/**
* Registry instance
* Container instance
*
* @access protected
* @var \Core\Registry
* @var \Pimple\Container
*/
protected $registry;
protected $container;
/**
* Constructor
*
* @access public
* @param \Core\Registry $registry Registry instance
* @param \Pimple\Container $container
*/
public function __construct(Registry $registry)
public function __construct(Container $container)
{
$this->registry = $registry;
$this->db = $this->registry->shared('db');
$this->event = $this->registry->shared('event');
$this->container = $container;
$this->db = $this->container['db'];
$this->event = $this->container['event'];
}
/**
@ -87,7 +89,27 @@ abstract class Base
*/
public function __get($name)
{
return Tool::loadModel($this->registry, $name);
return Tool::loadModel($this->container, $name);
}
/**
* Save a record in the database
*
* @access public
* @param string $table Table name
* @param array $values Form values
* @return boolean|integer
*/
public function persist($table, array $values)
{
return $this->db->transaction(function($db) use ($table, $values) {
if (! $db->table($table)->save($values)) {
return false;
}
return (int) $db->getConnection()->getLastId();
});
}
/**

View file

@ -109,16 +109,18 @@ class Board extends Base
* @param integer $project_id Project id
* @param string $title Column title
* @param integer $task_limit Task limit
* @return boolean
* @return boolean|integer
*/
public function addColumn($project_id, $title, $task_limit = 0)
{
return $this->db->table(self::TABLE)->save(array(
$values = array(
'project_id' => $project_id,
'title' => $title,
'task_limit' => $task_limit,
'position' => $this->getLastColumnPosition($project_id) + 1,
));
);
return $this->persist(self::TABLE, $values);
}
/**
@ -229,10 +231,9 @@ class Board extends Base
*
* @access public
* @param integer $project_id Project id
* @param array $filters
* @return array
*/
public function get($project_id, array $filters = array())
public function get($project_id)
{
$columns = $this->getColumns($project_id);
$tasks = $this->taskFinder->getTasksOnBoard($project_id);

View file

@ -45,6 +45,34 @@ class Category extends Base
return $this->db->table(self::TABLE)->eq('id', $category_id)->findOne();
}
/**
* Get the category name by the id
*
* @access public
* @param integer $category_id Category id
* @return string
*/
public function getNameById($category_id)
{
return $this->db->table(self::TABLE)->eq('id', $category_id)->findOneColumn('name') ?: '';
}
/**
* Get a category id by the project and the name
*
* @access public
* @param integer $project_id Project id
* @param string $category_name Category name
* @return integer
*/
public function getIdByName($project_id, $category_name)
{
return (int) $this->db->table(self::TABLE)
->eq('project_id', $project_id)
->eq('name', $category_name)
->findOneColumn('id');
}
/**
* Return the list of all categories
*
@ -94,11 +122,11 @@ class Category extends Base
*
* @access public
* @param array $values Form values
* @return bool
* @return bool|integer
*/
public function create(array $values)
{
return $this->db->table(self::TABLE)->save($values);
return $this->persist(self::TABLE, $values);
}
/**
@ -137,26 +165,26 @@ class Category extends Base
}
/**
* Duplicate categories from a project to another one
* Duplicate categories from a project to another one, must be executed inside a transaction
*
* @author Antonio Rabelo
* @param integer $project_from Project Template
* @return integer $project_to Project that receives the copy
* @param integer $src_project_id Source project id
* @return integer $dst_project_id Destination project id
* @return boolean
*/
public function duplicate($project_from, $project_to)
public function duplicate($src_project_id, $dst_project_id)
{
$categories = $this->db->table(self::TABLE)
->columns('name')
->eq('project_id', $project_from)
->eq('project_id', $src_project_id)
->asc('name')
->findAll();
foreach ($categories as $category) {
$category['project_id'] = $project_to;
$category['project_id'] = $dst_project_id;
if (! $this->category->create($category)) {
if (! $this->db->table(self::TABLE)->save($category)) {
return false;
}
}

View file

@ -28,4 +28,15 @@ class Color extends Base
'grey' => t('Grey'),
);
}
/**
* Get the default color
*
* @access public
* @return string
*/
public function getDefaultColor()
{
return 'yellow'; // TODO: make this parameter configurable
}
}

View file

@ -95,24 +95,22 @@ class Comment extends Base
}
/**
* Save a comment in the database
* Create a new comment
*
* @access public
* @param array $values Form values
* @return boolean
* @return boolean|integer
*/
public function create(array $values)
{
$values['date'] = time();
$comment_id = $this->persist(self::TABLE, $values);
if ($this->db->table(self::TABLE)->save($values)) {
$values['id'] = $this->db->getConnection()->getLastId();
$this->event->trigger(self::EVENT_CREATE, $values);
return true;
if ($comment_id) {
$this->event->trigger(self::EVENT_CREATE, array('id' => $comment_id) + $values);
}
return false;
return $comment_id;
}
/**

View file

@ -47,13 +47,11 @@ class GithubWebhook extends Base
*
* @access public
* @param string $type Github event type
* @param string $payload Raw Github event (JSON)
* @param array $payload Github event
* @return boolean
*/
public function parsePayload($type, $payload)
public function parsePayload($type, array $payload)
{
$payload = json_decode($payload, true);
switch ($type) {
case 'push':
return $this->parsePushEvent($payload);

View file

@ -101,23 +101,23 @@ class Notification extends Base
public function attachEvents()
{
$events = array(
Task::EVENT_CREATE => 'notification_task_creation',
Task::EVENT_UPDATE => 'notification_task_update',
Task::EVENT_CLOSE => 'notification_task_close',
Task::EVENT_OPEN => 'notification_task_open',
Task::EVENT_MOVE_COLUMN => 'notification_task_move_column',
Task::EVENT_MOVE_POSITION => 'notification_task_move_position',
Task::EVENT_ASSIGNEE_CHANGE => 'notification_task_assignee_change',
SubTask::EVENT_CREATE => 'notification_subtask_creation',
SubTask::EVENT_UPDATE => 'notification_subtask_update',
Comment::EVENT_CREATE => 'notification_comment_creation',
Comment::EVENT_UPDATE => 'notification_comment_update',
File::EVENT_CREATE => 'notification_file_creation',
Task::EVENT_CREATE => 'task_creation',
Task::EVENT_UPDATE => 'task_update',
Task::EVENT_CLOSE => 'task_close',
Task::EVENT_OPEN => 'task_open',
Task::EVENT_MOVE_COLUMN => 'task_move_column',
Task::EVENT_MOVE_POSITION => 'task_move_position',
Task::EVENT_ASSIGNEE_CHANGE => 'task_assignee_change',
SubTask::EVENT_CREATE => 'subtask_creation',
SubTask::EVENT_UPDATE => 'subtask_update',
Comment::EVENT_CREATE => 'comment_creation',
Comment::EVENT_UPDATE => 'comment_update',
File::EVENT_CREATE => 'file_creation',
);
foreach ($events as $event_name => $template_name) {
$listener = new NotificationListener($this->registry);
$listener = new NotificationListener($this->container);
$listener->setTemplate($template_name);
$this->event->attach($event_name, $listener);
@ -135,8 +135,7 @@ class Notification extends Base
public function sendEmails($template, array $users, array $data)
{
try {
$transport = $this->registry->shared('mailer');
$mailer = Swift_Mailer::newInstance($transport);
$mailer = Swift_Mailer::newInstance($this->container['mailer']);
$message = Swift_Message::newInstance()
->setSubject($this->getMailSubject($template, $data))
@ -149,7 +148,7 @@ class Notification extends Base
}
}
catch (Swift_TransportException $e) {
debug($e->getMessage());
$this->container['logger']->addError($e->getMessage());
}
}
@ -163,43 +162,43 @@ class Notification extends Base
public function getMailSubject($template, array $data)
{
switch ($template) {
case 'notification_file_creation':
case 'file_creation':
$subject = e('[%s][New attachment] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_comment_creation':
case 'comment_creation':
$subject = e('[%s][New comment] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_comment_update':
case 'comment_update':
$subject = e('[%s][Comment updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_subtask_creation':
case 'subtask_creation':
$subject = e('[%s][New subtask] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_subtask_update':
case 'subtask_update':
$subject = e('[%s][Subtask updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_task_creation':
case 'task_creation':
$subject = e('[%s][New task] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_task_update':
case 'task_update':
$subject = e('[%s][Task updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_task_close':
case 'task_close':
$subject = e('[%s][Task closed] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
break;
case 'notification_task_open':
case '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':
case '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':
case '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':
case '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':
case 'task_due':
$subject = e('[%s][Due tasks]', $data['project']);
break;
default:
@ -219,7 +218,7 @@ class Notification extends Base
public function getMailContent($template, array $data)
{
$tpl = new Template;
return $tpl->load($template, $data + array('application_url' => $this->config->get('application_url')));
return $tpl->load('notification/'.$template, $data + array('application_url' => $this->config->get('application_url')));
}
/**

View file

@ -192,7 +192,7 @@ class Project extends Base
public function getStats($project_id)
{
$stats = array();
$columns = $this->board->getcolumns($project_id);
$columns = $this->board->getColumns($project_id);
$stats['nb_active_tasks'] = 0;
foreach ($columns as &$column) {
@ -270,11 +270,12 @@ class Project extends Base
* Create a project
*
* @access public
* @param array $values Form values
* @param integer $user_id User who create the project
* @return integer Project id
* @param array $values Form values
* @param integer $user_id User who create the project
* @param bool $add_user Automatically add the user
* @return integer Project id
*/
public function create(array $values, $user_id = 0)
public function create(array $values, $user_id = 0, $add_user = false)
{
$this->db->startTransaction();
@ -294,7 +295,7 @@ class Project extends Base
return false;
}
if ($values['is_private'] && $user_id) {
if ($add_user && $user_id) {
$this->projectPermission->allowUser($project_id, $user_id);
}
@ -512,7 +513,7 @@ class Project extends Base
GithubWebhook::EVENT_COMMIT,
);
$listener = new ProjectModificationDateListener($this->registry);
$listener = new ProjectModificationDateListener($this->container);
foreach ($events as $event_name) {
$this->event->attach($event_name, $listener);

View file

@ -147,7 +147,7 @@ class ProjectActivity extends Base
SubTask::EVENT_CREATE,
);
$listener = new ProjectActivityListener($this->registry);
$listener = new ProjectActivityListener($this->container);
foreach ($events as $event_name) {
$this->event->attach($event_name, $listener);
@ -164,7 +164,7 @@ class ProjectActivity extends Base
public function getContent(array $params)
{
$tpl = new Template;
return $tpl->load('event_'.str_replace('.', '_', $params['event_name']), $params);
return $tpl->load('event/'.str_replace('.', '_', $params['event_name']), $params);
}
/**

View file

@ -0,0 +1,88 @@
<?php
namespace Model;
/**
* Project analytic model
*
* @package model
* @author Frederic Guillot
*/
class ProjectAnalytic extends Base
{
/**
* Get tasks repartition
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getTaskRepartition($project_id)
{
$metrics = array();
$total = 0;
$columns = $this->board->getColumns($project_id);
foreach ($columns as $column) {
$nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']);
$total += $nb_tasks;
$metrics[] = array(
'column_title' => $column['title'],
'nb_tasks' => $nb_tasks,
);
}
if ($total === 0) {
return array();
}
foreach ($metrics as &$metric) {
$metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
}
return $metrics;
}
/**
* Get users repartition
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getUserRepartition($project_id)
{
$metrics = array();
$total = 0;
$tasks = $this->taskFinder->getAll($project_id);
$users = $this->projectPermission->getMemberList($project_id);
foreach ($tasks as $task) {
$user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0];
$total++;
if (! isset($metrics[$user])) {
$metrics[$user] = array(
'nb_tasks' => 0,
'percentage' => 0,
'user' => $user,
);
}
$metrics[$user]['nb_tasks']++;
}
if ($total === 0) {
return array();
}
foreach ($metrics as &$metric) {
$metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
}
return array_values($metrics);
}
}

View file

@ -0,0 +1,181 @@
<?php
namespace Model;
use Core\Template;
use Event\ProjectDailySummaryListener;
/**
* Project daily summary
*
* @package model
* @author Frederic Guillot
*/
class ProjectDailySummary extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'project_daily_summaries';
/**
* Update daily totals for the project
*
* @access public
* @param integer $project_id Project id
* @param string $date Record date (YYYY-MM-DD)
* @return boolean
*/
public function updateTotals($project_id, $date)
{
return $this->db->transaction(function($db) use ($project_id, $date) {
$column_ids = $db->table(Board::TABLE)->eq('project_id', $project_id)->findAllByColumn('id');
foreach ($column_ids as $column_id) {
// This call will fail if the record already exists
// (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE)
$db->table(ProjectDailySummary::TABLE)->insert(array(
'day' => $date,
'project_id' => $project_id,
'column_id' => $column_id,
'total' => 0,
));
$db->table(ProjectDailySummary::TABLE)
->eq('project_id', $project_id)
->eq('column_id', $column_id)
->eq('day', $date)
->update(array(
'total' => $db->table(Task::TABLE)
->eq('project_id', $project_id)
->eq('column_id', $column_id)
->eq('is_active', Task::STATUS_OPEN)
->count()
));
}
});
}
/**
* Count the number of recorded days for the data range
*
* @access public
* @param integer $project_id Project id
* @param string $from Start date (ISO format YYYY-MM-DD)
* @param string $to End date
* @return integer
*/
public function countDays($project_id, $from, $to)
{
$rq = $this->db->execute(
'SELECT COUNT(DISTINCT day) FROM '.self::TABLE.' WHERE day >= ? AND day <= ? AND project_id=?',
array($from, $to, $project_id)
);
return $rq !== false ? $rq->fetchColumn(0) : 0;
}
/**
* Get raw metrics for the project within a data range
*
* @access public
* @param integer $project_id Project id
* @param string $from Start date (ISO format YYYY-MM-DD)
* @param string $to End date
* @return array
*/
public function getRawMetrics($project_id, $from, $to)
{
return $this->db->table(ProjectDailySummary::TABLE)
->columns(
ProjectDailySummary::TABLE.'.column_id',
ProjectDailySummary::TABLE.'.day',
ProjectDailySummary::TABLE.'.total',
Board::TABLE.'.title AS column_title'
)
->join(Board::TABLE, 'id', 'column_id')
->eq(ProjectDailySummary::TABLE.'.project_id', $project_id)
->gte('day', $from)
->lte('day', $to)
->findAll();
}
/**
* Get aggregated metrics for the project within a data range
*
* [
* ['Date', 'Column1', 'Column2'],
* ['2014-11-16', 2, 5],
* ['2014-11-17', 20, 15],
* ]
*
* @access public
* @param integer $project_id Project id
* @param string $from Start date (ISO format YYYY-MM-DD)
* @param string $to End date
* @return array
*/
public function getAggregatedMetrics($project_id, $from, $to)
{
$columns = $this->board->getColumnsList($project_id);
$column_ids = array_keys($columns);
$metrics = array(array(e('Date')) + $columns);
$aggregates = array();
// Fetch metrics for the project
$records = $this->db->table(ProjectDailySummary::TABLE)
->eq('project_id', $project_id)
->gte('day', $from)
->lte('day', $to)
->findAll();
// Aggregate by day
foreach ($records as $record) {
if (! isset($aggregates[$record['day']])) {
$aggregates[$record['day']] = array($record['day']);
}
$aggregates[$record['day']][$record['column_id']] = $record['total'];
}
// Aggregate by row
foreach ($aggregates as $aggregate) {
$row = array($aggregate[0]);
foreach ($column_ids as $column_id) {
$row[] = (int) $aggregate[$column_id];
}
$metrics[] = $row;
}
return $metrics;
}
/**
* Attach events to be able to record the metrics
*
* @access public
*/
public function attachEvents()
{
$events = array(
Task::EVENT_CREATE,
Task::EVENT_CLOSE,
Task::EVENT_OPEN,
Task::EVENT_MOVE_COLUMN,
);
$listener = new ProjectDailySummaryListener($this->container);
foreach ($events as $event_name) {
$this->event->attach($event_name, $listener);
}
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Model;
/**
* Project Paginator
*
* @package model
* @author Frederic Guillot
*/
class ProjectPaginator extends Base
{
/**
* Get project summary for a list of project (number of tasks for each column)
*
* @access public
* @param array $project_ids List of project id
* @param integer $offset Offset
* @param integer $limit Limit
* @param string $column Sorting column
* @param string $direction Sorting direction
* @return array
*/
public function projectSummaries(array $project_ids, $offset = 0, $limit = 25, $column = 'name', $direction = 'asc')
{
if (empty($project_ids)) {
return array();
}
$projects = $this->db
->table(Project::TABLE)
->in('id', $project_ids)
->offset($offset)
->limit($limit)
->orderBy($column, $direction)
->findAll();
foreach ($projects as &$project) {
$project['columns'] = $this->board->getColumns($project['id']);
foreach ($project['columns'] as &$column) {
$column['nb_tasks'] = $this->taskFinder->countByColumnId($project['id'], $column['id']);
}
}
return $projects;
}
}

View file

@ -27,11 +27,16 @@ class ProjectPermission extends Base
* @param integer $project_id Project id
* @param bool $prepend_unassigned Prepend the 'Unassigned' value
* @param bool $prepend_everybody Prepend the 'Everbody' value
* @param bool $allow_single_user If there is only one user return only this user
* @return array
*/
public function getUsersList($project_id, $prepend_unassigned = true, $prepend_everybody = false)
public function getMemberList($project_id, $prepend_unassigned = true, $prepend_everybody = false, $allow_single_user = false)
{
$allowed_users = $this->getAllowedUsers($project_id);
$allowed_users = $this->getMembers($project_id);
if ($allow_single_user && count($allowed_users) === 1) {
return $allowed_users;
}
if ($prepend_unassigned) {
$allowed_users = array(t('Unassigned')) + $allowed_users;
@ -51,7 +56,7 @@ class ProjectPermission extends Base
* @param integer $project_id Project id
* @return array
*/
public function getAllowedUsers($project_id)
public function getMembers($project_id)
{
if ($this->isEverybodyAllowed($project_id)) {
return $this->user->getList();
@ -96,7 +101,7 @@ class ProjectPermission extends Base
$all_users = $this->user->getList();
$users['allowed'] = $this->getAllowedUsers($project_id);
$users['allowed'] = $this->getMembers($project_id);
foreach ($all_users as $user_id => $username) {
@ -141,19 +146,15 @@ class ProjectPermission extends Base
}
/**
* Check if a specific user is allowed to access to a given project
* Check if a specific user is member of a project
*
* @access public
* @param integer $project_id Project id
* @param integer $user_id User id
* @return bool
*/
public function isUserAllowed($project_id, $user_id)
public function isMember($project_id, $user_id)
{
if ($this->user->isAdmin($user_id)) {
return true;
}
if ($this->isEverybodyAllowed($project_id)) {
return true;
}
@ -165,6 +166,19 @@ class ProjectPermission extends Base
->count();
}
/**
* Check if a specific user is allowed to access to a given project
*
* @access public
* @param integer $project_id Project id
* @param integer $user_id User id
* @return bool
*/
public function isUserAllowed($project_id, $user_id)
{
return $project_id === 0 || $this->user->isAdmin($user_id) || $this->isMember($project_id, $user_id);
}
/**
* Return true if everybody is allowed for the project
*
@ -204,12 +218,13 @@ class ProjectPermission extends Base
* @access public
* @param array $projects Project list: ['project_id' => 'project_name']
* @param integer $user_id User id
* @param string $filter Method name to apply
* @return array
*/
public function filterProjects(array $projects, $user_id)
public function filterProjects(array $projects, $user_id, $filter = 'isUserAllowed')
{
foreach ($projects as $project_id => $project_name) {
if (! $this->isUserAllowed($project_id, $user_id)) {
if (! $this->$filter($project_id, $user_id)) {
unset($projects[$project_id]);
}
}
@ -218,7 +233,7 @@ class ProjectPermission extends Base
}
/**
* Return a list of projects for a given user
* Return a list of allowed projects for a given user
*
* @access public
* @param integer $user_id User id
@ -226,7 +241,19 @@ class ProjectPermission extends Base
*/
public function getAllowedProjects($user_id)
{
return $this->filterProjects($this->project->getListByStatus(Project::ACTIVE), $user_id);
return $this->filterProjects($this->project->getListByStatus(Project::ACTIVE), $user_id, 'isUserAllowed');
}
/**
* Return a list of projects where the user is member
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function getMemberProjects($user_id)
{
return $this->filterProjects($this->project->getListByStatus(Project::ACTIVE), $user_id, 'isMember');
}
/**
@ -239,7 +266,7 @@ class ProjectPermission extends Base
*/
public function duplicate($project_from, $project_to)
{
$users = $this->getAllowedUsers($project_from);
$users = $this->getMembers($project_from);
foreach ($users as $user_id => $name) {
if (! $this->allowUser($project_to, $user_id)) {

View file

@ -134,23 +134,22 @@ class SubTask extends Base
}
/**
* Create
* Create a new subtask
*
* @access public
* @param array $values Form values
* @return bool
* @return bool|integer
*/
public function create(array $values)
{
$this->prepare($values);
$result = $this->db->table(self::TABLE)->save($values);
$subtask_id = $this->persist(self::TABLE, $values);
if ($result) {
$values['id'] = $this->db->getConnection()->getLastId();
$this->event->trigger(self::EVENT_CREATE, $values);
if ($subtask_id) {
$this->event->trigger(self::EVENT_CREATE, array('id' => $subtask_id) + $values);
}
return $result;
return $subtask_id;
}
/**
@ -172,6 +171,28 @@ class SubTask extends Base
return $result;
}
/**
* Change the status of subtask
*
* Todo -> In progress -> Done -> Todo -> etc...
*
* @access public
* @param integer $subtask_id
* @return bool
*/
public function toggleStatus($subtask_id)
{
$subtask = $this->getById($subtask_id);
$values = array(
'id' => $subtask['id'],
'status' => ($subtask['status'] + 1) % 3,
'task_id' => $subtask['task_id'],
);
return $this->update($values);
}
/**
* Remove
*
@ -194,22 +215,22 @@ class SubTask extends Base
*/
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();
return $this->db->transaction(function ($db) use ($src_task_id, $dst_task_id) {
foreach ($subtasks as &$subtask) {
$subtasks = $db->table(SubTask::TABLE)
->columns('title', 'time_estimated')
->eq('task_id', $src_task_id)
->findAll();
$subtask['task_id'] = $dst_task_id;
$subtask['time_spent'] = 0;
foreach ($subtasks as &$subtask) {
if (! $this->db->table(self::TABLE)->save($subtask)) {
return false;
$subtask['task_id'] = $dst_task_id;
if (! $db->table(SubTask::TABLE)->save($subtask)) {
return false;
}
}
}
return true;
});
}
/**
@ -242,6 +263,29 @@ class SubTask extends Base
* @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')),
new Validators\Required('title', t('The title is required')),
);
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Validate 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 subtask id is required')),

View file

@ -0,0 +1,68 @@
<?php
namespace Model;
/**
* Subtask Paginator
*
* @package model
* @author Frederic Guillot
*/
class SubtaskPaginator extends Base
{
/**
* Get all subtasks assigned to a user
*
* @access public
* @param integer $user_id User id
* @param array $status List of status
* @param integer $offset Offset
* @param integer $limit Limit
* @param string $column Sorting column
* @param string $direction Sorting direction
* @return array
*/
public function userSubtasks($user_id, array $status, $offset = 0, $limit = 25, $column = 'tasks.id', $direction = 'asc')
{
$status_list = $this->subTask->getStatusList();
$subtasks = $this->db->table(SubTask::TABLE)
->columns(
SubTask::TABLE.'.*',
Task::TABLE.'.project_id',
Task::TABLE.'.color_id',
Project::TABLE.'.name AS project_name'
)
->eq('user_id', $user_id)
->in(SubTask::TABLE.'.status', $status)
->join(Task::TABLE, 'id', 'task_id')
->join(Project::TABLE, 'id', 'project_id', Task::TABLE)
->offset($offset)
->limit($limit)
->orderBy($column, $direction)
->findAll();
foreach ($subtasks as &$subtask) {
$subtask['status_name'] = $status_list[$subtask['status']];
}
return $subtasks;
}
/**
* Count all subtasks assigned to the user
*
* @access public
* @param integer $user_id User id
* @param array $status List of status
* @return integer
*/
public function countUserSubtasks($user_id, array $status)
{
return $this->db
->table(SubTask::TABLE)
->eq('user_id', $user_id)
->in('status', $status)
->count();
}
}

View file

@ -39,204 +39,6 @@ class Task extends Base
const EVENT_CREATE_UPDATE = 'task.create_update';
const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change';
/**
* Prepare data before task creation or modification
*
* @access public
* @param array $values Form values
*/
public function prepare(array &$values)
{
$this->dateParser->convert($values, array('date_due', 'date_started'));
$this->removeFields($values, array('another_task', 'id'));
$this->resetFields($values, array('date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent'));
$this->convertIntegerFields($values, array('is_active'));
}
/**
* Prepare data before task creation
*
* @access public
* @param array $values Form values
*/
public function prepareCreation(array &$values)
{
$this->prepare($values);
if (empty($values['column_id'])) {
$values['column_id'] = $this->board->getFirstColumn($values['project_id']);
}
if (empty($values['color_id'])) {
$colors = $this->color->getList();
$values['color_id'] = key($colors);
}
$values['date_creation'] = time();
$values['date_modification'] = $values['date_creation'];
$values['position'] = $this->taskFinder->countByColumnId($values['project_id'], $values['column_id']) + 1;
}
/**
* Prepare data before task modification
*
* @access public
* @param array $values Form values
*/
public function prepareModification(array &$values)
{
$this->prepare($values);
$values['date_modification'] = time();
}
/**
* Create a task
*
* @access public
* @param array $values Form values
* @return boolean|integer
*/
public function create(array $values)
{
$this->db->startTransaction();
$this->prepareCreation($values);
if (! $this->db->table(self::TABLE)->save($values)) {
$this->db->cancelTransaction();
return false;
}
$task_id = $this->db->getConnection()->getLastId();
$this->db->closeTransaction();
// Trigger events
$this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $values);
$this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $values);
return $task_id;
}
/**
* Update a task
*
* @access public
* @param array $values Form values
* @param boolean $trigger_Events Trigger events
* @return boolean
*/
public function update(array $values, $trigger_events = true)
{
// Fetch original task
$original_task = $this->taskFinder->getById($values['id']);
if (! $original_task) {
return false;
}
// Prepare data
$updated_task = $values;
$this->prepareModification($updated_task);
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($updated_task);
if ($result && $trigger_events) {
$this->triggerUpdateEvents($original_task, $updated_task);
}
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);
}
}
/**
* Mark a task closed
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function close($task_id)
{
if (! $this->taskFinder->exists($task_id)) {
return false;
}
$result = $this->db
->table(self::TABLE)
->eq('id', $task_id)
->update(array(
'is_active' => 0,
'date_completed' => time()
));
if ($result) {
$this->event->trigger(self::EVENT_CLOSE, array('task_id' => $task_id) + $this->taskFinder->getById($task_id));
}
return $result;
}
/**
* Mark a task open
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function open($task_id)
{
if (! $this->taskFinder->exists($task_id)) {
return false;
}
$result = $this->db
->table(self::TABLE)
->eq('id', $task_id)
->update(array(
'is_active' => 1,
'date_completed' => 0
));
if ($result) {
$this->event->trigger(self::EVENT_OPEN, array('task_id' => $task_id) + $this->taskFinder->getById($task_id));
}
return $result;
}
/**
* Remove a task
*
@ -255,228 +57,6 @@ class Task extends Base
return $this->db->table(self::TABLE)->eq('id', $task_id)->remove();
}
/**
* Move a task to another column or to another position
*
* @access public
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param integer $column_id Column id
* @param integer $position Position (must be >= 1)
* @return boolean
*/
public function movePosition($project_id, $task_id, $column_id, $position)
{
// The position can't be lower than 1
if ($position < 1) {
return false;
}
$board = $this->db->table(Board::TABLE)->eq('project_id', $project_id)->asc('position')->findAllByColumn('id');
$columns = array();
// 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->projectPermission->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->taskFinder->countByColumnId($project_id, $values['column_id']) + 1;
$values['project_id'] = $project_id;
// The task will be open (close event binding)
$values['is_active'] = 1;
if ($this->db->table(self::TABLE)->eq('id', $task['id'])->update($values)) {
return $task['id'];
}
return false;
}
/**
* Generic method to duplicate a task
*
* @access public
* @param array $task Task data
* @param array $override Task properties to override
* @return integer|boolean
*/
public function copy(array $task, array $override = array())
{
// Values to override
if (! empty($override)) {
$task = $override + $task;
}
$this->db->startTransaction();
// Assign new values
$values = array();
$values['title'] = $task['title'];
$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->taskFinder->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->projectPermission->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
if (! $this->db->table(Task::TABLE)->save($values)) {
$this->db->cancelTransaction();
return false;
}
$task_id = $this->db->getConnection()->getLastId();
// Duplicate subtasks
if (! $this->subTask->duplicate($task['id'], $task_id)) {
$this->db->cancelTransaction();
return false;
}
$this->db->closeTransaction();
// Trigger events
$this->event->trigger(Task::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $values);
$this->event->trigger(Task::EVENT_CREATE, array('task_id' => $task_id) + $values);
return $task_id;
}
/**
* Duplicate a task to the same project
*
* @access public
* @param array $task Task data
* @return integer|boolean
*/
public function duplicateToSameProject($task)
{
return $this->copy($task);
}
/**
* Duplicate a task to another project (always copy to the first column)
*
* @access public
* @param integer $project_id Destination project id
* @param array $task Task data
* @return integer|boolean
*/
public function duplicateToAnotherProject($project_id, array $task)
{
return $this->copy($task, array(
'project_id' => $project_id,
'column_id' => $this->board->getFirstColumn($project_id),
));
}
/**
* Get a the task id from a text
*

View file

@ -0,0 +1,70 @@
<?php
namespace Model;
/**
* Task Creation
*
* @package model
* @author Frederic Guillot
*/
class TaskCreation extends Base
{
/**
* Create a task
*
* @access public
* @param array $values Form values
* @return integer
*/
public function create(array $values)
{
$this->prepare($values);
$task_id = $this->persist(Task::TABLE, $values);
if ($task_id) {
$this->fireEvents($task_id, $values);
}
return (int) $task_id;
}
/**
* Prepare data
*
* @access public
* @param array $values Form values
*/
public function prepare(array &$values)
{
$this->dateParser->convert($values, array('date_due', 'date_started'));
$this->removeFields($values, array('another_task'));
$this->resetFields($values, array('owner_id', 'owner_id', 'date_due', 'score', 'category_id', 'time_estimated'));
if (empty($values['column_id'])) {
$values['column_id'] = $this->board->getFirstColumn($values['project_id']);
}
if (empty($values['color_id'])) {
$values['color_id'] = $this->color->getDefaultColor();
}
$values['date_creation'] = time();
$values['date_modification'] = $values['date_creation'];
$values['position'] = $this->taskFinder->countByColumnId($values['project_id'], $values['column_id']) + 1;
}
/**
* Fire events
*
* @access private
* @param integer $task_id Task id
* @param array $values Form values
*/
private function fireEvents($task_id, array $values)
{
$values['task_id'] = $task_id;
$this->event->trigger(Task::EVENT_CREATE_UPDATE, $values);
$this->event->trigger(Task::EVENT_CREATE, $values);
}
}

View file

@ -0,0 +1,145 @@
<?php
namespace Model;
/**
* Task Duplication
*
* @package model
* @author Frederic Guillot
*/
class TaskDuplication extends Base
{
/**
* Fields to copy when duplicating a task
*
* @access private
* @var array
*/
private $fields_to_duplicate = array(
'title',
'description',
'date_due',
'color_id',
'project_id',
'column_id',
'owner_id',
'score',
'category_id',
'time_estimated',
);
/**
* Duplicate a task to the same project
*
* @access public
* @param integer $task_id Task id
* @return boolean|integer Duplicated task id
*/
public function duplicate($task_id)
{
return $this->save($task_id, $this->copyFields($task_id));
}
/**
* Duplicate a task to another project
*
* @access public
* @param integer $task_id Task id
* @param integer $project_id Project id
* @return boolean|integer Duplicated task id
*/
public function duplicateToProject($task_id, $project_id)
{
$values = $this->copyFields($task_id);
$values['project_id'] = $project_id;
$values['column_id'] = $this->board->getFirstColumn($project_id);
$this->checkDestinationProjectValues($values);
return $this->save($task_id, $values);
}
/**
* Move a task to another project
*
* @access public
* @param integer $task_id Task id
* @param integer $project_id Project id
* @return boolean
*/
public function moveToProject($task_id, $project_id)
{
$task = $this->taskFinder->getById($task_id);
$values = array();
$values['is_active'] = 1;
$values['project_id'] = $project_id;
$values['column_id'] = $this->board->getFirstColumn($project_id);
$values['position'] = $this->taskFinder->countByColumnId($project_id, $values['column_id']) + 1;
$values['owner_id'] = $task['owner_id'];
$values['category_id'] = $task['category_id'];
$this->checkDestinationProjectValues($values);
return $this->db->table(Task::TABLE)->eq('id', $task['id'])->update($values);
}
/**
* Check if the assignee and the category are available in the destination project
*
* @access private
* @param array $values
*/
private function checkDestinationProjectValues(&$values)
{
// Check if the assigned user is allowed for the destination project
if ($values['owner_id'] > 0 && ! $this->projectPermission->isUserAllowed($values['project_id'], $values['owner_id'])) {
$values['owner_id'] = 0;
}
// Check if the category exists for the destination project
if ($values['category_id'] > 0) {
$category_name = $this->category->getNameById($values['category_id']);
$values['category_id'] = $this->category->getIdByName($values['project_id'], $category_name);
}
}
/**
* Duplicate fields for the new task
*
* @access private
* @param integer $task_id Task id
* @return array
*/
private function copyFields($task_id)
{
$task = $this->taskFinder->getById($task_id);
$values = array();
foreach ($this->fields_to_duplicate as $field) {
$values[$field] = $task[$field];
}
return $values;
}
/**
* Create the new task and duplicate subtasks
*
* @access private
* @param integer $task_id Task id
* @param array $values Form values
* @return boolean|integer
*/
private function save($task_id, array $values)
{
$new_task_id = $this->taskCreation->create($values);
if ($new_task_id) {
$this->subTask->duplicate($task_id, $new_task_id);
}
return $new_task_id;
}
}

View file

@ -15,10 +15,10 @@ class TaskFinder extends Base
/**
* Common request to fetch a list of tasks
*
* @access private
* @access public
* @return \PicoDb\Table
*/
private function prepareRequestList()
public function getQuery()
{
return $this->db
->table(Task::TABLE)
@ -50,51 +50,6 @@ class TaskFinder extends Base
->join(User::TABLE, 'id', 'owner_id');
}
/**
* Task search with pagination
*
* @access public
* @param integer $project_id Project id
* @param string $search Search terms
* @param integer $offset Offset
* @param integer $limit Limit
* @param string $column Sorting column
* @param string $direction Sorting direction
* @return array
*/
public function search($project_id, $search, $offset = 0, $limit = 25, $column = 'tasks.id', $direction = 'DESC')
{
return $this->prepareRequestList()
->eq('project_id', $project_id)
->like('title', '%'.$search.'%')
->offset($offset)
->limit($limit)
->orderBy($column, $direction)
->findAll();
}
/**
* Get all completed tasks with pagination
*
* @access public
* @param integer $project_id Project id
* @param integer $offset Offset
* @param integer $limit Limit
* @param string $column Sorting column
* @param string $direction Sorting direction
* @return array
*/
public function getClosedTasks($project_id, $offset = 0, $limit = 25, $column = 'tasks.date_completed', $direction = 'DESC')
{
return $this->prepareRequestList()
->eq('project_id', $project_id)
->eq('is_active', Task::STATUS_CLOSED)
->offset($offset)
->limit($limit)
->orderBy($column, $direction)
->findAll();
}
/**
* Get all tasks shown on the board (sorted by position)
*
@ -104,40 +59,13 @@ class TaskFinder extends Base
*/
public function getTasksOnBoard($project_id)
{
return $this->prepareRequestList()
return $this->getQuery()
->eq('project_id', $project_id)
->eq('is_active', Task::STATUS_OPEN)
->asc('tasks.position')
->findAll();
}
/**
* Get all open tasks for a given user
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function getAllTasksByUser($user_id)
{
return $this->db
->table(Task::TABLE)
->columns(
'tasks.id',
'tasks.title',
'tasks.date_due',
'tasks.date_creation',
'tasks.project_id',
'tasks.color_id',
'projects.name AS project_name'
)
->join(Project::TABLE, 'id', 'project_id')
->eq('tasks.owner_id', $user_id)
->eq('tasks.is_active', Task::STATUS_OPEN)
->asc('tasks.id')
->findAll();
}
/**
* Get all tasks for a given project and status
*
@ -295,22 +223,6 @@ class TaskFinder extends Base
->count();
}
/**
* Count the number of tasks for a custom search
*
* @access public
* @param integer $project_id Project id
* @param string $search Search terms
* @return integer
*/
public function countSearch($project_id, $search)
{
return $this->db->table(Task::TABLE)
->eq('project_id', $project_id)
->like('title', '%'.$search.'%')
->count();
}
/**
* Return true if the task exists
*

View file

@ -0,0 +1,73 @@
<?php
namespace Model;
/**
* Task Modification
*
* @package model
* @author Frederic Guillot
*/
class TaskModification extends Base
{
/**
* Update a task
*
* @access public
* @param array $values
* @param boolean $fire_events
* @return boolean
*/
public function update(array $values, $fire_events = true)
{
$original_task = $this->taskFinder->getById($values['id']);
$this->prepare($values);
$result = $this->db->table(Task::TABLE)->eq('id', $original_task['id'])->update($values);
if ($result && $fire_events) {
$this->fireEvents($original_task, $values);
}
return $result;
}
/**
* Fire events
*
* @access public
* @param array $task
* @param array $new_values
*/
public function fireEvents(array $task, array $new_values)
{
$event_data = array_merge($task, $new_values, array('task_id' => $task['id']));
if (isset($new_values['owner_id']) && $task['owner_id'] != $new_values['owner_id']) {
$events = array(Task::EVENT_ASSIGNEE_CHANGE);
}
else {
$events = array(Task::EVENT_CREATE_UPDATE, Task::EVENT_UPDATE);
}
foreach ($events as $event) {
$this->event->trigger($event, $event_data);
}
}
/**
* Prepare data before task modification
*
* @access public
* @param array $values Form values
*/
public function prepare(array &$values)
{
$this->dateParser->convert($values, array('date_due', 'date_started'));
$this->removeFields($values, array('another_task', 'id'));
$this->resetFields($values, array('date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent'));
$this->convertIntegerFields($values, array('is_active'));
$values['date_modification'] = time();
}
}

View file

@ -0,0 +1,139 @@
<?php
namespace Model;
/**
* Task Paginator model
*
* @package model
* @author Frederic Guillot
*/
class TaskPaginator extends Base
{
/**
* Task search with pagination
*
* @access public
* @param integer $project_id Project id
* @param string $search Search terms
* @param integer $offset Offset
* @param integer $limit Limit
* @param string $column Sorting column
* @param string $direction Sorting direction
* @return array
*/
public function searchTasks($project_id, $search, $offset = 0, $limit = 25, $column = 'tasks.id', $direction = 'DESC')
{
return $this->taskFinder->getQuery()
->eq('project_id', $project_id)
->like('title', '%'.$search.'%')
->offset($offset)
->limit($limit)
->orderBy($column, $direction)
->findAll();
}
/**
* Count the number of tasks for a custom search
*
* @access public
* @param integer $project_id Project id
* @param string $search Search terms
* @return integer
*/
public function countSearchTasks($project_id, $search)
{
return $this->db->table(Task::TABLE)
->eq('project_id', $project_id)
->like('title', '%'.$search.'%')
->count();
}
/**
* Get all completed tasks with pagination
*
* @access public
* @param integer $project_id Project id
* @param integer $offset Offset
* @param integer $limit Limit
* @param string $column Sorting column
* @param string $direction Sorting direction
* @return array
*/
public function closedTasks($project_id, $offset = 0, $limit = 25, $column = 'tasks.date_completed', $direction = 'DESC')
{
return $this->taskFinder->getQuery()
->eq('project_id', $project_id)
->eq('is_active', Task::STATUS_CLOSED)
->offset($offset)
->limit($limit)
->orderBy($column, $direction)
->findAll();
}
/**
* Count all closed tasks
*
* @access public
* @param integer $project_id Project id
* @param array $status List of status id
* @return integer
*/
public function countClosedTasks($project_id)
{
return $this->db
->table(Task::TABLE)
->eq('project_id', $project_id)
->eq('is_active', Task::STATUS_CLOSED)
->count();
}
/**
* Get all open tasks for a given user
*
* @access public
* @param integer $user_id User id
* @param integer $offset Offset
* @param integer $limit Limit
* @param string $column Sorting column
* @param string $direction Sorting direction
* @return array
*/
public function userTasks($user_id, $offset = 0, $limit = 25, $column = 'tasks.id', $direction = 'ASC')
{
return $this->db
->table(Task::TABLE)
->columns(
'tasks.id',
'tasks.title',
'tasks.date_due',
'tasks.date_creation',
'tasks.project_id',
'tasks.color_id',
'projects.name AS project_name'
)
->join(Project::TABLE, 'id', 'project_id')
->eq('tasks.owner_id', $user_id)
->eq('tasks.is_active', Task::STATUS_OPEN)
->offset($offset)
->limit($limit)
->orderBy($column, $direction)
->findAll();
}
/**
* Count all tasks assigned to the user
*
* @access public
* @param integer $user_id User id
* @return integer
*/
public function countUserTasks($user_id)
{
return $this->db
->table(Task::TABLE)
->eq('owner_id', $user_id)
->eq('is_active', Task::STATUS_OPEN)
->count();
}
}

View file

@ -0,0 +1,136 @@
<?php
namespace Model;
/**
* Task Position
*
* @package model
* @author Frederic Guillot
*/
class TaskPosition extends Base
{
/**
* Move a task to another column or to another position
*
* @access public
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param integer $column_id Column id
* @param integer $position Position (must be >= 1)
* @return boolean
*/
public function movePosition($project_id, $task_id, $column_id, $position)
{
$original_task = $this->taskFinder->getById($task_id);
$positions = $this->calculatePositions($project_id, $task_id, $column_id, $position);
if ($positions === false || ! $this->savePositions($positions)) {
return false;
}
$this->fireEvents($original_task, $column_id, $position);
return true;
}
/**
* Calculate the new position of all tasks
*
* @access public
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param integer $column_id Column id
* @param integer $position Position (must be >= 1)
* @return array|boolean
*/
public function calculatePositions($project_id, $task_id, $column_id, $position)
{
// The position can't be lower than 1
if ($position < 1) {
return false;
}
$board = $this->db->table(Board::TABLE)->eq('project_id', $project_id)->asc('position')->findAllByColumn('id');
$columns = array();
// For each column fetch all tasks ordered by position
foreach ($board as $board_column_id) {
$columns[$board_column_id] = $this->db->table(Task::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);
return $columns;
}
/**
* Save task positions
*
* @access private
* @param array $columns Sorted tasks
* @return boolean
*/
private function savePositions(array $columns)
{
return $this->db->transaction(function ($db) use ($columns) {
foreach ($columns as $column_id => $column) {
$position = 1;
foreach ($column as $task_id) {
$result = $db->table(Task::TABLE)->eq('id', $task_id)->update(array(
'position' => $position,
'column_id' => $column_id
));
if (! $result) {
return false;
}
$position++;
}
}
});
}
/**
* Fire events
*
* @access public
* @param array $task
* @param integer $new_column_id
* @param integer $new_position
*/
public function fireEvents(array $task, $new_column_id, $new_position)
{
$event_data = array(
'task_id' => $task['id'],
'project_id' => $task['project_id'],
'position' => $new_position,
'column_id' => $new_column_id,
);
if ($task['column_id'] != $new_column_id) {
$this->event->trigger(Task::EVENT_MOVE_COLUMN, $event_data);
}
else if ($task['position'] != $new_position) {
$this->event->trigger(Task::EVENT_MOVE_POSITION, $event_data);
}
}
}

View file

@ -0,0 +1,112 @@
<?php
namespace Model;
/**
* Task Status
*
* @package model
* @author Frederic Guillot
*/
class TaskStatus extends Base
{
/**
* Return true if the task is closed
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function isClosed($task_id)
{
return $this->checkStatus($task_id, Task::STATUS_CLOSED);
}
/**
* Return true if the task is open
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function isOpen($task_id)
{
return $this->checkStatus($task_id, Task::STATUS_OPEN);
}
/**
* Mark a task closed
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function close($task_id)
{
return $this->changeStatus($task_id, Task::STATUS_CLOSED, time(), Task::EVENT_CLOSE);
}
/**
* Mark a task open
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function open($task_id)
{
return $this->changeStatus($task_id, Task::STATUS_OPEN, 0, Task::EVENT_OPEN);
}
/**
* Common method to change the status of task
*
* @access private
* @param integer $task_id Task id
* @param integer $status Task status
* @param integer $date_completed Timestamp
* @param string $event Event name
* @return boolean
*/
private function changeStatus($task_id, $status, $date_completed, $event)
{
if (! $this->taskFinder->exists($task_id)) {
return false;
}
$result = $this->db
->table(Task::TABLE)
->eq('id', $task_id)
->update(array(
'is_active' => $status,
'date_completed' => $date_completed,
'date_modification' => time(),
));
if ($result) {
$this->event->trigger(
$event,
array('task_id' => $task_id) + $this->taskFinder->getById($task_id)
);
}
return $result;
}
/**
* Check the status of task
*
* @access private
* @param integer $task_id Task id
* @param integer $status Task status
* @return boolean
*/
private function checkStatus($task_id, $status)
{
return $this->db
->table(Task::TABLE)
->eq('id', $task_id)
->eq('is_active', $status)
->count() === 1;
}
}

View file

@ -70,7 +70,6 @@ class TaskValidator extends Base
{
$rules = array(
new Validators\Required('id', t('The id is required')),
new Validators\Required('description', t('The description is required')),
);
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));

View file

@ -255,12 +255,12 @@ class User extends Base
*
* @access public
* @param array $values Form values
* @return boolean
* @return boolean|integer
*/
public function create(array $values)
{
$this->prepare($values);
return $this->db->table(self::TABLE)->save($values);
return $this->persist(self::TABLE, $values);
}
/**
@ -292,15 +292,29 @@ class User extends Base
*/
public function remove($user_id)
{
$this->db->startTransaction();
return $this->db->transaction(function ($db) use ($user_id) {
// All tasks assigned to this user will be unassigned
$this->db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => 0));
$result = $this->db->table(self::TABLE)->eq('id', $user_id)->remove();
// All assigned tasks are now unassigned
if (! $db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => 0))) {
return false;
}
$this->db->closeTransaction();
// All private projects are removed
$project_ids = $db->table(Project::TABLE)
->eq('is_private', 1)
->eq(ProjectPermission::TABLE.'.user_id', $user_id)
->join(ProjectPermission::TABLE, 'project_id', 'id')
->findAllByColumn(Project::TABLE.'.id');
return $result;
if (! empty($project_ids)) {
$db->table(Project::TABLE)->in('id', $project_ids)->remove();
}
// Finally remove the user
if (! $db->table(User::TABLE)->eq('id', $user_id)->remove()) {
return false;
}
});
}
/**

View file

@ -93,7 +93,7 @@ class Webhook extends Base
Task::EVENT_ASSIGNEE_CHANGE,
);
$listener = new WebhookListener($this->registry);
$listener = new WebhookListener($this->container);
$listener->setUrl($this->url_task_modification);
foreach ($events as $event_name) {
@ -108,7 +108,7 @@ class Webhook extends Base
*/
public function attachCreateEvents()
{
$listener = new WebhookListener($this->registry);
$listener = new WebhookListener($this->container);
$listener->setUrl($this->url_task_creation);
$this->event->attach(Task::EVENT_CREATE, $listener);

View file

@ -5,7 +5,30 @@ namespace Schema;
use PDO;
use Core\Security;
const VERSION = 34;
const VERSION = 36;
function version_36($pdo)
{
$pdo->exec('ALTER TABLE tasks MODIFY title VARCHAR(255) NOT NULL');
}
function version_35($pdo)
{
$pdo->exec("
CREATE TABLE project_daily_summaries (
id INT NOT NULL AUTO_INCREMENT,
day CHAR(10) NOT NULL,
project_id INT NOT NULL,
column_id INT NOT NULL,
total INT NOT NULL DEFAULT 0,
PRIMARY KEY(id),
FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec('CREATE UNIQUE INDEX project_daily_column_stats_idx ON project_daily_summaries(day, project_id, column_id)');
}
function version_34($pdo)
{

View file

@ -5,7 +5,29 @@ namespace Schema;
use PDO;
use Core\Security;
const VERSION = 15;
const VERSION = 17;
function version_17($pdo)
{
$pdo->exec('ALTER TABLE tasks ALTER COLUMN title SET NOT NULL');
}
function version_16($pdo)
{
$pdo->exec("
CREATE TABLE project_daily_summaries (
id SERIAL PRIMARY KEY,
day CHAR(10) NOT NULL,
project_id INTEGER NOT NULL,
column_id INTEGER NOT NULL,
total INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
)
");
$pdo->exec('CREATE UNIQUE INDEX project_daily_column_stats_idx ON project_daily_summaries(day, project_id, column_id)');
}
function version_15($pdo)
{

View file

@ -5,7 +5,24 @@ namespace Schema;
use Core\Security;
use PDO;
const VERSION = 34;
const VERSION = 35;
function version_35($pdo)
{
$pdo->exec("
CREATE TABLE project_daily_summaries (
id INTEGER PRIMARY KEY,
day TEXT NOT NULL,
project_id INTEGER NOT NULL,
column_id INTEGER NOT NULL,
total INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
)
");
$pdo->exec('CREATE UNIQUE INDEX project_daily_column_stats_idx ON project_daily_summaries(day, project_id, column_id)');
}
function version_34($pdo)
{
@ -440,7 +457,7 @@ function version_1($pdo)
$pdo->exec("
CREATE TABLE tasks (
id INTEGER PRIMARY KEY,
title TEXT,
title TEXT NOT NULL,
description TEXT,
date_creation INTEGER,
color_id TEXT,

View file

@ -0,0 +1,100 @@
<?php
namespace ServiceProvider;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use PicoDb\Database as Dbal;
class Database implements ServiceProviderInterface
{
public function register(Container $container)
{
$container['db'] = $this->getInstance();
}
/**
* Setup the database driver and execute schema migration
*
* @return PicoDb\Database
*/
public function getInstance()
{
switch (DB_DRIVER) {
case 'sqlite':
$db = $this->getSqliteInstance();
break;
case 'mysql':
$db = $this->getMysqlInstance();
break;
case 'postgres':
$db = $this->getPostgresInstance();
break;
default:
die('Database driver not supported');
}
if ($db->schema()->check(\Schema\VERSION)) {
return $db;
}
else {
$errors = $db->getLogMessages();
die('Unable to migrate database schema: <br/><br/><strong>'.(isset($errors[0]) ? $errors[0] : 'Unknown error').'</strong>');
}
}
/**
* Setup the Sqlite database driver
*
* @return PicoDb\Database
*/
function getSqliteInstance()
{
require_once __DIR__.'/../Schema/Sqlite.php';
return new Dbal(array(
'driver' => 'sqlite',
'filename' => DB_FILENAME
));
}
/**
* Setup the Mysql database driver
*
* @return PicoDb\Database
*/
function getMysqlInstance()
{
require_once __DIR__.'/../Schema/Mysql.php';
return new Dbal(array(
'driver' => 'mysql',
'hostname' => DB_HOSTNAME,
'username' => DB_USERNAME,
'password' => DB_PASSWORD,
'database' => DB_NAME,
'charset' => 'utf8',
));
}
/**
* Setup the Postgres database driver
*
* @return PicoDb\Database
*/
public function getPostgresInstance()
{
require_once __DIR__.'/../Schema/Postgres.php';
return new Dbal(array(
'driver' => 'postgres',
'hostname' => DB_HOSTNAME,
'username' => DB_USERNAME,
'password' => DB_PASSWORD,
'database' => DB_NAME,
));
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace ServiceProvider;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Core\Event as EventDispatcher;
class Event implements ServiceProviderInterface
{
public function register(Container $container)
{
$container['event'] = new EventDispatcher;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace ServiceProvider;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogHandler;
class Logging implements ServiceProviderInterface
{
public function register(Container $container)
{
$logger = new Logger('app');
$logger->pushHandler(new StreamHandler(__DIR__.'/../../data/debug.log', Logger::DEBUG));
$logger->pushHandler(new SyslogHandler('kanboard', LOG_USER, Logger::DEBUG));
$container['logger'] = $logger;
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace ServiceProvider;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Swift_SmtpTransport;
use Swift_SendmailTransport;
use Swift_MailTransport;
class Mailer implements ServiceProviderInterface
{
public function register(Container $container)
{
$container['mailer'] = $this->getInstance();
}
public function getInstance()
{
switch (MAIL_TRANSPORT) {
case 'smtp':
$transport = Swift_SmtpTransport::newInstance(MAIL_SMTP_HOSTNAME, MAIL_SMTP_PORT);
$transport->setUsername(MAIL_SMTP_USERNAME);
$transport->setPassword(MAIL_SMTP_PASSWORD);
$transport->setEncryption(MAIL_SMTP_ENCRYPTION);
break;
case 'sendmail':
$transport = Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND);
break;
default:
$transport = Swift_MailTransport::newInstance();
}
return $transport;
}
}

View file

@ -3,8 +3,10 @@
</div>
<h3><?= t('Choose an event') ?></h3>
<form method="post" action="?controller=action&amp;action=params&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<form method="post" action="<?= Helper\u('action', 'params', array('project_id' => $project['id'])) ?>">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_hidden('action_name', $values) ?>
@ -17,6 +19,7 @@
<div class="form-actions">
<input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/>
<?= t('or') ?> <a href="?controller=action&amp;action=index&amp;project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a>
<?= t('or') ?>
<?= Helper\a(t('cancel'), 'action', 'index', array('project_id' => $project['id'])) ?>
</div>
</form>

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