-
+
- = Helper\a(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?> +
diff --git a/sources/app/Action/Base.php b/sources/app/Action/Base.php index be9c3d4..a2b07e3 100644 --- a/sources/app/Action/Base.php +++ b/sources/app/Action/Base.php @@ -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); } /** diff --git a/sources/app/Action/CommentCreation.php b/sources/app/Action/CommentCreation.php index 3543403..5dbe32f 100644 --- a/sources/app/Action/CommentCreation.php +++ b/sources/app/Action/CommentCreation.php @@ -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'], diff --git a/sources/app/Action/TaskAssignCategoryColor.php b/sources/app/Action/TaskAssignCategoryColor.php index be15f65..4134b58 100644 --- a/sources/app/Action/TaskAssignCategoryColor.php +++ b/sources/app/Action/TaskAssignCategoryColor.php @@ -67,7 +67,7 @@ class TaskAssignCategoryColor extends Base 'category_id' => $this->getParam('category_id'), ); - return $this->task->update($values, false); + return $this->taskModification->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignCategoryLabel.php b/sources/app/Action/TaskAssignCategoryLabel.php index 5e1b025..da41a31 100644 --- a/sources/app/Action/TaskAssignCategoryLabel.php +++ b/sources/app/Action/TaskAssignCategoryLabel.php @@ -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); } /** diff --git a/sources/app/Action/TaskAssignColorCategory.php b/sources/app/Action/TaskAssignColorCategory.php index f5a9ac5..68bca5d 100644 --- a/sources/app/Action/TaskAssignColorCategory.php +++ b/sources/app/Action/TaskAssignColorCategory.php @@ -67,7 +67,7 @@ class TaskAssignColorCategory extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->task->update($values, false); + return $this->taskModification->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignColorUser.php b/sources/app/Action/TaskAssignColorUser.php index 0068018..d419ab4 100644 --- a/sources/app/Action/TaskAssignColorUser.php +++ b/sources/app/Action/TaskAssignColorUser.php @@ -68,7 +68,7 @@ class TaskAssignColorUser extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->task->update($values, false); + return $this->taskModification->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignCurrentUser.php b/sources/app/Action/TaskAssignCurrentUser.php index 7a9cf70..9317bf8 100644 --- a/sources/app/Action/TaskAssignCurrentUser.php +++ b/sources/app/Action/TaskAssignCurrentUser.php @@ -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); } /** diff --git a/sources/app/Action/TaskAssignSpecificUser.php b/sources/app/Action/TaskAssignSpecificUser.php index f70459b..c3b979c 100644 --- a/sources/app/Action/TaskAssignSpecificUser.php +++ b/sources/app/Action/TaskAssignSpecificUser.php @@ -68,7 +68,7 @@ class TaskAssignSpecificUser extends Base 'owner_id' => $this->getParam('user_id'), ); - return $this->task->update($values, false); + return $this->taskModification->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignUser.php b/sources/app/Action/TaskAssignUser.php index 29ea91e..d01c407 100644 --- a/sources/app/Action/TaskAssignUser.php +++ b/sources/app/Action/TaskAssignUser.php @@ -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); } /** diff --git a/sources/app/Action/TaskClose.php b/sources/app/Action/TaskClose.php index f71d4b0..6cf9be0 100644 --- a/sources/app/Action/TaskClose.php +++ b/sources/app/Action/TaskClose.php @@ -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']); } /** diff --git a/sources/app/Action/TaskCreation.php b/sources/app/Action/TaskCreation.php index 41d0200..0c79168 100644 --- a/sources/app/Action/TaskCreation.php +++ b/sources/app/Action/TaskCreation.php @@ -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'], diff --git a/sources/app/Action/TaskDuplicateAnotherProject.php b/sources/app/Action/TaskDuplicateAnotherProject.php index 4ab8853..55ebc76 100644 --- a/sources/app/Action/TaskDuplicateAnotherProject.php +++ b/sources/app/Action/TaskDuplicateAnotherProject.php @@ -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')); } /** diff --git a/sources/app/Action/TaskMoveAnotherProject.php b/sources/app/Action/TaskMoveAnotherProject.php index d852f56..ee21299 100644 --- a/sources/app/Action/TaskMoveAnotherProject.php +++ b/sources/app/Action/TaskMoveAnotherProject.php @@ -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')); } /** diff --git a/sources/app/Action/TaskOpen.php b/sources/app/Action/TaskOpen.php index 6847856..fc29e9e 100644 --- a/sources/app/Action/TaskOpen.php +++ b/sources/app/Action/TaskOpen.php @@ -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']); } /** diff --git a/sources/app/Auth/Base.php b/sources/app/Auth/Base.php index e174ff8..9633af4 100644 --- a/sources/app/Auth/Base.php +++ b/sources/app/Auth/Base.php @@ -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); } } diff --git a/sources/app/Auth/GitHub.php b/sources/app/Auth/GitHub.php index 096d410..034f926 100644 --- a/sources/app/Auth/GitHub.php +++ b/sources/app/Auth/GitHub.php @@ -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; diff --git a/sources/app/Auth/Google.php b/sources/app/Auth/Google.php index 2bed5b0..587ecde 100644 --- a/sources/app/Auth/Google.php +++ b/sources/app/Auth/Google.php @@ -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; diff --git a/sources/app/Auth/Ldap.php b/sources/app/Auth/Ldap.php index 4f20998..82307e8 100644 --- a/sources/app/Auth/Ldap.php +++ b/sources/app/Auth/Ldap.php @@ -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; } diff --git a/sources/app/Auth/RememberMe.php b/sources/app/Auth/RememberMe.php index 380abbe..cb8a9b4 100644 --- a/sources/app/Auth/RememberMe.php +++ b/sources/app/Auth/RememberMe.php @@ -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 ); } diff --git a/sources/app/Console/Base.php b/sources/app/Console/Base.php new file mode 100644 index 0000000..f955b42 --- /dev/null +++ b/sources/app/Console/Base.php @@ -0,0 +1,57 @@ +container = $container; + } + + /** + * Load automatically models + * + * @access public + * @param string $name Model name + * @return mixed + */ + public function __get($name) + { + return Tool::loadModel($this->container, $name); + } +} diff --git a/sources/app/Console/ProjectDailySummaryCalculation.php b/sources/app/Console/ProjectDailySummaryCalculation.php new file mode 100644 index 0000000..04c4083 --- /dev/null +++ b/sources/app/Console/ProjectDailySummaryCalculation.php @@ -0,0 +1,29 @@ +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')); + } + } +} diff --git a/sources/app/Console/ProjectDailySummaryExport.php b/sources/app/Console/ProjectDailySummaryExport.php new file mode 100644 index 0000000..6b96fdd --- /dev/null +++ b/sources/app/Console/ProjectDailySummaryExport.php @@ -0,0 +1,35 @@ +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); + } + } +} diff --git a/sources/app/Console/TaskExport.php b/sources/app/Console/TaskExport.php new file mode 100644 index 0000000..dea71fe --- /dev/null +++ b/sources/app/Console/TaskExport.php @@ -0,0 +1,35 @@ +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); + } + } +} diff --git a/sources/app/Console/TaskOverdueNotification.php b/sources/app/Console/TaskOverdueNotification.php new file mode 100644 index 0000000..aa70fd0 --- /dev/null +++ b/sources/app/Console/TaskOverdueNotification.php @@ -0,0 +1,69 @@ +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(); + } +} diff --git a/sources/app/Controller/Action.php b/sources/app/Controller/Action.php index 714c87f..22358cb 100644 --- a/sources/app/Controller/Action.php +++ b/sources/app/Controller/Action.php @@ -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') ))); } diff --git a/sources/app/Controller/Analytic.php b/sources/app/Controller/Analytic.php new file mode 100644 index 0000000..6c49089 --- /dev/null +++ b/sources/app/Controller/Analytic.php @@ -0,0 +1,128 @@ +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']), + ))); + } + } +} diff --git a/sources/app/Controller/App.php b/sources/app/Controller/App.php index feec422..c88fd92 100644 --- a/sources/app/Controller/App.php +++ b/sources/app/Controller/App.php @@ -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('
'.t('Nothing to preview...').'
'); + } + else { + $this->response->html(Helper\markdown($payload['text'])); + } + } + } diff --git a/sources/app/Controller/Base.php b/sources/app/Controller/Base.php index a8e22fd..5027cf3 100644 --- a/sources/app/Controller/Base.php +++ b/sources/app/Controller/Base.php @@ -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'].' > '.$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'].' > '.$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); } /** diff --git a/sources/app/Controller/Board.php b/sources/app/Controller/Board.php index d49ad02..7d498f8 100644 --- a/sources/app/Controller/Board.php +++ b/sources/app/Controller/Board.php @@ -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 + ))); + } } diff --git a/sources/app/Controller/Category.php b/sources/app/Controller/Category.php index 3832229..27c0d9f 100644 --- a/sources/app/Controller/Category.php +++ b/sources/app/Controller/Category.php @@ -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') ))); } diff --git a/sources/app/Controller/Comment.php b/sources/app/Controller/Comment.php index a9032ed..fb21353 100644 --- a/sources/app/Controller/Comment.php +++ b/sources/app/Controller/Comment.php @@ -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') ))); } diff --git a/sources/app/Controller/Config.php b/sources/app/Controller/Config.php index 7c8373c..199259d 100644 --- a/sources/app/Controller/Config.php +++ b/sources/app/Controller/Config.php @@ -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').' > '.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').' > '.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').' > '.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').' > '.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').' > '.t('API'), ))); } diff --git a/sources/app/Controller/File.php b/sources/app/Controller/File.php index 3c8c32d..ae44cac 100644 --- a/sources/app/Controller/File.php +++ b/sources/app/Controller/File.php @@ -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') ))); } } diff --git a/sources/app/Controller/Project.php b/sources/app/Controller/Project.php index c5f1649..83c81ca 100644 --- a/sources/app/Controller/Project.php +++ b/sources/app/Controller/Project.php @@ -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); } } diff --git a/sources/app/Controller/Subtask.php b/sources/app/Controller/Subtask.php index 48f0d6e..948f3c7 100644 --- a/sources/app/Controller/Subtask.php +++ b/sources/app/Controller/Subtask.php @@ -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.')); } diff --git a/sources/app/Controller/Task.php b/sources/app/Controller/Task.php index 1b20cf1..8d38317 100644 --- a/sources/app/Controller/Task.php +++ b/sources/app/Controller/Task.php @@ -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'].' > '.$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'].' > '.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') ))); } } diff --git a/sources/app/Controller/User.php b/sources/app/Controller/User.php index e757fa8..93b5ca1 100644 --- a/sources/app/Controller/User.php +++ b/sources/app/Controller/User.php @@ -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, diff --git a/sources/app/Controller/Webhook.php b/sources/app/Controller/Webhook.php index fa9c583..dcd66a1 100644 --- a/sources/app/Controller/Webhook.php +++ b/sources/app/Controller/Webhook.php @@ -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'; diff --git a/sources/app/Core/Cli.php b/sources/app/Core/Cli.php deleted file mode 100644 index 13533b9..0000000 --- a/sources/app/Core/Cli.php +++ /dev/null @@ -1,75 +0,0 @@ -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); - } -} diff --git a/sources/app/Core/Event.php b/sources/app/Core/Event.php index a32499d..935f8b9 100644 --- a/sources/app/Core/Event.php +++ b/sources/app/Core/Event.php @@ -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]); } /** diff --git a/sources/app/Core/Loader.php b/sources/app/Core/Loader.php deleted file mode 100644 index 151081c..0000000 --- a/sources/app/Core/Loader.php +++ /dev/null @@ -1,62 +0,0 @@ -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; - } -} diff --git a/sources/app/Core/Registry.php b/sources/app/Core/Registry.php deleted file mode 100644 index d8b9063..0000000 --- a/sources/app/Core/Registry.php +++ /dev/null @@ -1,83 +0,0 @@ -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]; - } -} diff --git a/sources/app/Core/Request.php b/sources/app/Core/Request.php index a4c426f..c7ca318 100644 --- a/sources/app/Core/Request.php +++ b/sources/app/Core/Request.php @@ -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 * diff --git a/sources/app/Core/Response.php b/sources/app/Core/Response.php index 347cdde..6534d64 100644 --- a/sources/app/Core/Response.php +++ b/sources/app/Core/Response.php @@ -246,7 +246,7 @@ class Response */ public function hsts() { - if (Tool::isHTTPS()) { + if (Request::isHTTPS()) { header('Strict-Transport-Security: max-age=31536000'); } } diff --git a/sources/app/Core/Router.php b/sources/app/Core/Router.php index c9af6e2..36c11a0 100644 --- a/sources/app/Core/Router.php +++ b/sources/app/Core/Router.php @@ -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(); diff --git a/sources/app/Core/Session.php b/sources/app/Core/Session.php index 6028f0b..3305eca 100644 --- a/sources/app/Core/Session.php +++ b/sources/app/Core/Session.php @@ -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(); } diff --git a/sources/app/Core/Tool.php b/sources/app/Core/Tool.php index e54a0d3..c010d93 100644 --- a/sources/app/Core/Tool.php +++ b/sources/app/Core/Tool.php @@ -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]; } } diff --git a/sources/app/Event/Base.php b/sources/app/Event/Base.php index 745871a..0217fa0 100644 --- a/sources/app/Event/Base.php +++ b/sources/app/Event/Base.php @@ -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, '.')); } } diff --git a/sources/app/Event/ProjectActivityListener.php b/sources/app/Event/ProjectActivityListener.php index 8958bd2..75efe65 100644 --- a/sources/app/Event/ProjectActivityListener.php +++ b/sources/app/Event/ProjectActivityListener.php @@ -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 ); } diff --git a/sources/app/Event/ProjectDailySummaryListener.php b/sources/app/Event/ProjectDailySummaryListener.php new file mode 100644 index 0000000..cd593ab --- /dev/null +++ b/sources/app/Event/ProjectDailySummaryListener.php @@ -0,0 +1,28 @@ +projectDailySummary->updateTotals($data['project_id'], date('Y-m-d')); + } + + return false; + } +} diff --git a/sources/app/Locale/da_DK/translations.php b/sources/app/Locale/da_DK/translations.php index 5c8fb07..c4fcc1c 100644 --- a/sources/app/Locale/da_DK/translations.php +++ b/sources/app/Locale/da_DK/translations.php @@ -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' => '', ); diff --git a/sources/app/Locale/de_DE/translations.php b/sources/app/Locale/de_DE/translations.php index 7446348..9881223 100644 --- a/sources/app/Locale/de_DE/translations.php +++ b/sources/app/Locale/de_DE/translations.php @@ -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 #%d to the column "%s"' => '%s hat die Aufgabe #%d in die Spalte "%s" verschoben', '%s created the task #%d' => '%s hat die Aufgabe #%d angelegt', '%s closed the task #%d' => '%s hat die Aufgabe #%d geschlossen', - '%s created a subtask for the task #%d' => '%s hat eine Unteraufgabe für die Aufgabe #%d angelegt', - '%s updated a subtask for the task #%d' => '%s hat eine Unteraufgabe der Aufgabe #%d verändert', + '%s created a subtask for the task #%d' => '%s hat eine Teilaufgabe für die Aufgabe #%d angelegt', + '%s updated a subtask for the task #%d' => '%s hat eine Teilaufgabe der Aufgabe #%d verändert', 'Assigned to %s with an estimate of %s/%sh' => 'An %s zugewiesen mit einer Schätzung von %s/%s Stunden', 'Not assigned, estimate of %sh' => 'Nicht zugewiesen, Schätzung von %s Stunden', '%s updated a comment on the task #%d' => '%s hat einen Kommentat der Aufgabe #%d aktualisiert', @@ -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', ); diff --git a/sources/app/Locale/es_ES/translations.php b/sources/app/Locale/es_ES/translations.php index 479983e..a261832 100644 --- a/sources/app/Locale/es_ES/translations.php +++ b/sources/app/Locale/es_ES/translations.php @@ -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' => '', ); diff --git a/sources/app/Locale/fi_FI/translations.php b/sources/app/Locale/fi_FI/translations.php index 78a7c84..c0bc4bb 100644 --- a/sources/app/Locale/fi_FI/translations.php +++ b/sources/app/Locale/fi_FI/translations.php @@ -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' => '', ); diff --git a/sources/app/Locale/fr_FR/translations.php b/sources/app/Locale/fr_FR/translations.php index 5f7e793..94ed70d 100644 --- a/sources/app/Locale/fr_FR/translations.php +++ b/sources/app/Locale/fr_FR/translations.php @@ -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', ); diff --git a/sources/app/Locale/it_IT/translations.php b/sources/app/Locale/it_IT/translations.php index e0b2b89..b7132bc 100644 --- a/sources/app/Locale/it_IT/translations.php +++ b/sources/app/Locale/it_IT/translations.php @@ -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' => '', ); diff --git a/sources/app/Locale/ja_JP/translations.php b/sources/app/Locale/ja_JP/translations.php index 440c1e8..7a59ec0 100644 --- a/sources/app/Locale/ja_JP/translations.php +++ b/sources/app/Locale/ja_JP/translations.php @@ -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' => '', ); diff --git a/sources/app/Locale/pl_PL/translations.php b/sources/app/Locale/pl_PL/translations.php index a294de7..edc696b 100644 --- a/sources/app/Locale/pl_PL/translations.php +++ b/sources/app/Locale/pl_PL/translations.php @@ -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' => '', ); diff --git a/sources/app/Locale/pt_BR/translations.php b/sources/app/Locale/pt_BR/translations.php index 6986e78..7525445 100644 --- a/sources/app/Locale/pt_BR/translations.php +++ b/sources/app/Locale/pt_BR/translations.php @@ -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 #%d' => '', // '%s open the task #%d' => '', // '%s moved the task #%d to the position #%d in the column "%s"' => '', // '%s moved the task #%d to the column "%s"' => '', // '%s created the task #%d' => '', // '%s closed the task #%d' => '', - // '%s created a subtask for the task #%d' => '', - // '%s updated a subtask for the task #%d' => '', - // 'Assigned to %s with an estimate of %s/%sh' => '', - // 'Not assigned, estimate of %sh' => '', - // '%s updated a comment on the task #%d' => '', - // '%s commented the task #%d' => '', - // '%s\'s activity' => '', - // 'No activity.' => '', + '%s created a subtask for the task #%d' => '%s criou uma sub-tarefa para a tarefa #%d', + '%s updated a subtask for the task #%d' => '%s atualizou uma sub-tarefa da tarefa #%d', + 'Assigned to %s with an estimate of %s/%sh' => 'Designado para %s com tempo estimado de %s/%sh', + 'Not assigned, estimate of %sh' => 'Não designado, estimado em %sh', + '%s updated a comment on the task #%d' => '%s atualizou o comentário na tarefa #%d', + '%s commented the task #%d' => '%s comentou a tarefa #%d', + '%s\'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 #%d 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 #%d to %s' => '%s mudou a designação da tarefa #%d para %s', // '[%s][Column Change] %s (#%d)' => '', // '[%s][Position Change] %s (#%d)' => '', // '[%s][Assignee Change] %s (#%d)' => '', - // 'New password for the user "%s"' => '', - // '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' => '', ); diff --git a/sources/app/Locale/ru_RU/translations.php b/sources/app/Locale/ru_RU/translations.php index ae75252..a1cc7cf 100644 --- a/sources/app/Locale/ru_RU/translations.php +++ b/sources/app/Locale/ru_RU/translations.php @@ -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' => '', ); diff --git a/sources/app/Locale/sv_SE/translations.php b/sources/app/Locale/sv_SE/translations.php index d3a1c5e..ece0b2a 100644 --- a/sources/app/Locale/sv_SE/translations.php +++ b/sources/app/Locale/sv_SE/translations.php @@ -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' => '', ); diff --git a/sources/app/Locale/th_TH/translations.php b/sources/app/Locale/th_TH/translations.php index 65ea09b..42a1bcb 100644 --- a/sources/app/Locale/th_TH/translations.php +++ b/sources/app/Locale/th_TH/translations.php @@ -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' => '', ); diff --git a/sources/app/Locale/zh_CN/translations.php b/sources/app/Locale/zh_CN/translations.php index 11f45f7..d146459 100644 --- a/sources/app/Locale/zh_CN/translations.php +++ b/sources/app/Locale/zh_CN/translations.php @@ -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' => '书写', ); diff --git a/sources/app/Model/Acl.php b/sources/app/Model/Acl.php index 9a6866d..4a07d11 100644 --- a/sources/app/Model/Acl.php +++ b/sources/app/Model/Acl.php @@ -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'), ); /** diff --git a/sources/app/Model/Action.php b/sources/app/Model/Action.php index c3acdc5..f8dbd88 100644 --- a/sources/app/Model/Action.php +++ b/sources/app/Model/Action.php @@ -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); } /** diff --git a/sources/app/Model/Authentication.php b/sources/app/Model/Authentication.php index b9ebcfe..a0e9684 100644 --- a/sources/app/Model/Authentication.php +++ b/sources/app/Model/Authentication.php @@ -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]; } /** diff --git a/sources/app/Model/Base.php b/sources/app/Model/Base.php index 72d91c3..56a4d8e 100644 --- a/sources/app/Model/Base.php +++ b/sources/app/Model/Base.php @@ -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(); + }); } /** diff --git a/sources/app/Model/Board.php b/sources/app/Model/Board.php index 4c78b0f..9ba2e06 100644 --- a/sources/app/Model/Board.php +++ b/sources/app/Model/Board.php @@ -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); diff --git a/sources/app/Model/Category.php b/sources/app/Model/Category.php index fb54594..54a0f55 100644 --- a/sources/app/Model/Category.php +++ b/sources/app/Model/Category.php @@ -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; } } diff --git a/sources/app/Model/Color.php b/sources/app/Model/Color.php index f414e83..8668cf0 100644 --- a/sources/app/Model/Color.php +++ b/sources/app/Model/Color.php @@ -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 + } } diff --git a/sources/app/Model/Comment.php b/sources/app/Model/Comment.php index cd361b1..3b7dfbc 100644 --- a/sources/app/Model/Comment.php +++ b/sources/app/Model/Comment.php @@ -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; } /** diff --git a/sources/app/Model/GithubWebhook.php b/sources/app/Model/GithubWebhook.php index 58c2fa5..9c8bd36 100644 --- a/sources/app/Model/GithubWebhook.php +++ b/sources/app/Model/GithubWebhook.php @@ -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); diff --git a/sources/app/Model/Notification.php b/sources/app/Model/Notification.php index d2fcf52..8d1fca0 100644 --- a/sources/app/Model/Notification.php +++ b/sources/app/Model/Notification.php @@ -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'))); } /** diff --git a/sources/app/Model/Project.php b/sources/app/Model/Project.php index 32b7fcb..c657e82 100644 --- a/sources/app/Model/Project.php +++ b/sources/app/Model/Project.php @@ -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); diff --git a/sources/app/Model/ProjectActivity.php b/sources/app/Model/ProjectActivity.php index 6d6ef45..000dfa0 100644 --- a/sources/app/Model/ProjectActivity.php +++ b/sources/app/Model/ProjectActivity.php @@ -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); } /** diff --git a/sources/app/Model/ProjectAnalytic.php b/sources/app/Model/ProjectAnalytic.php new file mode 100644 index 0000000..46f2242 --- /dev/null +++ b/sources/app/Model/ProjectAnalytic.php @@ -0,0 +1,88 @@ +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); + } +} diff --git a/sources/app/Model/ProjectDailySummary.php b/sources/app/Model/ProjectDailySummary.php new file mode 100644 index 0000000..0ed3c02 --- /dev/null +++ b/sources/app/Model/ProjectDailySummary.php @@ -0,0 +1,181 @@ +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); + } + } +} diff --git a/sources/app/Model/ProjectPaginator.php b/sources/app/Model/ProjectPaginator.php new file mode 100644 index 0000000..9f1c39f --- /dev/null +++ b/sources/app/Model/ProjectPaginator.php @@ -0,0 +1,49 @@ +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; + } +} diff --git a/sources/app/Model/ProjectPermission.php b/sources/app/Model/ProjectPermission.php index fb9847b..8984ef3 100644 --- a/sources/app/Model/ProjectPermission.php +++ b/sources/app/Model/ProjectPermission.php @@ -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)) { diff --git a/sources/app/Model/SubTask.php b/sources/app/Model/SubTask.php index 886ad1f..f301ad6 100644 --- a/sources/app/Model/SubTask.php +++ b/sources/app/Model/SubTask.php @@ -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')), diff --git a/sources/app/Model/SubtaskPaginator.php b/sources/app/Model/SubtaskPaginator.php new file mode 100644 index 0000000..8ccbd69 --- /dev/null +++ b/sources/app/Model/SubtaskPaginator.php @@ -0,0 +1,68 @@ +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(); + } +} diff --git a/sources/app/Model/Task.php b/sources/app/Model/Task.php index a009064..a745f30 100644 --- a/sources/app/Model/Task.php +++ b/sources/app/Model/Task.php @@ -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 * diff --git a/sources/app/Model/TaskCreation.php b/sources/app/Model/TaskCreation.php new file mode 100644 index 0000000..320bcb9 --- /dev/null +++ b/sources/app/Model/TaskCreation.php @@ -0,0 +1,70 @@ +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); + } +} diff --git a/sources/app/Model/TaskDuplication.php b/sources/app/Model/TaskDuplication.php new file mode 100644 index 0000000..ab7a57f --- /dev/null +++ b/sources/app/Model/TaskDuplication.php @@ -0,0 +1,145 @@ +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; + } +} diff --git a/sources/app/Model/TaskFinder.php b/sources/app/Model/TaskFinder.php index 5679515..0e58102 100644 --- a/sources/app/Model/TaskFinder.php +++ b/sources/app/Model/TaskFinder.php @@ -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 * diff --git a/sources/app/Model/TaskModification.php b/sources/app/Model/TaskModification.php new file mode 100644 index 0000000..b165ea2 --- /dev/null +++ b/sources/app/Model/TaskModification.php @@ -0,0 +1,73 @@ +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(); + } +} diff --git a/sources/app/Model/TaskPaginator.php b/sources/app/Model/TaskPaginator.php new file mode 100644 index 0000000..4ae3566 --- /dev/null +++ b/sources/app/Model/TaskPaginator.php @@ -0,0 +1,139 @@ +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(); + } +} diff --git a/sources/app/Model/TaskPosition.php b/sources/app/Model/TaskPosition.php new file mode 100644 index 0000000..c23bc3b --- /dev/null +++ b/sources/app/Model/TaskPosition.php @@ -0,0 +1,136 @@ += 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); + } + } +} diff --git a/sources/app/Model/TaskStatus.php b/sources/app/Model/TaskStatus.php new file mode 100644 index 0000000..99faffd --- /dev/null +++ b/sources/app/Model/TaskStatus.php @@ -0,0 +1,112 @@ +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; + } +} diff --git a/sources/app/Model/TaskValidator.php b/sources/app/Model/TaskValidator.php index 816008c..ecaf090 100644 --- a/sources/app/Model/TaskValidator.php +++ b/sources/app/Model/TaskValidator.php @@ -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())); diff --git a/sources/app/Model/User.php b/sources/app/Model/User.php index 41bad0b..8fdfa81 100644 --- a/sources/app/Model/User.php +++ b/sources/app/Model/User.php @@ -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; + } + }); } /** diff --git a/sources/app/Model/Webhook.php b/sources/app/Model/Webhook.php index b84728c..14d5068 100644 --- a/sources/app/Model/Webhook.php +++ b/sources/app/Model/Webhook.php @@ -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); diff --git a/sources/app/Schema/Mysql.php b/sources/app/Schema/Mysql.php index 4f74f76..52dbea5 100644 --- a/sources/app/Schema/Mysql.php +++ b/sources/app/Schema/Mysql.php @@ -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) { diff --git a/sources/app/Schema/Postgres.php b/sources/app/Schema/Postgres.php index f301f3e..9493e60 100644 --- a/sources/app/Schema/Postgres.php +++ b/sources/app/Schema/Postgres.php @@ -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) { diff --git a/sources/app/Schema/Sqlite.php b/sources/app/Schema/Sqlite.php index 8571d92..82c2f41 100644 --- a/sources/app/Schema/Sqlite.php +++ b/sources/app/Schema/Sqlite.php @@ -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, diff --git a/sources/app/ServiceProvider/Database.php b/sources/app/ServiceProvider/Database.php new file mode 100644 index 0000000..75e1f73 --- /dev/null +++ b/sources/app/ServiceProvider/Database.php @@ -0,0 +1,100 @@ +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:= t('Not enough data to show the graph.') ?>
+ += t('Not enough data to show the graph.') ?>
+ += t('Column') ?> | += t('Number of tasks') ?> | += t('Percentage') ?> | +
---|---|---|
+ = Helper\escape($metric['column_title']) ?> + | ++ = $metric['nb_tasks'] ?> + | ++ = n($metric['percentage']) ?>% + | +
= t('Not enough data to show the graph.') ?>
+ += t('User') ?> | += t('Number of tasks') ?> | += t('Percentage') ?> | +
---|---|---|
+ = Helper\escape($metric['user']) ?> + | ++ = $metric['nb_tasks'] ?> + | ++ = n($metric['percentage']) ?>% + | +
= t('Access Forbidden') ?>
diff --git a/sources/app/Template/app_notfound.php b/sources/app/Template/app/notfound.php similarity index 59% rename from sources/app/Template/app_notfound.php rename to sources/app/Template/app/notfound.php index 734d16a..686f1fa 100644 --- a/sources/app/Template/app_notfound.php +++ b/sources/app/Template/app/notfound.php @@ -1,8 +1,4 @@= t('Sorry, I didn\'t found this information in my database!') ?>
diff --git a/sources/app/Template/app/projects.php b/sources/app/Template/app/projects.php new file mode 100644 index 0000000..2c13a05 --- /dev/null +++ b/sources/app/Template/app/projects.php @@ -0,0 +1,33 @@ += t('Your are not member of any project.') ?>
+ += Helper\order('Id', 'id', $pagination) ?> | += Helper\order(t('Project'), 'name', $pagination) ?> | += t('Columns') ?> | +
---|---|---|
+ = Helper\a('#'.$project['id'], 'board', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link') ?> + | ++ + = Helper\a('', 'project', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Settings')) ?> + + = Helper\a(Helper\escape($project['name']), 'board', 'show', array('project_id' => $project['id'])) ?> + | ++ + = $column['nb_tasks'] ?> + = Helper\escape($column['title']) ?> + + | +
= t('There is nothing assigned to you.') ?>
+ += Helper\order('Id', 'tasks.id', $pagination) ?> | += Helper\order(t('Project'), 'project_name', $pagination) ?> | += Helper\order(t('Status'), 'status', $pagination) ?> | += Helper\order(t('Subtask'), 'title', $pagination) ?> | +
---|---|---|---|
+ = Helper\a('#'.$subtask['task_id'], 'task', 'show', array('task_id' => $subtask['task_id'])) ?> + | ++ = Helper\a(Helper\escape($subtask['project_name']), 'board', 'show', array('project_id' => $subtask['project_id'])) ?> + | ++ = Helper\escape($subtask['status_name']) ?> + | ++ = Helper\a(Helper\escape($subtask['title']), 'task', 'show', array('task_id' => $subtask['task_id'])) ?> + | +
= t('There is nothing assigned to you.') ?>
+ += Helper\order('Id', 'tasks.id', $pagination) ?> | += Helper\order(t('Project'), 'project_name', $pagination) ?> | += Helper\order(t('Task'), 'title', $pagination) ?> | += Helper\order(t('Due date'), 'date_due', $pagination) ?> | +
---|---|---|---|
+ = Helper\a('#'.$task['id'], 'task', 'show', array('task_id' => $task['id'])) ?> + | ++ = Helper\a(Helper\escape($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?> + | ++ = Helper\a(Helper\escape($task['title']), 'task', 'show', array('task_id' => $task['id'])) ?> + | ++ = dt('%B %e, %Y', $task['date_due']) ?> + | +
= t('There is nothing assigned to you.') ?>
- -- | = t('Project') ?> | -= t('Title') ?> | -= t('Due date') ?> | -= t('Date created') ?> | -
---|---|---|---|---|
- = Helper\a('#'.$task['id'], 'task', 'show', array('task_id' => $task['id'])) ?> - | -- = Helper\a(Helper\escape($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?> - | -- = Helper\a(Helper\escape($task['title']), 'task', 'show', array('task_id' => $task['id'])) ?> - | -- = dt('%B %e, %Y', $task['date_due']) ?> - | -- = dt('%B %e, %Y', $task['date_creation']) ?> - | -
+ = Helper\escape($comment['name'] ?: $comment['username']) ?> @ = dt('%b %e, %Y, %k:%M %p', $comment['date']) ?> +
+ +