diff options
author | Frédéric Guillot <fred@kanboard.net> | 2014-05-21 22:33:57 -0400 |
---|---|---|
committer | Frédéric Guillot <fred@kanboard.net> | 2014-05-21 22:33:57 -0400 |
commit | a750b8ab2a0cb715da6fd9025a7ec8375db68a4d (patch) | |
tree | 5d5cdac1830336baf93b057e93cd2c1c56f405de | |
parent | 57e40671af56ae49eda467d9d5949bf9707020ee (diff) |
Add categories for projects and tasks
39 files changed, 815 insertions, 44 deletions
diff --git a/actions/task_assign_color_category.php b/actions/task_assign_color_category.php new file mode 100644 index 00000000..6fba5223 --- /dev/null +++ b/actions/task_assign_color_category.php @@ -0,0 +1,85 @@ +<?php + +namespace Action; + +require_once __DIR__.'/base.php'; + +/** + * Assign a color to a specific category + * + * @package action + * @author Frederic Guillot + */ +class TaskAssignColorCategory extends Base +{ + /** + * Task model + * + * @accesss private + * @var \Model\Task + */ + private $task; + + /** + * Constructor + * + * @access public + * @param integer $project_id Project id + * @param \Model\Task $task Task model instance + */ + public function __construct($project_id, \Model\Task $task) + { + parent::__construct($project_id); + $this->task = $task; + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'color_id' => t('Color'), + 'category_id' => t('Category'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'category_id', + ); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + if ($data['category_id'] == $this->getParam('category_id')) { + + $this->task->update(array( + 'id' => $data['task_id'], + 'color_id' => $this->getParam('color_id'), + )); + + return true; + } + + return false; + } +} diff --git a/actions/task_assign_color_user.php b/actions/task_assign_color_user.php index d3ce1fbe..d5784270 100644 --- a/actions/task_assign_color_user.php +++ b/actions/task_assign_color_user.php @@ -42,7 +42,6 @@ class TaskAssignColorUser extends Base public function getActionRequiredParameters() { return array( - 'column_id' => t('Column'), 'color_id' => t('Color'), 'user_id' => t('Assignee'), ); @@ -59,7 +58,6 @@ class TaskAssignColorUser extends Base return array( 'task_id', 'owner_id', - 'column_id', ); } @@ -72,7 +70,7 @@ class TaskAssignColorUser extends Base */ public function doAction(array $data) { - if ($data['column_id'] == $this->getParam('column_id') && $data['owner_id'] == $this->getParam('user_id')) { + if ($data['owner_id'] == $this->getParam('user_id')) { $this->task->update(array( 'id' => $data['task_id'], diff --git a/assets/css/app.css b/assets/css/app.css index 67e4e6df..45ec7444 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -484,23 +484,26 @@ nav .active a { } .project-menu li { + padding-left: 5px; display: inline; - padding-left: 10px; - padding-right: 10px; border-right: 1px dotted #ccc; } .project-menu li:last-child { border: none; - padding-right: 0; } .project-menu ul { padding-bottom: 5px; } +.project-menu a { + padding-right: 5px; +} + .project-menu { text-align: right; + font-size: 0.8em; } .public-board { @@ -540,7 +543,26 @@ a.filter-on { color: #444; } +.task-category-container { + text-align: right; + padding-bottom: 2px; +} + +.task-category { + font-weight: bold; + font-size: 0.8em; + color: #000; + border: 1px solid #555; + border-radius: 4px; + padding: 2px; + padding-right: 5px; + padding-left: 5px; +} + .task-date { + position: absolute; + bottom: 0; + left: 5px; font-weight: bold; color: #D90000; } @@ -552,7 +574,7 @@ a.filter-on { } .task-footer { - margin-top: 10px; + height: 18px; } .task { diff --git a/assets/js/board.js b/assets/js/board.js index 84aca8b3..af54d0c8 100644 --- a/assets/js/board.js +++ b/assets/js/board.js @@ -76,7 +76,7 @@ $("#board").remove(); $("#main").append(data); board_load_events(); - filter_apply(filter_get_user_id(), filter_has_due_date()); + filter_apply(); } }); } @@ -96,32 +96,25 @@ $("#main").append(data); board_unload_events(); board_load_events(); - filter_apply(filter_get_user_id(), filter_has_due_date()); + filter_apply(); } } }); } } - // Get the selected user id - function filter_get_user_id() - { - return $("#form-user_id").val(); - } - - // Return true if the filter is activated - function filter_has_due_date() - { - return $("#filter-due-date").hasClass("filter-on"); - } - // Apply user or date filter (change tasks opacity) - function filter_apply(selectedUserId, filterDueDate) + function filter_apply() { + var selectedUserId = $("#form-user_id").val(); + var selectedCategoryId = $("#form-category_id").val(); + var filterDueDate = $("#filter-due-date").hasClass("filter-on"); + $("[data-task-id]").each(function(index, item) { var ownerId = item.getAttribute("data-owner-id"); var dueDate = item.getAttribute("data-due-date"); + var categoryId = item.getAttribute("data-category-id"); if (ownerId != selectedUserId && selectedUserId != -1) { item.style.opacity = "0.2"; @@ -133,19 +126,23 @@ if (filterDueDate && (dueDate == "" || dueDate == "0")) { item.style.opacity = "0.2"; } + + if (categoryId != selectedCategoryId && selectedCategoryId != -1) { + item.style.opacity = "0.2"; + } }); } // Load filter events function filter_load_events() { - $("#form-user_id").change(function() { - filter_apply(filter_get_user_id(), filter_has_due_date()); - }); + $("#form-user_id").change(filter_apply); + + $("#form-category_id").change(filter_apply); $("#filter-due-date").click(function(e) { $(this).toggleClass("filter-on"); - filter_apply(filter_get_user_id(), filter_has_due_date()); + filter_apply(); e.preventDefault(); }); } @@ -6,7 +6,7 @@ require __DIR__.'/core/translator.php'; $registry = new Core\Registry; -$registry->db_version = 15; +$registry->db_version = 16; $registry->db = function() use ($registry) { require __DIR__.'/vendor/PicoDb/Database.php'; @@ -110,6 +110,11 @@ $registry->google = function() use ($registry) { return new \Model\Google($registry->shared('db'), $registry->shared('event')); }; +$registry->category = function() use ($registry) { + require_once __DIR__.'/models/category.php'; + return new \Model\Category($registry->shared('db'), $registry->shared('event')); +}; + if (file_exists('config.php')) require 'config.php'; // Board refresh frequency in seconds for the public board view diff --git a/controllers/action.php b/controllers/action.php index b4006940..5c5d5726 100644 --- a/controllers/action.php +++ b/controllers/action.php @@ -38,6 +38,7 @@ class Action extends Base 'users_list' => $this->project->getUsersList($project['id'], false), 'projects_list' => $this->project->getList(false), 'colors_list' => $this->task->getColors(), + 'categories_list' => $this->category->getList($project['id'], false), 'menu' => 'projects', 'title' => t('Automatic actions') ))); @@ -68,6 +69,7 @@ class Action extends Base 'users_list' => $this->project->getUsersList($project['id'], false), 'projects_list' => $this->project->getList(false), 'colors_list' => $this->task->getColors(), + 'categories_list' => $this->category->getList($project['id'], false), 'project' => $project, 'menu' => 'projects', 'title' => t('Automatic actions') diff --git a/controllers/base.php b/controllers/base.php index 07c5db63..49376dce 100644 --- a/controllers/base.php +++ b/controllers/base.php @@ -131,6 +131,14 @@ abstract class Base protected $google; /** + * Category model + * + * @accesss protected + * @var \Model\Category + */ + protected $category; + + /** * Event instance * * @accesss protected @@ -157,6 +165,7 @@ abstract class Base $this->rememberMe = $registry->rememberMe; $this->lastLogin = $registry->lastLogin; $this->google = $registry->google; + $this->category = $registry->category; $this->event = $registry->shared('event'); } diff --git a/controllers/board.php b/controllers/board.php index 02669e3a..f7128210 100644 --- a/controllers/board.php +++ b/controllers/board.php @@ -128,6 +128,7 @@ class Board extends Base $this->response->html($this->template->layout('board_public', array( 'project' => $project, 'columns' => $this->board->get($project['id']), + 'categories' => $this->category->getList($project['id'], false), 'title' => $project['name'], 'no_layout' => true, 'auto_refresh' => true, @@ -195,6 +196,7 @@ class Board extends Base 'current_project_id' => $project_id, 'current_project_name' => $projects[$project_id], 'board' => $this->board->get($project_id), + 'categories' => $this->category->getList($project_id, true, true), 'menu' => 'boards', 'title' => $projects[$project_id] ))); @@ -369,7 +371,11 @@ class Board extends Base } $this->response->html( - $this->template->load('board_show', array('current_project_id' => $project_id, 'board' => $this->board->get($project_id))), + $this->template->load('board_show', array( + 'current_project_id' => $project_id, + 'board' => $this->board->get($project_id), + 'categories' => $this->category->getList($project_id, false), + )), 201 ); } @@ -390,7 +396,11 @@ class Board extends Base if ($this->project->isModifiedSince($project_id, $timestamp)) { $this->response->html( - $this->template->load('board_show', array('current_project_id' => $project_id, 'board' => $this->board->get($project_id))) + $this->template->load('board_show', array( + 'current_project_id' => $project_id, + 'board' => $this->board->get($project_id), + 'categories' => $this->category->getList($project_id, false), + )) ); } else { diff --git a/controllers/category.php b/controllers/category.php new file mode 100644 index 00000000..23de2735 --- /dev/null +++ b/controllers/category.php @@ -0,0 +1,191 @@ +<?php + +namespace Controller; + +require_once __DIR__.'/base.php'; + +/** + * Categories management + * + * @package controller + * @author Frederic Guillot + */ +class Category extends Base +{ + /** + * Get the current project (common method between actions) + * + * @access private + * @return array + */ + private function getProject() + { + $project_id = $this->request->getIntegerParam('project_id'); + $project = $this->project->getById($project_id); + + if (! $project) { + $this->session->flashError(t('Project not found.')); + $this->response->redirect('?controller=project'); + } + + return $project; + } + + /** + * Get the category (common method between actions) + * + * @access private + * @return array + */ + private function getCategory($project_id) + { + $category = $this->category->getById($this->request->getIntegerParam('category_id')); + + if (! $category) { + $this->session->flashError(t('Category not found.')); + $this->response->redirect('?controller=category&action=index&project_id='.$project_id); + } + + return $category; + } + + /** + * List of categories for a given project + * + * @access public + */ + public function index() + { + $project = $this->getProject(); + + $this->response->html($this->template->layout('category_index', array( + 'categories' => $this->category->getList($project['id'], false), + 'values' => array('project_id' => $project['id']), + 'errors' => array(), + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Categories') + ))); + } + + /** + * Validate and save a new project + * + * @access public + */ + public function save() + { + $project = $this->getProject(); + + $values = $this->request->getValues(); + list($valid, $errors) = $this->category->validateCreation($values); + + if ($valid) { + + if ($this->category->create($values)) { + $this->session->flash(t('Your category have been created successfully.')); + $this->response->redirect('?controller=category&action=index&project_id='.$project['id']); + } + else { + $this->session->flashError(t('Unable to create your category.')); + } + } + + $this->response->html($this->template->layout('category_index', array( + 'categories' => $this->category->getList($project['id'], false), + 'values' => $values, + 'errors' => $errors, + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Categories') + ))); + } + + /** + * Edit a category (display the form) + * + * @access public + */ + public function edit() + { + $project = $this->getProject(); + $category = $this->getCategory($project['id']); + + $this->response->html($this->template->layout('category_edit', array( + 'values' => $category, + 'errors' => array(), + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Categories') + ))); + } + + /** + * Edit a category (validate the form and update the database) + * + * @access public + */ + public function update() + { + $project = $this->getProject(); + + $values = $this->request->getValues(); + list($valid, $errors) = $this->category->validateModification($values); + + if ($valid) { + + if ($this->category->update($values)) { + $this->session->flash(t('Your category have been updated successfully.')); + $this->response->redirect('?controller=category&action=index&project_id='.$project['id']); + } + else { + $this->session->flashError(t('Unable to update your category.')); + } + } + + $this->response->html($this->template->layout('category_edit', array( + 'values' => $values, + 'errors' => $errors, + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Categories') + ))); + } + + /** + * Confirmation dialog before removing a category + * + * @access public + */ + public function confirm() + { + $project = $this->getProject(); + $category = $this->getCategory($project['id']); + + $this->response->html($this->template->layout('category_remove', array( + 'project' => $project, + 'category' => $category, + 'menu' => 'projects', + 'title' => t('Remove a category') + ))); + } + + /** + * Remove a category + * + * @access public + */ + public function remove() + { + $project = $this->getProject(); + $category = $this->getCategory($project['id']); + + if ($this->category->remove($category['id'])) { + $this->session->flash(t('Category removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this category.')); + } + + $this->response->redirect('?controller=category&action=index&project_id='.$project['id']); + } +} diff --git a/controllers/project.php b/controllers/project.php index a9ce8634..1b9fd110 100644 --- a/controllers/project.php +++ b/controllers/project.php @@ -52,7 +52,7 @@ class Project extends Base array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id), 'or' => array( array('column' => 'title', 'operator' => 'like', 'value' => '%'.$search.'%'), - array('column' => 'description', 'operator' => 'like', 'value' => '%'.$search.'%'), + //array('column' => 'description', 'operator' => 'like', 'value' => '%'.$search.'%'), ) ); @@ -72,6 +72,7 @@ class Project extends Base 'menu' => 'projects', 'project' => $project, 'columns' => $this->board->getColumnsList($project_id), + 'categories' => $this->category->getList($project['id'], false), 'title' => $project['name'].($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '') ))); } @@ -105,6 +106,7 @@ class Project extends Base 'menu' => 'projects', 'project' => $project, 'columns' => $this->board->getColumnsList($project_id), + 'categories' => $this->category->getList($project['id'], false), 'tasks' => $tasks, 'nb_tasks' => $nb_tasks, 'title' => $project['name'].' ('.$nb_tasks.')' diff --git a/controllers/task.php b/controllers/task.php index f5738a55..bdedb9dd 100644 --- a/controllers/task.php +++ b/controllers/task.php @@ -34,6 +34,7 @@ class Task extends Base 'project_id' => $this->request->getIntegerParam('project_id', $defaultProject['id']), 'owner_id' => $this->request->getIntegerParam('owner_id'), 'column_id' => $this->request->getIntegerParam('column_id'), + 'category_id' => $this->request->getIntegerParam('category_id'), ); if ($values['column_id'] == 0) { @@ -121,6 +122,7 @@ class Task extends Base 'columns_list' => $this->board->getColumnsList($project_id), 'users_list' => $this->project->getUsersList($project_id), 'colors_list' => $this->task->getColors(), + 'categories_list' => $this->category->getList($project_id), 'menu' => 'tasks', 'title' => t('New task') ))); @@ -164,6 +166,7 @@ class Task extends Base 'columns_list' => $this->board->getColumnsList($values['project_id']), 'users_list' => $this->project->getUsersList($values['project_id']), 'colors_list' => $this->task->getColors(), + 'categories_list' => $this->category->getList($values['project_id']), 'menu' => 'tasks', 'title' => t('New task') ))); @@ -196,6 +199,7 @@ class Task extends Base 'columns_list' => $this->board->getColumnsList($task['project_id']), 'users_list' => $this->project->getUsersList($task['project_id']), 'colors_list' => $this->task->getColors(), + 'categories_list' => $this->category->getList($task['project_id']), 'menu' => 'tasks', 'title' => t('Edit a task') ))); @@ -230,6 +234,7 @@ class Task extends Base 'columns_list' => $this->board->getColumnsList($values['project_id']), 'users_list' => $this->project->getUsersList($values['project_id']), 'colors_list' => $this->task->getColors(), + 'categories_list' => $this->category->getList($values['project_id']), 'menu' => 'tasks', 'title' => t('Edit a task') ))); @@ -383,6 +388,7 @@ class Task extends Base 'columns_list' => $this->board->getColumnsList($task['project_id']), 'users_list' => $this->project->getUsersList($task['project_id']), 'colors_list' => $this->task->getColors(), + 'categories_list' => $this->category->getList($task['project_id']), 'duplicate' => true, 'menu' => 'tasks', 'title' => t('New task') diff --git a/core/helper.php b/core/helper.php index 7e65543a..8351328a 100644 --- a/core/helper.php +++ b/core/helper.php @@ -112,13 +112,13 @@ function contains($haystack, $needle) return strpos($haystack, $needle) !== false; } -function in_list($id, array $listing) +function in_list($id, array $listing, $default_value = '?') { if (isset($listing[$id])) { return escape($listing[$id]); } - return '?'; + return $default_value; } function error_class(array $errors, $name) diff --git a/docs/automatic-actions.markdown b/docs/automatic-actions.markdown index 6cbc0937..9526e9af 100644 --- a/docs/automatic-actions.markdown +++ b/docs/automatic-actions.markdown @@ -24,6 +24,7 @@ List of available events - Task creation - Open a closed task - Closing a task +- Task creation or modification List of available actions ------------------------- @@ -33,6 +34,7 @@ List of available actions - Assign the task to the person who does the action - Duplicate the task to another project - Assign a color to a specific user +- Assign a color to a specific category Examples -------- @@ -65,8 +67,14 @@ Let's say we have two projects "Customer orders" and "Production", once the orde - Choose the action: **Duplicate the task to another project** - Define the action parameters: **Column = Validated** and **Project = Production** -### When I create a new task in the column "To do", assign a specific color to the user Bob +### I want to assign a specific color to the user Bob - Choose the event: **Task creation** - Choose the action: **Assign a color to a specific user** -- Define the action parameters: **Column = To do**, **Color = Green** and **Assignee = Bob** +- Define the action parameters: **Color = Green** and **Assignee = Bob** + +### I want to assign a specific color to the category "Feature Request" + +- Choose the event: **Task creation or modification** +- Choose the action: **Assign a color to a specific category** +- Define the action parameters: **Color = Blue** and **Category = Feature Request** diff --git a/locales/es_ES/translations.php b/locales/es_ES/translations.php index e1d54c03..ac97d46c 100644 --- a/locales/es_ES/translations.php +++ b/locales/es_ES/translations.php @@ -309,4 +309,25 @@ return array( // 'Unable to remove this task.' => '', // 'Remove a task' => '', // 'Do you really want to remove this task: "%s"?' => '', + // 'Assign a color to a specific category' => '', + // 'Task creation or modification' => '', + // 'Category' => '', + // 'Category:' => '', + // 'Categories' => '', + // 'Category not found.' => '', + // 'Your category have been created successfully.' => '', + // 'Unable to create your category.' => '', + // 'Your category have been updated successfully.' => '', + // 'Unable to update your category.' => '', + // 'Remove a category' => '', + // 'Category removed successfully.' => '', + // 'Unable to remove this category.' => '', + // 'Category modification for the project "%s"' => '', + // 'Category Name' => '', + // 'Categories for the project "%s"' => '', + // 'Add a new category' => '', + // 'Do you really want to remove this category: "%s"?' => '', + // 'Filter by category' => '', + // 'All categories' => '', + // 'No category' => '', ); diff --git a/locales/fr_FR/translations.php b/locales/fr_FR/translations.php index ac216c9b..46faf502 100644 --- a/locales/fr_FR/translations.php +++ b/locales/fr_FR/translations.php @@ -309,4 +309,25 @@ return array( 'Unable to remove this task.' => 'Impossible de supprimer cette tâche.', 'Remove a task' => 'Supprimer une tâche', 'Do you really want to remove this task: "%s"?' => 'Voulez-vous vraiment supprimer cette tâche « %s » ?', + 'Assign a color to a specific category' => 'Assigner une couleur à une catégorie spécifique', + 'Task creation or modification' => 'Création ou modification d\'une tâche', + 'Category' => 'Catégorie', + 'Category:' => 'Catégorie :', + 'Categories' => 'Catégories', + 'Category not found.' => 'Catégorie introuvable', + 'Your category have been created successfully.' => 'Votre catégorie a été créé avec succès.', + 'Unable to create your category.' => 'Impossible de créer votre catégorie.', + 'Your category have been updated successfully.' => 'Votre catégorie a été mise à jour avec succès.', + 'Unable to update your category.' => 'Impossible de mettre à jour votre catégorie.', + 'Remove a category' => 'Supprimer une catégorie', + 'Category removed successfully.' => 'Catégorie supprimée avec succès.', + 'Unable to remove this category.' => 'Impossible de supprimer cette catégorie.', + 'Category modification for the project "%s"' => 'Modification d\'une catégorie pour le projet « %s »', + 'Category Name' => 'Nom de la catégorie', + 'Categories for the project "%s"' => 'Catégories du projet « %s »', + 'Add a new category' => 'Ajouter une nouvelle catégorie', + 'Do you really want to remove this category: "%s"?' => 'Voulez-vous vraiment supprimer cette catégorie « %s » ?', + 'Filter by category' => 'Filtrer par catégorie', + 'All categories' => 'Toutes les catégories', + 'No category' => 'Aucune catégorie', ); diff --git a/locales/pl_PL/translations.php b/locales/pl_PL/translations.php index a8feb807..94726c20 100644 --- a/locales/pl_PL/translations.php +++ b/locales/pl_PL/translations.php @@ -314,4 +314,25 @@ return array( // 'Unable to remove this task.' => '', // 'Remove a task' => '', // 'Do you really want to remove this task: "%s"?' => '', + // 'Assign a color to a specific category' => '', + // 'Task creation or modification' => '', + // 'Category' => '', + // 'Category:' => '', + // 'Categories' => '', + // 'Category not found.' => '', + // 'Your category have been created successfully.' => '', + // 'Unable to create your category.' => '', + // 'Your category have been updated successfully.' => '', + // 'Unable to update your category.' => '', + // 'Remove a category' => '', + // 'Category removed successfully.' => '', + // 'Unable to remove this category.' => '', + // 'Category modification for the project "%s"' => '', + // 'Category Name' => '', + // 'Categories for the project "%s"' => '', + // 'Add a new category' => '', + // 'Do you really want to remove this category: "%s"?' => '', + // 'Filter by category' => '', + // 'All categories' => '', + // 'No category' => '', ); diff --git a/locales/pt_BR/translations.php b/locales/pt_BR/translations.php index ffb578c5..fc91b0b2 100644 --- a/locales/pt_BR/translations.php +++ b/locales/pt_BR/translations.php @@ -310,4 +310,25 @@ return array( // 'Unable to remove this task.' => '', // 'Remove a task' => '', // 'Do you really want to remove this task: "%s"?' => '', + // 'Assign a color to a specific category' => '', + // 'Task creation or modification' => '', + // 'Category' => '', + // 'Category:' => '', + // 'Categories' => '', + // 'Category not found.' => '', + // 'Your category have been created successfully.' => '', + // 'Unable to create your category.' => '', + // 'Your category have been updated successfully.' => '', + // 'Unable to update your category.' => '', + // 'Remove a category' => '', + // 'Category removed successfully.' => '', + // 'Unable to remove this category.' => '', + // 'Category modification for the project "%s"' => '', + // 'Category Name' => '', + // 'Categories for the project "%s"' => '', + // 'Add a new category' => '', + // 'Do you really want to remove this category: "%s"?' => '', + // 'Filter by category' => '', + // 'All categories' => '', + // 'No category' => '', ); diff --git a/models/action.php b/models/action.php index a0236eff..c8cbf3b1 100644 --- a/models/action.php +++ b/models/action.php @@ -44,6 +44,7 @@ class Action extends Base 'TaskAssignCurrentUser' => t('Assign the task to the person who does the action'), 'TaskDuplicateAnotherProject' => t('Duplicate the task to another project'), 'TaskAssignColorUser' => t('Assign a color to a specific user'), + 'TaskAssignColorCategory' => t('Assign a color to a specific category'), ); } @@ -62,6 +63,7 @@ class Action extends Base Task::EVENT_CREATE => t('Task creation'), Task::EVENT_OPEN => t('Open a closed task'), Task::EVENT_CLOSE => t('Closing a task'), + Task::EVENT_CREATE_UPDATE => t('Task creation or modification'), ); } @@ -239,6 +241,10 @@ class Action extends Base require_once __DIR__.'/../actions/task_assign_color_user.php'; $className = '\Action\TaskAssignColorUser'; return new $className($project_id, new Task($this->db, $this->event)); + case 'TaskAssignColorCategory': + require_once __DIR__.'/../actions/task_assign_color_category.php'; + $className = '\Action\TaskAssignColorCategory'; + return new $className($project_id, new Task($this->db, $this->event)); default: throw new \LogicException('Action not found: '.$name); } diff --git a/models/category.php b/models/category.php new file mode 100644 index 00000000..2a1891a5 --- /dev/null +++ b/models/category.php @@ -0,0 +1,152 @@ +<?php + +namespace Model; + +require_once __DIR__.'/base.php'; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Category model + * + * @package model + * @author Frederic Guillot + */ +class Category extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'project_has_categories'; + + /** + * Get a category by the id + * + * @access public + * @param integer $category_id Category id + * @return array + */ + public function getById($category_id) + { + return $this->db->table(self::TABLE)->eq('id', $category_id)->findOne(); + } + + /** + * Return the list of all categories + * + * @access public + * @param integer $project_id Project id + * @param bool $prepend_none If true, prepend to the list the value 'None' + * @param bool $prepend_all If true, prepend to the list the value 'All' + * @return array + */ + public function getList($project_id, $prepend_none = true, $prepend_all = false) + { + $listing = $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->asc('name') + ->listing('id', 'name'); + + $prepend = array(); + + if ($prepend_all) { + $prepend[-1] = t('All categories'); + } + + if ($prepend_none) { + $prepend[0] = t('No category'); + } + + return $prepend + $listing; + } + + /** + * Create a category + * + * @access public + * @param array $values Form values + * @return bool + */ + public function create(array $values) + { + return $this->db->table(self::TABLE)->save($values); + } + + /** + * Update a category + * + * @access public + * @param array $values Form values + * @return bool + */ + public function update(array $values) + { + return $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); + } + + /** + * Remove a category + * + * @access public + * @param integer $category_id Category id + * @return bool + */ + public function remove($category_id) + { + $this->db->startTransaction(); + $r1 = $this->db->table(Task::TABLE)->eq('category_id', $category_id)->update(array('category_id' => 0)); + $r2 = $this->db->table(self::TABLE)->eq('id', $category_id)->remove(); + $this->db->closeTransaction(); + + return $r1 && $r2; + } + + /** + * Validate category creation + * + * @access public + * @param array $array Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('project_id', t('The project id is required')), + new Validators\Integer('project_id', t('The project id must be an integer')), + new Validators\Required('name', t('The name is required')), + new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate category modification + * + * @access public + * @param array $array Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $v = new Validator($values, array( + new Validators\Required('id', t('The id is required')), + new Validators\Integer('id', t('The id must be an integer')), + new Validators\Required('project_id', t('The project id is required')), + new Validators\Integer('project_id', t('The project id must be an integer')), + new Validators\Required('name', t('The name is required')), + new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/models/project.php b/models/project.php index 680d0750..cbbbfda2 100644 --- a/models/project.php +++ b/models/project.php @@ -8,9 +8,9 @@ require_once __DIR__.'/board.php'; require_once __DIR__.'/task.php'; require_once __DIR__.'/../events/task_modification.php'; -use \SimpleValidator\Validator; -use \SimpleValidator\Validators; -use \Event\TaskModification; +use SimpleValidator\Validator; +use SimpleValidator\Validators; +use Event\TaskModification; /** * Project model diff --git a/models/task.php b/models/task.php index f6258e86..0c62a9f4 100644 --- a/models/task.php +++ b/models/task.php @@ -4,8 +4,8 @@ namespace Model; require_once __DIR__.'/base.php'; -use \SimpleValidator\Validator; -use \SimpleValidator\Validators; +use SimpleValidator\Validator; +use SimpleValidator\Validators; use DateTime; /** @@ -42,6 +42,7 @@ class Task extends Base const EVENT_CREATE = 'task.create'; const EVENT_CLOSE = 'task.close'; const EVENT_OPEN = 'task.open'; + const EVENT_CREATE_UPDATE = 'task.create_update'; /** * Get available colors @@ -90,10 +91,13 @@ class Task extends Base self::TABLE.'.position', self::TABLE.'.is_active', self::TABLE.'.score', + self::TABLE.'.category_id', + Category::TABLE.'.name AS category_name', Project::TABLE.'.name AS project_name', Board::TABLE.'.title AS column_title', User::TABLE.'.username' ) + ->join(Category::TABLE, 'id', 'category_id') ->join(Project::TABLE, 'id', 'project_id') ->join(Board::TABLE, 'id', 'column_id') ->join(User::TABLE, 'id', 'owner_id') @@ -150,6 +154,7 @@ class Task extends Base 'tasks.position', 'tasks.is_active', 'tasks.score', + 'tasks.category_id', 'users.username' ) ->join('users', 'id', 'owner_id'); @@ -236,6 +241,7 @@ class Task extends Base $this->db->closeTransaction(); // Trigger events + $this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $task); $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $task); return $task_id; @@ -265,6 +271,7 @@ class Task extends Base // Assign new values $task['date_creation'] = time(); $task['owner_id'] = 0; + $task['category_id'] = 0; $task['is_active'] = 1; $task['column_id'] = $boardModel->getFirstColumn($project_id); $task['project_id'] = $project_id; @@ -281,6 +288,7 @@ class Task extends Base $this->db->closeTransaction(); // Trigger events + $this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $task); $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $task); return $task_id; @@ -320,6 +328,7 @@ class Task extends Base $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; @@ -355,7 +364,8 @@ class Task extends Base $events = array(); - if ($this->event->getLastTriggeredEvent() !== self::EVENT_UPDATE) { + if (! in_array($this->event->getLastTriggeredEvent(), array(self::EVENT_CREATE_UPDATE))) { + $events[] = self::EVENT_CREATE_UPDATE; $events[] = self::EVENT_UPDATE; } diff --git a/schemas/mysql.php b/schemas/mysql.php index 46f56e7b..eb869465 100644 --- a/schemas/mysql.php +++ b/schemas/mysql.php @@ -2,6 +2,22 @@ namespace Schema; +function version_16($pdo) +{ + $pdo->exec(" + CREATE TABLE project_has_categories ( + id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(255), + project_id INT, + PRIMARY KEY (id), + UNIQUE KEY `idx_project_category` (project_id, name), + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8" + ); + + $pdo->exec("ALTER TABLE tasks ADD COLUMN category_id INT DEFAULT 0"); +} + function version_15($pdo) { $pdo->exec("ALTER TABLE projects ADD COLUMN last_modified INT DEFAULT 0"); diff --git a/schemas/sqlite.php b/schemas/sqlite.php index 94580235..b444faa5 100644 --- a/schemas/sqlite.php +++ b/schemas/sqlite.php @@ -2,6 +2,21 @@ namespace Schema; +function version_16($pdo) +{ + $pdo->exec(" + CREATE TABLE project_has_categories ( + id INTEGER PRIMARY KEY, + name TEXT COLLATE NOCASE, + project_id INT, + UNIQUE (project_id, name), + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + )" + ); + + $pdo->exec("ALTER TABLE tasks ADD COLUMN category_id INTEGER DEFAULT 0"); +} + function version_15($pdo) { $pdo->exec("ALTER TABLE projects ADD COLUMN last_modified INTEGER DEFAULT 0"); diff --git a/templates/action_index.php b/templates/action_index.php index eccc0ec4..b515ccaa 100644 --- a/templates/action_index.php +++ b/templates/action_index.php @@ -36,6 +36,8 @@ <?= Helper\in_list($param['value'], $projects_list) ?> <?php elseif (Helper\contains($param['name'], 'color_id')): ?> <?= Helper\in_list($param['value'], $colors_list) ?> + <?php elseif (Helper\contains($param['name'], 'category_id')): ?> + <?= Helper\in_list($param['value'], $categories_list) ?> <?php endif ?> </strong> </li> diff --git a/templates/action_params.php b/templates/action_params.php index ed9d8b8f..15a1d420 100644 --- a/templates/action_params.php +++ b/templates/action_params.php @@ -28,6 +28,9 @@ <?php elseif (Helper\contains($param_name, 'color_id')): ?> <?= Helper\form_label($param_desc, $param_name) ?> <?= Helper\form_select('params['.$param_name.']', $colors_list, $values) ?><br/> + <?php elseif (Helper\contains($param_name, 'category_id')): ?> + <?= Helper\form_label($param_desc, $param_name) ?> + <?= Helper\form_select('params['.$param_name.']', $categories_list, $values) ?><br/> <?php endif ?> <?php endforeach ?> diff --git a/templates/board_index.php b/templates/board_index.php index 523e3392..989c2e06 100644 --- a/templates/board_index.php +++ b/templates/board_index.php @@ -21,6 +21,10 @@ <?= t('Filter by user') ?> <?= Helper\form_select('user_id', $users, $filters) ?> </li> + <li> + <?= t('Filter by category') ?> + <?= Helper\form_select('category_id', $categories, $filters) ?> + </li> <li><a href="#" id="filter-due-date"><?= t('Filter by due date') ?></a></li> <li><a href="?controller=project&action=search&project_id=<?= $current_project_id ?>"><?= t('Search') ?></a></li> <li><a href="?controller=project&action=tasks&project_id=<?= $current_project_id ?>"><?= t('Completed tasks') ?></a></li> @@ -30,7 +34,7 @@ <?php if (empty($board)): ?> <p class="alert alert-error"><?= t('There is no column in your project!') ?></p> <?php else: ?> - <?= Helper\template('board_show', array('current_project_id' => $current_project_id, 'board' => $board)) ?> + <?= Helper\template('board_show', array('current_project_id' => $current_project_id, 'board' => $board, 'categories' => $categories)) ?> <?php endif ?> </section> diff --git a/templates/board_public.php b/templates/board_public.php index 2c2381ef..0808079e 100644 --- a/templates/board_public.php +++ b/templates/board_public.php @@ -39,7 +39,17 @@ <?= Helper\escape($task['title']) ?> </div> + <?php if ($task['category_id']): ?> + <div class="task-category-container"> + <span class="task-category"> + <?= Helper\in_list($task['category_id'], $categories) ?> + </span> + </div> + <?php endif ?> + + <?php if (! empty($task['date_due']) || ! empty($task['nb_comments']) || ! empty($task['description'])): ?> <div class="task-footer"> + <?php if (! empty($task['date_due'])): ?> <div class="task-date"> <?= dt('%B %e, %G', $task['date_due']) ?> @@ -56,6 +66,7 @@ <?php endif ?> </div> </div> + <?php endif ?> </div> <?php endforeach ?> diff --git a/templates/board_show.php b/templates/board_show.php index ad459399..719e3bdd 100644 --- a/templates/board_show.php +++ b/templates/board_show.php @@ -29,6 +29,7 @@ <div class="task draggable-item task-<?= $task['color_id'] ?>" data-task-id="<?= $task['id'] ?>" data-owner-id="<?= $task['owner_id'] ?>" + data-category-id="<?= $task['category_id'] ?>" data-due-date="<?= $task['date_due'] ?>" title="<?= t('View this task') ?>"> @@ -50,7 +51,17 @@ <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>"><?= Helper\escape($task['title']) ?></a> </div> + <?php if ($task['category_id']): ?> + <div class="task-category-container"> + <span class="task-category"> + <?= Helper\in_list($task['category_id'], $categories) ?> + </span> + </div> + <?php endif ?> + + <?php if (! empty($task['date_due']) || ! empty($task['nb_comments']) || ! empty($task['description'])): ?> <div class="task-footer"> + <?php if (! empty($task['date_due'])): ?> <div class="task-date"> <?= dt('%B %e, %G', $task['date_due']) ?> @@ -67,6 +78,8 @@ <?php endif ?> </div> </div> + <?php endif ?> + </div> <?php endforeach ?> </td> diff --git a/templates/category_edit.php b/templates/category_edit.php new file mode 100644 index 00000000..99ba0c7c --- /dev/null +++ b/templates/category_edit.php @@ -0,0 +1,24 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Category modification for the project "%s"', $project['name']) ?></h2> + <ul> + <li><a href="?controller=project"><?= t('All projects') ?></a></li> + </ul> + </div> + <section> + + <form method="post" action="?controller=category&action=update&project_id=<?= $project['id'] ?>" autocomplete="off"> + + <?= Helper\form_hidden('id', $values) ?> + <?= Helper\form_hidden('project_id', $values) ?> + + <?= Helper\form_label(t('Category Name'), 'name') ?> + <?= Helper\form_text('name', $values, $errors, array('required')) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> + </form> + + </section> +</section>
\ No newline at end of file diff --git a/templates/category_index.php b/templates/category_index.php new file mode 100644 index 00000000..db986143 --- /dev/null +++ b/templates/category_index.php @@ -0,0 +1,48 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Categories for the project "%s"', $project['name']) ?></h2> + <ul> + <li><a href="?controller=project"><?= t('All projects') ?></a></li> + </ul> + </div> + <section> + + <?php if (! empty($categories)): ?> + <table> + <tr> + <th><?= t('Category Name') ?></th> + <th><?= t('Actions') ?></th> + </tr> + <?php foreach ($categories as $category_id => $category_name): ?> + <tr> + <td><?= Helper\escape($category_name) ?></td> + <td> + <ul> + <li> + <a href="?controller=category&action=edit&project_id=<?= $project['id'] ?>&category_id=<?= $category_id ?>"><?= t('Edit') ?></a> + </li> + <li> + <a href="?controller=category&action=confirm&project_id=<?= $project['id'] ?>&category_id=<?= $category_id ?>"><?= t('Remove') ?></a> + </li> + </ul> + </td> + </tr> + <?php endforeach ?> + </table> + <?php endif ?> + + <h3><?= t('Add a new category') ?></h3> + <form method="post" action="?controller=category&action=save&project_id=<?= $project['id'] ?>" autocomplete="off"> + + <?= Helper\form_hidden('project_id', $values) ?> + + <?= Helper\form_label(t('Category Name'), 'name') ?> + <?= Helper\form_text('name', $values, $errors, array('required')) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> + </form> + + </section> +</section>
\ No newline at end of file diff --git a/templates/category_remove.php b/templates/category_remove.php new file mode 100644 index 00000000..cc2eb678 --- /dev/null +++ b/templates/category_remove.php @@ -0,0 +1,16 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Remove a category') ?></h2> + </div> + + <div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this category: "%s"?', $category['name']) ?> + </p> + + <div class="form-actions"> + <a href="?controller=category&action=remove&project_id=<?= $project['id'] ?>&category_id=<?= $category['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <?= t('or') ?> <a href="?controller=category&project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a> + </div> + </div> +</section>
\ No newline at end of file diff --git a/templates/project_index.php b/templates/project_index.php index c7c7d226..df153fe7 100644 --- a/templates/project_index.php +++ b/templates/project_index.php @@ -60,6 +60,9 @@ <td> <ul> <li> + <a href="?controller=category&action=index&project_id=<?= $project['id'] ?>"><?= t('Categories') ?></a> + </li> + <li> <a href="?controller=project&action=edit&project_id=<?= $project['id'] ?>"><?= t('Edit project') ?></a> </li> <li> diff --git a/templates/project_search.php b/templates/project_search.php index ee1e5025..3594fd09 100644 --- a/templates/project_search.php +++ b/templates/project_search.php @@ -28,6 +28,7 @@ <tr> <th><?= t('Id') ?></th> <th><?= t('Column') ?></th> + <th><?= t('Category') ?></th> <th><?= t('Title') ?></th> <th><?= t('Assignee') ?></th> <th><?= t('Due date') ?></th> @@ -44,6 +45,9 @@ <?= Helper\in_list($task['column_id'], $columns) ?> </td> <td> + <?= Helper\in_list($task['category_id'], $categories, '') ?> + </td> + <td> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>"><?= Helper\escape($task['title']) ?></a> <div class="task-table-icons"> <?php if (! empty($task['nb_comments'])): ?> diff --git a/templates/project_tasks.php b/templates/project_tasks.php index 2c152497..9f4263b8 100644 --- a/templates/project_tasks.php +++ b/templates/project_tasks.php @@ -15,6 +15,7 @@ <tr> <th><?= t('Id') ?></th> <th><?= t('Column') ?></th> + <th><?= t('Category') ?></th> <th><?= t('Title') ?></th> <th><?= t('Assignee') ?></th> <th><?= t('Due date') ?></th> @@ -30,6 +31,9 @@ <?= Helper\in_list($task['column_id'], $columns) ?> </td> <td> + <?= Helper\in_list($task['category_id'], $categories, '') ?> + </td> + <td> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>"><?= Helper\escape($task['title']) ?></a> <div class="task-table-icons"> <?php if (! empty($task['nb_comments'])): ?> diff --git a/templates/task_edit.php b/templates/task_edit.php index 0c19db38..8c8bc107 100644 --- a/templates/task_edit.php +++ b/templates/task_edit.php @@ -24,6 +24,9 @@ <?= Helper\form_label(t('Assignee'), 'owner_id') ?> <?= Helper\form_select('owner_id', $users_list, $values, $errors) ?><br/> + <?= Helper\form_label(t('Category'), 'category_id') ?> + <?= Helper\form_select('category_id', $categories_list, $values, $errors) ?><br/> + <?= Helper\form_label(t('Column'), 'column_id') ?> <?= Helper\form_select('column_id', $columns_list, $values, $errors) ?><br/> diff --git a/templates/task_new.php b/templates/task_new.php index cf320e72..d233efd2 100644 --- a/templates/task_new.php +++ b/templates/task_new.php @@ -25,6 +25,9 @@ <?= Helper\form_label(t('Assignee'), 'owner_id') ?> <?= Helper\form_select('owner_id', $users_list, $values, $errors) ?><br/> + <?= Helper\form_label(t('Category'), 'category_id') ?> + <?= Helper\form_select('category_id', $categories_list, $values, $errors) ?><br/> + <?= Helper\form_label(t('Column'), 'column_id') ?> <?= Helper\form_select('column_id', $columns_list, $values, $errors) ?><br/> diff --git a/templates/task_show.php b/templates/task_show.php index f0cea410..a5b79359 100644 --- a/templates/task_show.php +++ b/templates/task_show.php @@ -31,6 +31,11 @@ <strong><?= Helper\escape($task['column_title']) ?></strong> (<?= Helper\escape($task['project_name']) ?>) </li> + <?php if ($task['category_name']): ?> + <li> + <?= t('Category:') ?> <strong><?= Helper\escape($task['category_name']) ?></strong> + </li> + <?php endif ?> <li> <?php if ($task['is_active'] == 1): ?> <?= t('Status is open') ?> diff --git a/tests/Base.php b/tests/Base.php index 4a0fe044..400643ac 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -36,7 +36,7 @@ abstract class Base extends PHPUnit_Framework_TestCase 'filename' => ':memory:' )); - if ($db->schema()->check(10)) { + if ($db->schema()->check(16)) { return $db; } else { diff --git a/tests/TaskTest.php b/tests/TaskTest.php index 594d0e13..2f645131 100644 --- a/tests/TaskTest.php +++ b/tests/TaskTest.php @@ -17,6 +17,7 @@ class TaskTest extends Base $this->assertEquals(2, $t->create(array('title' => 'test b', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 2, 'description' => 'toto et titi sont dans un bateau'))); $tasks = $t->find(array(array('column' => 'project_id', 'operator' => 'eq', 'value' => '1'))); + $this->assertNotFalse($tasks); $this->assertEquals(2, count($tasks)); $this->assertEquals(1, $tasks[0]['id']); $this->assertEquals(2, $tasks[1]['id']); @@ -101,7 +102,7 @@ class TaskTest extends Base // We create a task and a project $this->assertEquals(1, $p->create(array('name' => 'test1'))); - $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); + $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1, 'category_id' => 2))); $task = $t->getById(1); $this->assertNotEmpty($task); @@ -118,6 +119,7 @@ class TaskTest extends Base $this->assertEquals(1, $task['project_id']); $this->assertEquals(1, $task['owner_id']); $this->assertEquals(1, $task['position']); + $this->assertEquals(2, $task['category_id']); } public function testDuplicateToAnotherProject() @@ -130,11 +132,19 @@ class TaskTest extends Base $this->assertEquals(2, $p->create(array('name' => 'test2'))); // We create a task - $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1, 'category_id' => 1))); // We duplicate our task to the 2nd project $this->assertEquals(2, $t->duplicateToAnotherProject(1, 2)); $this->assertEquals(Task::EVENT_CREATE, $this->event->getLastTriggeredEvent()); + + // Check the values of the duplicated task + $task = $t->getById(2); + $this->assertNotEmpty($task); + $this->assertEquals(0, $task['owner_id']); + $this->assertEquals(0, $task['category_id']); + $this->assertEquals(2, $task['project_id']); + $this->assertEquals('test', $task['title']); } public function testEvents() |