diff options
Diffstat (limited to 'app/Model')
44 files changed, 984 insertions, 1544 deletions
diff --git a/app/Model/Action.php b/app/Model/Action.php index 4da2fb8f..f055d9d0 100644 --- a/app/Model/Action.php +++ b/app/Model/Action.php @@ -151,7 +151,7 @@ class Action extends Base * * @author Antonio Rabelo * @param integer $src_project_id Source project id - * @return integer $dst_project_id Destination project id + * @param integer $dst_project_id Destination project id * @return boolean */ public function duplicate($src_project_id, $dst_project_id) diff --git a/app/Model/ActionParameter.php b/app/Model/ActionParameter.php index 62b03142..53edcbc8 100644 --- a/app/Model/ActionParameter.php +++ b/app/Model/ActionParameter.php @@ -150,8 +150,8 @@ class ActionParameter extends Base case 'dest_column_id': case 'dst_column_id': case 'column_id': - $column = $this->board->getColumn($value); - return empty($column) ? false : $this->board->getColumnIdByTitle($project_id, $column['title']) ?: false; + $column = $this->column->getById($value); + return empty($column) ? false : $this->column->getColumnIdByTitle($project_id, $column['title']) ?: false; case 'user_id': case 'owner_id': return $this->projectPermission->isAssignable($project_id, $value) ? $value : false; diff --git a/app/Model/Base.php b/app/Model/Base.php index 6fe3d74a..714b4308 100644 --- a/app/Model/Base.php +++ b/app/Model/Base.php @@ -33,86 +33,6 @@ abstract class Base extends \Kanboard\Core\Base } /** - * Remove keys from an array - * - * @access public - * @param array $values Input array - * @param string[] $keys List of keys to remove - */ - public function removeFields(array &$values, array $keys) - { - foreach ($keys as $key) { - if (array_key_exists($key, $values)) { - unset($values[$key]); - } - } - } - - /** - * Remove keys from an array if empty - * - * @access public - * @param array $values Input array - * @param string[] $keys List of keys to remove - */ - public function removeEmptyFields(array &$values, array $keys) - { - foreach ($keys as $key) { - if (array_key_exists($key, $values) && empty($values[$key])) { - unset($values[$key]); - } - } - } - - /** - * Force fields to be at 0 if empty - * - * @access public - * @param array $values Input array - * @param string[] $keys List of keys - */ - public function resetFields(array &$values, array $keys) - { - foreach ($keys as $key) { - if (isset($values[$key]) && empty($values[$key])) { - $values[$key] = 0; - } - } - } - - /** - * Force some fields to be integer - * - * @access public - * @param array $values Input array - * @param string[] $keys List of keys - */ - public function convertIntegerFields(array &$values, array $keys) - { - foreach ($keys as $key) { - if (isset($values[$key])) { - $values[$key] = (int) $values[$key]; - } - } - } - - /** - * Force some fields to be null if empty - * - * @access public - * @param array $values Input array - * @param string[] $keys List of keys - */ - public function convertNullFields(array &$values, array $keys) - { - foreach ($keys as $key) { - if (array_key_exists($key, $values) && empty($values[$key])) { - $values[$key] = null; - } - } - } - - /** * Build SQL condition for a given time range * * @access protected @@ -135,23 +55,4 @@ abstract class Base extends \Kanboard\Core\Base return $start_column.' IS NOT NULL AND '.$start_column.' > 0 AND ('.implode(' OR ', $conditions).')'; } - - /** - * Group a collection of records by a column - * - * @access public - * @param array $collection - * @param string $column - * @return array - */ - public function groupByColumn(array $collection, $column) - { - $result = array(); - - foreach ($collection as $item) { - $result[$item[$column]][] = $item; - } - - return $result; - } } diff --git a/app/Model/Board.php b/app/Model/Board.php index 0f980f68..d41ecafe 100644 --- a/app/Model/Board.php +++ b/app/Model/Board.php @@ -2,8 +2,6 @@ namespace Kanboard\Model; -use PicoDb\Database; - /** * Board model * @@ -13,13 +11,6 @@ use PicoDb\Database; class Board extends Base { /** - * SQL table name - * - * @var string - */ - const TABLE = 'columns'; - - /** * Get Kanboard default columns * * @access public @@ -73,7 +64,7 @@ class Board extends Base 'description' => $column['description'], ); - if (! $this->db->table(self::TABLE)->save($values)) { + if (! $this->db->table(Column::TABLE)->save($values)) { return false; } } @@ -86,12 +77,12 @@ class Board extends Base * * @author Antonio Rabelo * @param integer $project_from Project Template - * @return integer $project_to Project that receives the copy + * @param integer $project_to Project that receives the copy * @return boolean */ public function duplicate($project_from, $project_to) { - $columns = $this->db->table(Board::TABLE) + $columns = $this->db->table(Column::TABLE) ->columns('title', 'task_limit', 'description') ->eq('project_id', $project_from) ->asc('position') @@ -101,134 +92,6 @@ class Board extends Base } /** - * Add a new column to the board - * - * @access public - * @param integer $project_id Project id - * @param string $title Column title - * @param integer $task_limit Task limit - * @param string $description Column description - * @return boolean|integer - */ - public function addColumn($project_id, $title, $task_limit = 0, $description = '') - { - $values = array( - 'project_id' => $project_id, - 'title' => $title, - 'task_limit' => intval($task_limit), - 'position' => $this->getLastColumnPosition($project_id) + 1, - 'description' => $description, - ); - - return $this->persist(self::TABLE, $values); - } - - /** - * Update a column - * - * @access public - * @param integer $column_id Column id - * @param string $title Column title - * @param integer $task_limit Task limit - * @param string $description Optional description - * @return boolean - */ - public function updateColumn($column_id, $title, $task_limit = 0, $description = '') - { - return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array( - 'title' => $title, - 'task_limit' => intval($task_limit), - 'description' => $description, - )); - } - - /** - * Get columns with consecutive positions - * - * If you remove a column, the positions are not anymore consecutives - * - * @access public - * @param integer $project_id - * @return array - */ - public function getNormalizedColumnPositions($project_id) - { - $columns = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'position'); - $position = 1; - - foreach ($columns as $column_id => $column_position) { - $columns[$column_id] = $position++; - } - - return $columns; - } - - /** - * Save the new positions for a set of columns - * - * @access public - * @param array $columns Hashmap of column_id/column_position - * @return boolean - */ - public function saveColumnPositions(array $columns) - { - return $this->db->transaction(function (Database $db) use ($columns) { - - foreach ($columns as $column_id => $position) { - if (! $db->table(Board::TABLE)->eq('id', $column_id)->update(array('position' => $position))) { - return false; - } - } - }); - } - - /** - * Move a column down, increment the column position value - * - * @access public - * @param integer $project_id Project id - * @param integer $column_id Column id - * @return boolean - */ - public function moveDown($project_id, $column_id) - { - $columns = $this->getNormalizedColumnPositions($project_id); - $positions = array_flip($columns); - - if (isset($columns[$column_id]) && $columns[$column_id] < count($columns)) { - $position = ++$columns[$column_id]; - $columns[$positions[$position]]--; - - return $this->saveColumnPositions($columns); - } - - return false; - } - - /** - * Move a column up, decrement the column position value - * - * @access public - * @param integer $project_id Project id - * @param integer $column_id Column id - * @return boolean - */ - public function moveUp($project_id, $column_id) - { - $columns = $this->getNormalizedColumnPositions($project_id); - $positions = array_flip($columns); - - if (isset($columns[$column_id]) && $columns[$column_id] > 1) { - $position = --$columns[$column_id]; - $columns[$positions[$position]]++; - - return $this->saveColumnPositions($columns); - } - - return false; - } - - /** * Get all tasks sorted by columns and swimlanes * * @access public @@ -239,7 +102,7 @@ class Board extends Base public function getBoard($project_id, $callback = null) { $swimlanes = $this->swimlane->getSwimlanes($project_id); - $columns = $this->getColumns($project_id); + $columns = $this->column->getAll($project_id); $nb_columns = count($columns); for ($i = 0, $ilen = count($swimlanes); $i < $ilen; $i++) { @@ -307,131 +170,4 @@ class Board extends Base return $prepend ? array(-1 => t('All columns')) + $listing : $listing; } - - /** - * Get the first column id for a given project - * - * @access public - * @param integer $project_id Project id - * @return integer - */ - public function getFirstColumn($project_id) - { - return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findOneColumn('id'); - } - - /** - * Get the last column id for a given project - * - * @access public - * @param integer $project_id Project id - * @return integer - */ - public function getLastColumn($project_id) - { - return $this->db->table(self::TABLE)->eq('project_id', $project_id)->desc('position')->findOneColumn('id'); - } - - /** - * Get the list of columns sorted by position [ column_id => title ] - * - * @access public - * @param integer $project_id Project id - * @param boolean $prepend Prepend a default value - * @return array - */ - public function getColumnsList($project_id, $prepend = false) - { - $listing = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'title'); - return $prepend ? array(-1 => t('All columns')) + $listing : $listing; - } - - /** - * Get all columns sorted by position for a given project - * - * @access public - * @param integer $project_id Project id - * @return array - */ - public function getColumns($project_id) - { - return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findAll(); - } - - /** - * Get the number of columns for a given project - * - * @access public - * @param integer $project_id Project id - * @return integer - */ - public function countColumns($project_id) - { - return $this->db->table(self::TABLE)->eq('project_id', $project_id)->count(); - } - - /** - * Get a column by the id - * - * @access public - * @param integer $column_id Column id - * @return array - */ - public function getColumn($column_id) - { - return $this->db->table(self::TABLE)->eq('id', $column_id)->findOne(); - } - - /** - * Get a column id by the name - * - * @access public - * @param integer $project_id - * @param string $title - * @return integer - */ - public function getColumnIdByTitle($project_id, $title) - { - return (int) $this->db->table(self::TABLE)->eq('project_id', $project_id)->eq('title', $title)->findOneColumn('id'); - } - - /** - * Get a column title by the id - * - * @access public - * @param integer $column_id - * @return integer - */ - public function getColumnTitleById($column_id) - { - return $this->db->table(self::TABLE)->eq('id', $column_id)->findOneColumn('title'); - } - - /** - * Get the position of the last column for a given project - * - * @access public - * @param integer $project_id Project id - * @return integer - */ - public function getLastColumnPosition($project_id) - { - return (int) $this->db - ->table(self::TABLE) - ->eq('project_id', $project_id) - ->desc('position') - ->findOneColumn('position'); - } - - /** - * Remove a column and all tasks associated to this column - * - * @access public - * @param integer $column_id Column id - * @return boolean - */ - public function removeColumn($column_id) - { - return $this->db->table(self::TABLE)->eq('id', $column_id)->remove(); - } } diff --git a/app/Model/Category.php b/app/Model/Category.php index 58cee738..1d5f6546 100644 --- a/app/Model/Category.php +++ b/app/Model/Category.php @@ -22,12 +22,11 @@ class Category extends Base * * @access public * @param integer $category_id Category id - * @param integer $project_id Project id * @return boolean */ - public function exists($category_id, $project_id) + public function exists($category_id) { - return $this->db->table(self::TABLE)->eq('id', $category_id)->eq('project_id', $project_id)->exists(); + return $this->db->table(self::TABLE)->eq('id', $category_id)->exists(); } /** @@ -115,25 +114,29 @@ class Category extends Base } /** - * Create default cetegories during project creation (transaction already started in Project::create()) + * Create default categories during project creation (transaction already started in Project::create()) * * @access public * @param integer $project_id + * @return boolean */ public function createDefaultCategories($project_id) { + $results = array(); $categories = explode(',', $this->config->get('project_categories')); foreach ($categories as $category) { $category = trim($category); if (! empty($category)) { - $this->db->table(self::TABLE)->insert(array( + $results[] = $this->db->table(self::TABLE)->insert(array( 'project_id' => $project_id, 'name' => $category, )); } } + + return in_array(false, $results, true); } /** @@ -188,16 +191,17 @@ class Category extends Base * * @author Antonio Rabelo * @param integer $src_project_id Source project id - * @return integer $dst_project_id Destination project id + * @param integer $dst_project_id Destination project id * @return boolean */ public function duplicate($src_project_id, $dst_project_id) { - $categories = $this->db->table(self::TABLE) - ->columns('name') - ->eq('project_id', $src_project_id) - ->asc('name') - ->findAll(); + $categories = $this->db + ->table(self::TABLE) + ->columns('name', 'description') + ->eq('project_id', $src_project_id) + ->asc('name') + ->findAll(); foreach ($categories as $category) { $category['project_id'] = $dst_project_id; diff --git a/app/Model/Color.php b/app/Model/Color.php index d341dd3c..dee28643 100644 --- a/app/Model/Color.php +++ b/app/Model/Color.php @@ -141,6 +141,7 @@ class Color extends Base * Get available colors * * @access public + * @param bool $prepend * @return array */ public function getList($prepend = false) @@ -177,7 +178,7 @@ class Color extends Base } /** - * Get Bordercolor from string + * Get border color from string * * @access public * @param string $color_id Color id diff --git a/app/Model/Column.php b/app/Model/Column.php new file mode 100644 index 00000000..ccdcb049 --- /dev/null +++ b/app/Model/Column.php @@ -0,0 +1,209 @@ +<?php + +namespace Kanboard\Model; + +/** + * Column Model + * + * @package model + * @author Frederic Guillot + */ +class Column extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'columns'; + + /** + * Get a column by the id + * + * @access public + * @param integer $column_id Column id + * @return array + */ + public function getById($column_id) + { + return $this->db->table(self::TABLE)->eq('id', $column_id)->findOne(); + } + + /** + * Get the first column id for a given project + * + * @access public + * @param integer $project_id Project id + * @return integer + */ + public function getFirstColumnId($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findOneColumn('id'); + } + + /** + * Get the last column id for a given project + * + * @access public + * @param integer $project_id Project id + * @return integer + */ + public function getLastColumnId($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->desc('position')->findOneColumn('id'); + } + + /** + * Get the position of the last column for a given project + * + * @access public + * @param integer $project_id Project id + * @return integer + */ + public function getLastColumnPosition($project_id) + { + return (int) $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->desc('position') + ->findOneColumn('position'); + } + + /** + * Get a column id by the name + * + * @access public + * @param integer $project_id + * @param string $title + * @return integer + */ + public function getColumnIdByTitle($project_id, $title) + { + return (int) $this->db->table(self::TABLE)->eq('project_id', $project_id)->eq('title', $title)->findOneColumn('id'); + } + + /** + * Get a column title by the id + * + * @access public + * @param integer $column_id + * @return integer + */ + public function getColumnTitleById($column_id) + { + return $this->db->table(self::TABLE)->eq('id', $column_id)->findOneColumn('title'); + } + + /** + * Get all columns sorted by position for a given project + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getAll($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findAll(); + } + + /** + * Get the list of columns sorted by position [ column_id => title ] + * + * @access public + * @param integer $project_id Project id + * @param boolean $prepend Prepend a default value + * @return array + */ + public function getList($project_id, $prepend = false) + { + $listing = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'title'); + return $prepend ? array(-1 => t('All columns')) + $listing : $listing; + } + + /** + * Add a new column to the board + * + * @access public + * @param integer $project_id Project id + * @param string $title Column title + * @param integer $task_limit Task limit + * @param string $description Column description + * @return boolean|integer + */ + public function create($project_id, $title, $task_limit = 0, $description = '') + { + $values = array( + 'project_id' => $project_id, + 'title' => $title, + 'task_limit' => intval($task_limit), + 'position' => $this->getLastColumnPosition($project_id) + 1, + 'description' => $description, + ); + + return $this->persist(self::TABLE, $values); + } + + /** + * Update a column + * + * @access public + * @param integer $column_id Column id + * @param string $title Column title + * @param integer $task_limit Task limit + * @param string $description Optional description + * @return boolean + */ + public function update($column_id, $title, $task_limit = 0, $description = '') + { + return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array( + 'title' => $title, + 'task_limit' => intval($task_limit), + 'description' => $description, + )); + } + + /** + * Remove a column and all tasks associated to this column + * + * @access public + * @param integer $column_id Column id + * @return boolean + */ + public function remove($column_id) + { + return $this->db->table(self::TABLE)->eq('id', $column_id)->remove(); + } + + /** + * Change column position + * + * @access public + * @param integer $project_id + * @param integer $column_id + * @param integer $position + * @return boolean + */ + public function changePosition($project_id, $column_id, $position) + { + if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('project_id', $project_id)->count()) { + return false; + } + + $column_ids = $this->db->table(self::TABLE)->eq('project_id', $project_id)->neq('id', $column_id)->asc('position')->findAllByColumn('id'); + $offset = 1; + $results = array(); + + foreach ($column_ids as $current_column_id) { + if ($offset == $position) { + $offset++; + } + + $results[] = $this->db->table(self::TABLE)->eq('id', $current_column_id)->update(array('position' => $offset)); + $offset++; + } + + $results[] = $this->db->table(self::TABLE)->eq('id', $column_id)->update(array('position' => $position)); + + return !in_array(false, $results, true); + } +} diff --git a/app/Model/Config.php b/app/Model/Config.php index 55999310..7b254c8d 100644 --- a/app/Model/Config.php +++ b/app/Model/Config.php @@ -76,6 +76,7 @@ class Config extends Setting 'en_US' => 'English', 'es_ES' => 'Español', 'fr_FR' => 'Français', + 'el_GR' => 'Grec', 'it_IT' => 'Italiano', 'hu_HU' => 'Magyar', 'my_MY' => 'Melayu', @@ -131,7 +132,8 @@ class Config extends Setting 'zh_CN' => 'zh-cn', 'ja_JP' => 'ja', 'th_TH' => 'th', - 'id_ID' => 'id' + 'id_ID' => 'id', + 'el_GR' => 'el', ); $lang = $this->getCurrentLanguage(); @@ -237,6 +239,7 @@ class Config extends Setting * Prepare data before save * * @access public + * @param array $values * @return array */ public function prepare(array $values) diff --git a/app/Model/File.php b/app/Model/File.php index be62cdb3..03ea691d 100644 --- a/app/Model/File.php +++ b/app/Model/File.php @@ -2,31 +2,44 @@ namespace Kanboard\Model; +use Exception; use Kanboard\Event\FileEvent; use Kanboard\Core\Tool; use Kanboard\Core\ObjectStorage\ObjectStorageException; /** - * File model + * Base File Model * * @package model * @author Frederic Guillot */ -class File extends Base +abstract class File extends Base { /** - * SQL table name - * - * @var string - */ - const TABLE = 'files'; - - /** - * Events + * Get PicoDb query to get all files * - * @var string + * @access protected + * @return \PicoDb\Table */ - const EVENT_CREATE = 'file.create'; + protected function getQuery() + { + return $this->db + ->table(static::TABLE) + ->columns( + static::TABLE.'.id', + static::TABLE.'.name', + static::TABLE.'.path', + static::TABLE.'.is_image', + static::TABLE.'.'.static::FOREIGN_KEY, + static::TABLE.'.date', + static::TABLE.'.user_id', + static::TABLE.'.size', + User::TABLE.'.username', + User::TABLE.'.name as user_name' + ) + ->join(User::TABLE, 'id', 'user_id') + ->asc(static::TABLE.'.name'); + } /** * Get a file by the id @@ -37,146 +50,120 @@ class File extends Base */ public function getById($file_id) { - return $this->db->table(self::TABLE)->eq('id', $file_id)->findOne(); + return $this->db->table(static::TABLE)->eq('id', $file_id)->findOne(); } /** - * Remove a file + * Get all files * * @access public - * @param integer $file_id File id - * @return bool + * @param integer $id + * @return array */ - public function remove($file_id) + public function getAll($id) { - try { - $file = $this->getbyId($file_id); - $this->objectStorage->remove($file['path']); - - if ($file['is_image'] == 1) { - $this->objectStorage->remove($this->getThumbnailPath($file['path'])); - } - - return $this->db->table(self::TABLE)->eq('id', $file['id'])->remove(); - } catch (ObjectStorageException $e) { - $this->logger->error($e->getMessage()); - return false; - } + return $this->getQuery()->eq(static::FOREIGN_KEY, $id)->findAll(); } /** - * Remove all files for a given task + * Get all images * * @access public - * @param integer $task_id Task id - * @return bool + * @param integer $id + * @return array */ - public function removeAll($task_id) + public function getAllImages($id) { - $file_ids = $this->db->table(self::TABLE)->eq('task_id', $task_id)->asc('id')->findAllByColumn('id'); - $results = array(); - - foreach ($file_ids as $file_id) { - $results[] = $this->remove($file_id); - } + return $this->getQuery()->eq(static::FOREIGN_KEY, $id)->eq('is_image', 1)->findAll(); + } - return ! in_array(false, $results, true); + /** + * Get all files without images + * + * @access public + * @param integer $id + * @return array + */ + public function getAllDocuments($id) + { + return $this->getQuery()->eq(static::FOREIGN_KEY, $id)->eq('is_image', 0)->findAll(); } /** * Create a file entry in the database * * @access public - * @param integer $task_id Task id + * @param integer $id Foreign key * @param string $name Filename * @param string $path Path on the disk * @param integer $size File size * @return bool|integer */ - public function create($task_id, $name, $path, $size) + public function create($id, $name, $path, $size) { - $result = $this->db->table(self::TABLE)->save(array( - 'task_id' => $task_id, + $values = array( + static::FOREIGN_KEY => $id, 'name' => substr($name, 0, 255), 'path' => $path, 'is_image' => $this->isImage($name) ? 1 : 0, 'size' => $size, 'user_id' => $this->userSession->getId() ?: 0, 'date' => time(), - )); + ); - if ($result) { - $this->container['dispatcher']->dispatch( - self::EVENT_CREATE, - new FileEvent(array('task_id' => $task_id, 'name' => $name)) - ); + $result = $this->db->table(static::TABLE)->insert($values); - return (int) $this->db->getLastId(); + if ($result) { + $file_id = (int) $this->db->getLastId(); + $event = new FileEvent($values + array('file_id' => $file_id)); + $this->dispatcher->dispatch(static::EVENT_CREATE, $event); + return $file_id; } return false; } /** - * Get PicoDb query to get all files + * Remove all files * * @access public - * @return \PicoDb\Table + * @param integer $id + * @return bool */ - public function getQuery() + public function removeAll($id) { - return $this->db - ->table(self::TABLE) - ->columns( - self::TABLE.'.id', - self::TABLE.'.name', - self::TABLE.'.path', - self::TABLE.'.is_image', - self::TABLE.'.task_id', - self::TABLE.'.date', - self::TABLE.'.user_id', - self::TABLE.'.size', - User::TABLE.'.username', - User::TABLE.'.name as user_name' - ) - ->join(User::TABLE, 'id', 'user_id') - ->asc(self::TABLE.'.name'); - } + $file_ids = $this->db->table(static::TABLE)->eq(static::FOREIGN_KEY, $id)->asc('id')->findAllByColumn('id'); + $results = array(); - /** - * Get all files for a given task - * - * @access public - * @param integer $task_id Task id - * @return array - */ - public function getAll($task_id) - { - return $this->getQuery()->eq('task_id', $task_id)->findAll(); - } + foreach ($file_ids as $file_id) { + $results[] = $this->remove($file_id); + } - /** - * Get all images for a given task - * - * @access public - * @param integer $task_id Task id - * @return array - */ - public function getAllImages($task_id) - { - return $this->getQuery()->eq('task_id', $task_id)->eq('is_image', 1)->findAll(); + return ! in_array(false, $results, true); } /** - * Get all files without images for a given task + * Remove a file * * @access public - * @param integer $task_id Task id - * @return array + * @param integer $file_id File id + * @return bool */ - public function getAllDocuments($task_id) + public function remove($file_id) { - return $this->getQuery()->eq('task_id', $task_id)->eq('is_image', 0)->findAll(); + try { + $file = $this->getById($file_id); + $this->objectStorage->remove($file['path']); + + if ($file['is_image'] == 1) { + $this->objectStorage->remove($this->getThumbnailPath($file['path'])); + } + + return $this->db->table(static::TABLE)->eq('id', $file['id'])->remove(); + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + return false; + } } /** @@ -202,127 +189,96 @@ class File extends Base } /** - * Return the image mimetype based on the file extension + * Generate the path for a thumbnails * * @access public - * @param $filename + * @param string $key Storage key * @return string */ - public function getImageMimeType($filename) + public function getThumbnailPath($key) { - $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); - - switch ($extension) { - case 'jpeg': - case 'jpg': - return 'image/jpeg'; - case 'png': - return 'image/png'; - case 'gif': - return 'image/gif'; - default: - return 'image/jpeg'; - } + return 'thumbnails'.DIRECTORY_SEPARATOR.$key; } /** * Generate the path for a new filename * * @access public - * @param integer $project_id Project id - * @param integer $task_id Task id + * @param integer $id Foreign key * @param string $filename Filename * @return string */ - public function generatePath($project_id, $task_id, $filename) + public function generatePath($id, $filename) { - return $project_id.DIRECTORY_SEPARATOR.$task_id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time()); + return static::PATH_PREFIX.DIRECTORY_SEPARATOR.$id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time()); } /** - * Generate the path for a thumbnails + * Upload multiple files * * @access public - * @param string $key Storage key - * @return string - */ - public function getThumbnailPath($key) - { - return 'thumbnails'.DIRECTORY_SEPARATOR.$key; - } - - /** - * Handle file upload - * - * @access public - * @param integer $project_id Project id - * @param integer $task_id Task id - * @param string $form_name File form name + * @param integer $id + * @param array $files * @return bool */ - public function uploadFiles($project_id, $task_id, $form_name) + public function uploadFiles($id, array $files) { try { - $file = $this->request->getFileInfo($form_name); - - if (empty($file)) { + if (empty($files)) { return false; } - foreach ($file['error'] as $key => $error) { - if ($error == UPLOAD_ERR_OK && $file['size'][$key] > 0) { - $original_filename = $file['name'][$key]; - $uploaded_filename = $file['tmp_name'][$key]; - $destination_filename = $this->generatePath($project_id, $task_id, $original_filename); - - if ($this->isImage($original_filename)) { - $this->generateThumbnailFromFile($uploaded_filename, $destination_filename); - } - - $this->objectStorage->moveUploadedFile($uploaded_filename, $destination_filename); - - $this->create( - $task_id, - $original_filename, - $destination_filename, - $file['size'][$key] - ); - } + foreach (array_keys($files['error']) as $key) { + $file = array( + 'name' => $files['name'][$key], + 'tmp_name' => $files['tmp_name'][$key], + 'size' => $files['size'][$key], + 'error' => $files['error'][$key], + ); + + $this->uploadFile($id, $file); } return true; - } catch (ObjectStorageException $e) { + } catch (Exception $e) { $this->logger->error($e->getMessage()); return false; } } /** - * Handle screenshot upload + * Upload a file * * @access public - * @param integer $project_id Project id - * @param integer $task_id Task id - * @param string $blob Base64 encoded image - * @return bool|integer + * @param integer $id + * @param array $file */ - public function uploadScreenshot($project_id, $task_id, $blob) + public function uploadFile($id, array $file) { - $original_filename = e('Screenshot taken %s', dt('%B %e, %Y at %k:%M %p', time())).'.png'; - return $this->uploadContent($project_id, $task_id, $original_filename, $blob); + if ($file['error'] == UPLOAD_ERR_OK && $file['size'] > 0) { + $destination_filename = $this->generatePath($id, $file['name']); + + if ($this->isImage($file['name'])) { + $this->generateThumbnailFromFile($file['tmp_name'], $destination_filename); + } + + $this->objectStorage->moveUploadedFile($file['tmp_name'], $destination_filename); + $this->create($id, $file['name'], $destination_filename, $file['size']); + } else { + throw new Exception('File not uploaded: '.var_export($file['error'], true)); + } } /** * Handle file upload (base64 encoded content) * * @access public - * @param integer $project_id Project id - * @param integer $task_id Task id - * @param string $original_filename Filename - * @param string $blob Base64 encoded file + * @param integer $id + * @param string $original_filename + * @param string $blob * @return bool|integer */ - public function uploadContent($project_id, $task_id, $original_filename, $blob) + public function uploadContent($id, $original_filename, $blob) { try { $data = base64_decode($blob); @@ -331,7 +287,7 @@ class File extends Base return false; } - $destination_filename = $this->generatePath($project_id, $task_id, $original_filename); + $destination_filename = $this->generatePath($id, $original_filename); $this->objectStorage->put($destination_filename, $data); if ($this->isImage($original_filename)) { @@ -339,7 +295,7 @@ class File extends Base } return $this->create( - $task_id, + $id, $original_filename, $destination_filename, strlen($data) diff --git a/app/Model/LastLogin.php b/app/Model/LastLogin.php index f5be020e..feb5f5a3 100644 --- a/app/Model/LastLogin.php +++ b/app/Model/LastLogin.php @@ -39,14 +39,14 @@ class LastLogin extends Base $this->cleanup($user_id); return $this->db - ->table(self::TABLE) - ->insert(array( - 'auth_type' => $auth_type, - 'user_id' => $user_id, - 'ip' => $ip, - 'user_agent' => substr($user_agent, 0, 255), - 'date_creation' => time(), - )); + ->table(self::TABLE) + ->insert(array( + 'auth_type' => $auth_type, + 'user_id' => $user_id, + 'ip' => $ip, + 'user_agent' => substr($user_agent, 0, 255), + 'date_creation' => time(), + )); } /** @@ -65,9 +65,9 @@ class LastLogin extends Base if (count($connections) >= self::NB_LOGINS) { $this->db->table(self::TABLE) - ->eq('user_id', $user_id) - ->notin('id', array_slice($connections, 0, self::NB_LOGINS - 1)) - ->remove(); + ->eq('user_id', $user_id) + ->notin('id', array_slice($connections, 0, self::NB_LOGINS - 1)) + ->remove(); } } diff --git a/app/Model/Link.php b/app/Model/Link.php index 7b81a237..903a98d6 100644 --- a/app/Model/Link.php +++ b/app/Model/Link.php @@ -90,7 +90,7 @@ class Link extends Base * * @access public * @param integer $exclude_id Exclude this link - * @param booelan $prepend Prepend default value + * @param boolean $prepend Prepend default value * @return array */ public function getList($exclude_id = 0, $prepend = true) diff --git a/app/Model/Metadata.php b/app/Model/Metadata.php index 690b2265..cb66c717 100644 --- a/app/Model/Metadata.php +++ b/app/Model/Metadata.php @@ -76,6 +76,7 @@ abstract class Metadata extends Base * @access public * @param integer $entity_id * @param array $values + * @return boolean */ public function save($entity_id, array $values) { diff --git a/app/Model/Notification.php b/app/Model/Notification.php index 87c1a796..c252aa31 100644 --- a/app/Model/Notification.php +++ b/app/Model/Notification.php @@ -72,7 +72,7 @@ class Notification extends Base return e('%s updated a comment on the task #%d', $event_author, $event_data['task']['id']); case Comment::EVENT_CREATE: return e('%s commented on the task #%d', $event_author, $event_data['task']['id']); - case File::EVENT_CREATE: + case TaskFile::EVENT_CREATE: return e('%s attached a file to the task #%d', $event_author, $event_data['task']['id']); case Task::EVENT_USER_MENTION: return e('%s mentioned you in the task #%d', $event_author, $event_data['task']['id']); @@ -94,7 +94,7 @@ class Notification extends Base public function getTitleWithoutAuthor($event_name, array $event_data) { switch ($event_name) { - case File::EVENT_CREATE: + case TaskFile::EVENT_CREATE: return e('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']); case Comment::EVENT_CREATE: return e('New comment on task #%d', $event_data['comment']['task_id']); diff --git a/app/Model/NotificationType.php b/app/Model/NotificationType.php index 9d8317b7..289aae9c 100644 --- a/app/Model/NotificationType.php +++ b/app/Model/NotificationType.php @@ -80,7 +80,7 @@ abstract class NotificationType extends Base * * @access public * @param string $type - * @return NotificationInterface + * @return \Kanboard\Notification\NotificationInterface */ public function getType($type) { diff --git a/app/Model/OverdueNotification.php b/app/Model/OverdueNotification.php deleted file mode 100644 index 84565548..00000000 --- a/app/Model/OverdueNotification.php +++ /dev/null @@ -1,58 +0,0 @@ -<?php - -namespace Kanboard\Model; - -/** - * Task Overdue Notification model - * - * @package model - * @author Frederic Guillot - */ -class OverdueNotification extends Base -{ - /** - * Send overdue tasks - * - * @access public - */ - public function sendOverdueTaskNotifications() - { - $tasks = $this->taskFinder->getOverdueTasks(); - - foreach ($this->groupByColumn($tasks, 'project_id') as $project_id => $project_tasks) { - $users = $this->userNotification->getUsersWithNotificationEnabled($project_id); - - foreach ($users as $user) { - $this->sendUserOverdueTaskNotifications($user, $project_tasks); - } - } - - return $tasks; - } - - /** - * Send overdue tasks for a given user - * - * @access public - * @param array $user - * @param array $tasks - */ - public function sendUserOverdueTaskNotifications(array $user, array $tasks) - { - $user_tasks = array(); - - foreach ($tasks as $task) { - if ($this->userNotificationFilter->shouldReceiveNotification($user, array('task' => $task))) { - $user_tasks[] = $task; - } - } - - if (! empty($user_tasks)) { - $this->userNotification->sendUserNotification( - $user, - Task::EVENT_OVERDUE, - array('tasks' => $user_tasks, 'project_name' => $tasks[0]['project_name']) - ); - } - } -} diff --git a/app/Model/PasswordReset.php b/app/Model/PasswordReset.php index c2d7dde9..5cfd3c97 100644 --- a/app/Model/PasswordReset.php +++ b/app/Model/PasswordReset.php @@ -20,7 +20,7 @@ class PasswordReset extends Base /** * Token duration (30 minutes) * - * @var string + * @var integer */ const DURATION = 1800; diff --git a/app/Model/Project.php b/app/Model/Project.php index d0a8bfc8..d2e5b7ce 100644 --- a/app/Model/Project.php +++ b/app/Model/Project.php @@ -241,7 +241,7 @@ class Project extends Base { $stats = array(); $stats['nb_active_tasks'] = 0; - $columns = $this->board->getColumns($project_id); + $columns = $this->column->getAll($project_id); $column_stats = $this->board->getColumnStats($project_id); foreach ($columns as &$column) { @@ -265,7 +265,7 @@ class Project extends Base */ public function getColumnStats(array &$project) { - $project['columns'] = $this->board->getColumns($project['id']); + $project['columns'] = $this->column->getAll($project['id']); $stats = $this->board->getColumnStats($project['id']); foreach ($project['columns'] as &$column) { @@ -334,7 +334,7 @@ class Project extends Base $values['identifier'] = strtoupper($values['identifier']); } - $this->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end')); + $this->helper->model->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end')); if (! $this->db->table(self::TABLE)->save($values)) { $this->db->cancelTransaction(); @@ -402,7 +402,7 @@ class Project extends Base $values['identifier'] = strtoupper($values['identifier']); } - $this->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end')); + $this->helper->model->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end')); return $this->exists($values['id']) && $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); diff --git a/app/Model/ProjectDailyColumnStats.php b/app/Model/ProjectDailyColumnStats.php index cf79be84..2bcc4d55 100644 --- a/app/Model/ProjectDailyColumnStats.php +++ b/app/Model/ProjectDailyColumnStats.php @@ -84,7 +84,7 @@ class ProjectDailyColumnStats extends Base */ public function getAggregatedMetrics($project_id, $from, $to, $field = 'total') { - $columns = $this->board->getColumnsList($project_id); + $columns = $this->column->getList($project_id); $metrics = $this->getMetrics($project_id, $from, $to); return $this->buildAggregate($metrics, $columns, $field); } diff --git a/app/Model/ProjectDuplication.php b/app/Model/ProjectDuplication.php index f0c66834..9c5f80ad 100644 --- a/app/Model/ProjectDuplication.php +++ b/app/Model/ProjectDuplication.php @@ -2,6 +2,8 @@ namespace Kanboard\Model; +use Kanboard\Core\Security\Role; + /** * Project Duplication * @@ -12,6 +14,28 @@ namespace Kanboard\Model; class ProjectDuplication extends Base { /** + * Get list of optional models to duplicate + * + * @access public + * @return string[] + */ + public function getOptionalSelection() + { + return array('category', 'projectPermission', 'action', 'swimlane', 'task'); + } + + /** + * Get list of all possible models to duplicate + * + * @access public + * @return string[] + */ + public function getPossibleSelection() + { + return array('board', 'category', 'projectPermission', 'action', 'swimlane', 'task'); + } + + /** * Get a valid project name for the duplication * * @access public @@ -31,78 +55,106 @@ class ProjectDuplication extends Base } /** - * Create a project from another one - * - * @param integer $project_id Project Id - * @return integer Cloned Project Id - */ - public function copy($project_id) - { - $project = $this->project->getById($project_id); - - $values = array( - 'name' => $this->getClonedProjectName($project['name']), - 'is_active' => true, - 'last_modified' => 0, - 'token' => '', - 'is_public' => 0, - 'is_private' => empty($project['is_private']) ? 0 : 1, - ); - - if (! $this->db->table(Project::TABLE)->save($values)) { - return 0; - } - - return $this->db->getLastId(); - } - - /** * Clone a project with all settings * - * @param integer $project_id Project Id - * @param array $part_selection Selection of optional project parts to duplicate. Possible options: 'swimlane', 'action', 'category', 'task' - * @return integer Cloned Project Id + * @param integer $src_project_id Project Id + * @param array $selection Selection of optional project parts to duplicate + * @param integer $owner_id Owner of the project + * @param string $name Name of the project + * @param boolean $private Force the project to be private + * @return integer Cloned Project Id */ - public function duplicate($project_id, $part_selection = array('category', 'action')) + public function duplicate($src_project_id, $selection = array('projectPermission', 'category', 'action'), $owner_id = 0, $name = null, $private = null) { $this->db->startTransaction(); // Get the cloned project Id - $clone_project_id = $this->copy($project_id); + $dst_project_id = $this->copy($src_project_id, $owner_id, $name, $private); - if (! $clone_project_id) { + if ($dst_project_id === false) { $this->db->cancelTransaction(); return false; } // Clone Columns, Categories, Permissions and Actions - $optional_parts = array('swimlane', 'action', 'category'); - foreach (array('board', 'category', 'projectPermission', 'action', 'swimlane') as $model) { + foreach ($this->getPossibleSelection() as $model) { // Skip if optional part has not been selected - if (in_array($model, $optional_parts) && ! in_array($model, $part_selection)) { + if (in_array($model, $this->getOptionalSelection()) && ! in_array($model, $selection)) { continue; } - if (! $this->$model->duplicate($project_id, $clone_project_id)) { + // Skip permissions for private projects + if ($private && $model === 'projectPermission') { + continue; + } + + if (! $this->$model->duplicate($src_project_id, $dst_project_id)) { $this->db->cancelTransaction(); return false; } } + if (! $this->makeOwnerManager($dst_project_id, $owner_id)) { + $this->db->cancelTransaction(); + return false; + } + $this->db->closeTransaction(); - // Clone Tasks if in $part_selection - if (in_array('task', $part_selection)) { - $tasks = $this->taskFinder->getAll($project_id); + return (int) $dst_project_id; + } + + /** + * Create a project from another one + * + * @access private + * @param integer $src_project_id + * @param integer $owner_id + * @param string $name + * @param boolean $private + * @return integer + */ + private function copy($src_project_id, $owner_id = 0, $name = null, $private = null) + { + $project = $this->project->getById($src_project_id); + $is_private = empty($project['is_private']) ? 0 : 1; + + $values = array( + 'name' => $name ?: $this->getClonedProjectName($project['name']), + 'is_active' => 1, + 'last_modified' => time(), + 'token' => '', + 'is_public' => 0, + 'is_private' => $private ? 1 : $is_private, + 'owner_id' => $owner_id, + ); + + if (! $this->db->table(Project::TABLE)->save($values)) { + return false; + } + + return $this->db->getLastId(); + } + + /** + * Make sure that the creator of the duplicated project is alsp owner + * + * @access private + * @param integer $dst_project_id + * @param integer $owner_id + * @return boolean + */ + private function makeOwnerManager($dst_project_id, $owner_id) + { + if ($owner_id > 0) { + $this->projectUserRole->removeUser($dst_project_id, $owner_id); - foreach ($tasks as $task) { - if (! $this->taskDuplication->duplicateToProject($task['id'], $clone_project_id)) { - return false; - } + if (! $this->projectUserRole->addUser($dst_project_id, $owner_id, Role::PROJECT_MANAGER)) { + return false; } } - return (int) $clone_project_id; + return true; } } diff --git a/app/Model/ProjectFile.php b/app/Model/ProjectFile.php new file mode 100644 index 00000000..aa9bf15b --- /dev/null +++ b/app/Model/ProjectFile.php @@ -0,0 +1,40 @@ +<?php + +namespace Kanboard\Model; + +/** + * Project File Model + * + * @package model + * @author Frederic Guillot + */ +class ProjectFile extends File +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'project_has_files'; + + /** + * SQL foreign key + * + * @var string + */ + const FOREIGN_KEY = 'project_id'; + + /** + * Path prefix + * + * @var string + */ + const PATH_PREFIX = 'projects'; + + /** + * Events + * + * @var string + */ + const EVENT_CREATE = 'project.file.create'; +} diff --git a/app/Model/ProjectGroupRole.php b/app/Model/ProjectGroupRole.php index 591b28c6..afad4a44 100644 --- a/app/Model/ProjectGroupRole.php +++ b/app/Model/ProjectGroupRole.php @@ -106,6 +106,7 @@ class ProjectGroupRole extends Base ->join(GroupMember::TABLE, 'user_id', 'id', User::TABLE) ->join(self::TABLE, 'group_id', 'group_id', GroupMember::TABLE) ->eq(self::TABLE.'.project_id', $project_id) + ->eq(User::TABLE.'.is_active', 1) ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER)) ->asc(User::TABLE.'.username') ->findAll(); @@ -165,7 +166,7 @@ class ProjectGroupRole extends Base * Copy group access from a project to another one * * @param integer $project_src_id Project Template - * @return integer $project_dst_id Project that receives the copy + * @param integer $project_dst_id Project that receives the copy * @return boolean */ public function duplicate($project_src_id, $project_dst_id) diff --git a/app/Model/ProjectPermission.php b/app/Model/ProjectPermission.php index cea62e13..db1573ae 100644 --- a/app/Model/ProjectPermission.php +++ b/app/Model/ProjectPermission.php @@ -107,7 +107,8 @@ class ProjectPermission extends Base */ public function isAssignable($project_id, $user_id) { - return in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER)); + return $this->user->isActive($user_id) && + in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER)); } /** diff --git a/app/Model/ProjectUserRole.php b/app/Model/ProjectUserRole.php index 8149a253..56da679c 100644 --- a/app/Model/ProjectUserRole.php +++ b/app/Model/ProjectUserRole.php @@ -152,13 +152,14 @@ class ProjectUserRole extends Base public function getAssignableUsers($project_id) { if ($this->projectPermission->isEverybodyAllowed($project_id)) { - return $this->user->getList(); + return $this->user->getActiveUsersList(); } $userMembers = $this->db->table(self::TABLE) ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name') ->join(User::TABLE, 'id', 'user_id') - ->eq('project_id', $project_id) + ->eq(User::TABLE.'.is_active', 1) + ->eq(self::TABLE.'.project_id', $project_id) ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER)) ->findAll(); diff --git a/app/Model/Setting.php b/app/Model/Setting.php index 44e6c065..6d29c6ec 100644 --- a/app/Model/Setting.php +++ b/app/Model/Setting.php @@ -75,6 +75,7 @@ abstract class Setting extends Base * * @access public * @param array $values + * @return boolean */ public function save(array $values) { diff --git a/app/Model/Subtask.php b/app/Model/Subtask.php index 0e039bb3..3f5cfe83 100644 --- a/app/Model/Subtask.php +++ b/app/Model/Subtask.php @@ -168,8 +168,8 @@ class Subtask extends Base */ public function prepare(array &$values) { - $this->removeFields($values, array('another_subtask')); - $this->resetFields($values, array('time_estimated', 'time_spent')); + $this->helper->model->removeFields($values, array('another_subtask')); + $this->helper->model->resetFields($values, array('time_estimated', 'time_spent')); } /** @@ -263,89 +263,36 @@ class Subtask extends Base } /** - * Get subtasks with consecutive positions - * - * If you remove a subtask, the positions are not anymore consecutives - * - * @access public - * @param integer $task_id - * @return array - */ - public function getNormalizedPositions($task_id) - { - $subtasks = $this->db->hashtable(self::TABLE)->eq('task_id', $task_id)->asc('position')->getAll('id', 'position'); - $position = 1; - - foreach ($subtasks as $subtask_id => $subtask_position) { - $subtasks[$subtask_id] = $position++; - } - - return $subtasks; - } - - /** - * Save the new positions for a set of subtasks - * - * @access public - * @param array $subtasks Hashmap of column_id/column_position - * @return boolean - */ - public function savePositions(array $subtasks) - { - return $this->db->transaction(function (Database $db) use ($subtasks) { - - foreach ($subtasks as $subtask_id => $position) { - if (! $db->table(Subtask::TABLE)->eq('id', $subtask_id)->update(array('position' => $position))) { - return false; - } - } - }); - } - - /** - * Move a subtask down, increment the position value + * Save subtask position * * @access public * @param integer $task_id * @param integer $subtask_id + * @param integer $position * @return boolean */ - public function moveDown($task_id, $subtask_id) + public function changePosition($task_id, $subtask_id, $position) { - $subtasks = $this->getNormalizedPositions($task_id); - $positions = array_flip($subtasks); - - if (isset($subtasks[$subtask_id]) && $subtasks[$subtask_id] < count($subtasks)) { - $position = ++$subtasks[$subtask_id]; - $subtasks[$positions[$position]]--; - - return $this->savePositions($subtasks); + if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('task_id', $task_id)->count()) { + return false; } - return false; - } + $subtask_ids = $this->db->table(self::TABLE)->eq('task_id', $task_id)->neq('id', $subtask_id)->asc('position')->findAllByColumn('id'); + $offset = 1; + $results = array(); - /** - * Move a subtask up, decrement the position value - * - * @access public - * @param integer $task_id - * @param integer $subtask_id - * @return boolean - */ - public function moveUp($task_id, $subtask_id) - { - $subtasks = $this->getNormalizedPositions($task_id); - $positions = array_flip($subtasks); - - if (isset($subtasks[$subtask_id]) && $subtasks[$subtask_id] > 1) { - $position = --$subtasks[$subtask_id]; - $subtasks[$positions[$position]]++; + foreach ($subtask_ids as $current_subtask_id) { + if ($offset == $position) { + $offset++; + } - return $this->savePositions($subtasks); + $results[] = $this->db->table(self::TABLE)->eq('id', $current_subtask_id)->update(array('position' => $offset)); + $offset++; } - return false; + $results[] = $this->db->table(self::TABLE)->eq('id', $subtask_id)->update(array('position' => $position)); + + return !in_array(false, $results, true); } /** @@ -353,15 +300,16 @@ class Subtask extends Base * * @access public * @param integer $subtask_id - * @return bool + * @return boolean|integer */ public function toggleStatus($subtask_id) { $subtask = $this->getById($subtask_id); + $status = ($subtask['status'] + 1) % 3; $values = array( 'id' => $subtask['id'], - 'status' => ($subtask['status'] + 1) % 3, + 'status' => $status, 'task_id' => $subtask['task_id'], ); @@ -369,7 +317,7 @@ class Subtask extends Base $values['user_id'] = $this->userSession->getId(); } - return $this->update($values); + return $this->update($values) ? $status : false; } /** @@ -435,10 +383,10 @@ class Subtask extends Base return $this->db->transaction(function (Database $db) use ($src_task_id, $dst_task_id) { $subtasks = $db->table(Subtask::TABLE) - ->columns('title', 'time_estimated', 'position') - ->eq('task_id', $src_task_id) - ->asc('position') - ->findAll(); + ->columns('title', 'time_estimated', 'position') + ->eq('task_id', $src_task_id) + ->asc('position') + ->findAll(); foreach ($subtasks as &$subtask) { $subtask['task_id'] = $dst_task_id; diff --git a/app/Model/SubtaskExport.php b/app/Model/SubtaskExport.php deleted file mode 100644 index 7c4e941d..00000000 --- a/app/Model/SubtaskExport.php +++ /dev/null @@ -1,119 +0,0 @@ -<?php - -namespace Kanboard\Model; - -/** - * Subtask Export - * - * @package model - * @author Frederic Guillot - */ -class SubtaskExport extends Base -{ - /** - * Subtask statuses - * - * @access private - * @var array - */ - private $subtask_status = array(); - - /** - * Fetch subtasks and return the prepared CSV - * - * @access public - * @param integer $project_id Project id - * @param mixed $from Start date (timestamp or user formatted date) - * @param mixed $to End date (timestamp or user formatted date) - * @return array - */ - public function export($project_id, $from, $to) - { - $this->subtask_status = $this->subtask->getStatusList(); - $subtasks = $this->getSubtasks($project_id, $from, $to); - $results = array($this->getColumns()); - - foreach ($subtasks as $subtask) { - $results[] = $this->format($subtask); - } - - return $results; - } - - /** - * Get column titles - * - * @access public - * @return string[] - */ - public function getColumns() - { - return array( - e('Subtask Id'), - e('Title'), - e('Status'), - e('Assignee'), - e('Time estimated'), - e('Time spent'), - e('Task Id'), - e('Task Title'), - ); - } - - /** - * Format the output of a subtask array - * - * @access public - * @param array $subtask Subtask properties - * @return array - */ - public function format(array $subtask) - { - $values = array(); - $values[] = $subtask['id']; - $values[] = $subtask['title']; - $values[] = $this->subtask_status[$subtask['status']]; - $values[] = $subtask['assignee_name'] ?: $subtask['assignee_username']; - $values[] = $subtask['time_estimated']; - $values[] = $subtask['time_spent']; - $values[] = $subtask['task_id']; - $values[] = $subtask['task_title']; - - return $values; - } - - /** - * Get all subtasks for a given project - * - * @access public - * @param integer $project_id Project id - * @param mixed $from Start date (timestamp or user formatted date) - * @param mixed $to End date (timestamp or user formatted date) - * @return array - */ - public function getSubtasks($project_id, $from, $to) - { - if (! is_numeric($from)) { - $from = $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($from)); - } - - if (! is_numeric($to)) { - $to = $this->dateParser->removeTimeFromTimestamp(strtotime('+1 day', $this->dateParser->getTimestamp($to))); - } - - return $this->db->table(Subtask::TABLE) - ->eq('project_id', $project_id) - ->columns( - Subtask::TABLE.'.*', - User::TABLE.'.username AS assignee_username', - User::TABLE.'.name AS assignee_name', - Task::TABLE.'.title AS task_title' - ) - ->gte('date_creation', $from) - ->lte('date_creation', $to) - ->join(Task::TABLE, 'id', 'task_id') - ->join(User::TABLE, 'id', 'user_id') - ->asc(Subtask::TABLE.'.id') - ->findAll(); - } -} diff --git a/app/Model/SubtaskTimeTracking.php b/app/Model/SubtaskTimeTracking.php index a741dbb5..b766b542 100644 --- a/app/Model/SubtaskTimeTracking.php +++ b/app/Model/SubtaskTimeTracking.php @@ -302,11 +302,11 @@ class SubtaskTimeTracking extends Base { $hook = 'model:subtask-time-tracking:calculate:time-spent'; $start_time = $this->db - ->table(self::TABLE) - ->eq('subtask_id', $subtask_id) - ->eq('user_id', $user_id) - ->eq('end', 0) - ->findOneColumn('start'); + ->table(self::TABLE) + ->eq('subtask_id', $subtask_id) + ->eq('user_id', $user_id) + ->eq('end', 0) + ->findOneColumn('start'); if (empty($start_time)) { return 0; diff --git a/app/Model/Swimlane.php b/app/Model/Swimlane.php index e5124e8e..721f20d3 100644 --- a/app/Model/Swimlane.php +++ b/app/Model/Swimlane.php @@ -96,10 +96,11 @@ class Swimlane extends Base */ public function getDefault($project_id) { - $result = $this->db->table(Project::TABLE) - ->eq('id', $project_id) - ->columns('id', 'default_swimlane', 'show_default_swimlane') - ->findOne(); + $result = $this->db + ->table(Project::TABLE) + ->eq('id', $project_id) + ->columns('id', 'default_swimlane', 'show_default_swimlane') + ->findOne(); if ($result['default_swimlane'] === 'Default swimlane') { $result['default_swimlane'] = t($result['default_swimlane']); @@ -117,10 +118,11 @@ class Swimlane extends Base */ public function getAll($project_id) { - return $this->db->table(self::TABLE) - ->eq('project_id', $project_id) - ->orderBy('position', 'asc') - ->findAll(); + return $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->orderBy('position', 'asc') + ->findAll(); } /** @@ -133,9 +135,10 @@ class Swimlane extends Base */ public function getAllByStatus($project_id, $status = self::ACTIVE) { - $query = $this->db->table(self::TABLE) - ->eq('project_id', $project_id) - ->eq('is_active', $status); + $query = $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', $status); if ($status == self::ACTIVE) { $query->asc('position'); @@ -155,17 +158,19 @@ class Swimlane extends Base */ public function getSwimlanes($project_id) { - $swimlanes = $this->db->table(self::TABLE) - ->columns('id', 'name', 'description') - ->eq('project_id', $project_id) - ->eq('is_active', self::ACTIVE) - ->orderBy('position', 'asc') - ->findAll(); - - $default_swimlane = $this->db->table(Project::TABLE) - ->eq('id', $project_id) - ->eq('show_default_swimlane', 1) - ->findOneColumn('default_swimlane'); + $swimlanes = $this->db + ->table(self::TABLE) + ->columns('id', 'name', 'description') + ->eq('project_id', $project_id) + ->eq('is_active', self::ACTIVE) + ->orderBy('position', 'asc') + ->findAll(); + + $default_swimlane = $this->db + ->table(Project::TABLE) + ->eq('id', $project_id) + ->eq('show_default_swimlane', 1) + ->findOneColumn('default_swimlane'); if ($default_swimlane) { if ($default_swimlane === 'Default swimlane') { @@ -200,11 +205,12 @@ class Swimlane extends Base $swimlanes[0] = $default === 'Default swimlane' ? t($default) : $default; } - return $swimlanes + $this->db->hashtable(self::TABLE) - ->eq('project_id', $project_id) - ->in('is_active', $only_active ? array(self::ACTIVE) : array(self::ACTIVE, self::INACTIVE)) - ->orderBy('position', 'asc') - ->getAll('id', 'name'); + return $swimlanes + $this->db + ->hashtable(self::TABLE) + ->eq('project_id', $project_id) + ->in('is_active', $only_active ? array(self::ACTIVE) : array(self::ACTIVE, self::INACTIVE)) + ->orderBy('position', 'asc') + ->getAll('id', 'name'); } /** @@ -232,9 +238,10 @@ class Swimlane extends Base */ public function update(array $values) { - return $this->db->table(self::TABLE) - ->eq('id', $values['id']) - ->update($values); + return $this->db + ->table(self::TABLE) + ->eq('id', $values['id']) + ->update($values); } /** @@ -247,12 +254,46 @@ class Swimlane extends Base public function updateDefault(array $values) { return $this->db - ->table(Project::TABLE) - ->eq('id', $values['id']) - ->update(array( - 'default_swimlane' => $values['default_swimlane'], - 'show_default_swimlane' => $values['show_default_swimlane'], - )); + ->table(Project::TABLE) + ->eq('id', $values['id']) + ->update(array( + 'default_swimlane' => $values['default_swimlane'], + 'show_default_swimlane' => $values['show_default_swimlane'], + )); + } + + /** + * Enable the default swimlane + * + * @access public + * @param integer $project_id + * @return bool + */ + public function enableDefault($project_id) + { + return $this->db + ->table(Project::TABLE) + ->eq('id', $project_id) + ->update(array( + 'show_default_swimlane' => 1, + )); + } + + /** + * Disable the default swimlane + * + * @access public + * @param integer $project_id + * @return bool + */ + public function disableDefault($project_id) + { + return $this->db + ->table(Project::TABLE) + ->eq('id', $project_id) + ->update(array( + 'show_default_swimlane' => 0, + )); } /** @@ -264,10 +305,11 @@ class Swimlane extends Base */ public function getLastPosition($project_id) { - return $this->db->table(self::TABLE) - ->eq('project_id', $project_id) - ->eq('is_active', 1) - ->count() + 1; + return $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', 1) + ->count() + 1; } /** @@ -281,12 +323,12 @@ class Swimlane extends Base public function disable($project_id, $swimlane_id) { $result = $this->db - ->table(self::TABLE) - ->eq('id', $swimlane_id) - ->update(array( - 'is_active' => self::INACTIVE, - 'position' => 0, - )); + ->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array( + 'is_active' => self::INACTIVE, + 'position' => 0, + )); if ($result) { // Re-order positions @@ -307,12 +349,12 @@ class Swimlane extends Base public function enable($project_id, $swimlane_id) { return $this->db - ->table(self::TABLE) - ->eq('id', $swimlane_id) - ->update(array( - 'is_active' => self::ACTIVE, - 'position' => $this->getLastPosition($project_id), - )); + ->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array( + 'is_active' => self::ACTIVE, + 'position' => $this->getLastPosition($project_id), + )); } /** @@ -353,11 +395,13 @@ class Swimlane extends Base public function updatePositions($project_id) { $position = 0; - $swimlanes = $this->db->table(self::TABLE) - ->eq('project_id', $project_id) - ->eq('is_active', 1) - ->asc('position') - ->findAllByColumn('id'); + $swimlanes = $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', 1) + ->asc('position') + ->asc('id') + ->findAllByColumn('id'); if (! $swimlanes) { return false; @@ -365,77 +409,50 @@ class Swimlane extends Base foreach ($swimlanes as $swimlane_id) { $this->db->table(self::TABLE) - ->eq('id', $swimlane_id) - ->update(array('position' => ++$position)); + ->eq('id', $swimlane_id) + ->update(array('position' => ++$position)); } return true; } /** - * Move a swimlane down, increment the position value + * Change swimlane position * * @access public - * @param integer $project_id Project id - * @param integer $swimlane_id Swimlane id + * @param integer $project_id + * @param integer $swimlane_id + * @param integer $position * @return boolean */ - public function moveDown($project_id, $swimlane_id) + public function changePosition($project_id, $swimlane_id, $position) { - $swimlanes = $this->db->hashtable(self::TABLE) - ->eq('project_id', $project_id) - ->eq('is_active', self::ACTIVE) - ->asc('position') - ->getAll('id', 'position'); - - $positions = array_flip($swimlanes); - - if (isset($swimlanes[$swimlane_id]) && $swimlanes[$swimlane_id] < count($swimlanes)) { - $position = ++$swimlanes[$swimlane_id]; - $swimlanes[$positions[$position]]--; - - $this->db->startTransaction(); - $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position)); - $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $swimlanes[$positions[$position]])); - $this->db->closeTransaction(); - - return true; + if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('project_id', $project_id)->count()) { + return false; } - return false; - } + $swimlane_ids = $this->db->table(self::TABLE) + ->eq('is_active', 1) + ->eq('project_id', $project_id) + ->neq('id', $swimlane_id) + ->asc('position') + ->findAllByColumn('id'); - /** - * Move a swimlane up, decrement the position value - * - * @access public - * @param integer $project_id Project id - * @param integer $swimlane_id Swimlane id - * @return boolean - */ - public function moveUp($project_id, $swimlane_id) - { - $swimlanes = $this->db->hashtable(self::TABLE) - ->eq('project_id', $project_id) - ->eq('is_active', self::ACTIVE) - ->asc('position') - ->getAll('id', 'position'); - - $positions = array_flip($swimlanes); - - if (isset($swimlanes[$swimlane_id]) && $swimlanes[$swimlane_id] > 1) { - $position = --$swimlanes[$swimlane_id]; - $swimlanes[$positions[$position]]++; + $offset = 1; + $results = array(); - $this->db->startTransaction(); - $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position)); - $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $swimlanes[$positions[$position]])); - $this->db->closeTransaction(); + foreach ($swimlane_ids as $current_swimlane_id) { + if ($offset == $position) { + $offset++; + } - return true; + $results[] = $this->db->table(self::TABLE)->eq('id', $current_swimlane_id)->update(array('position' => $offset)); + $offset++; } - return false; + $results[] = $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position)); + + return !in_array(false, $results, true); } /** diff --git a/app/Model/Task.php b/app/Model/Task.php index 7aa9e312..f8b41b9f 100644 --- a/app/Model/Task.php +++ b/app/Model/Task.php @@ -42,6 +42,7 @@ class Task extends Base const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change'; const EVENT_OVERDUE = 'task.overdue'; const EVENT_USER_MENTION = 'task.user.mention'; + const EVENT_DAILY_CRONJOB = 'task.cronjob.daily'; /** * Recurrence: status @@ -91,7 +92,7 @@ class Task extends Base return false; } - $this->file->removeAll($task_id); + $this->taskFile->removeAll($task_id); return $this->db->table(self::TABLE)->eq('id', $task_id)->remove(); } @@ -198,4 +199,25 @@ class Task extends Base return round(($position * 100) / count($columns), 1); } + + /** + * Helper method to duplicate all tasks to another project + * + * @access public + * @param integer $src_project_id + * @param integer $dst_project_id + * @return boolean + */ + public function duplicate($src_project_id, $dst_project_id) + { + $task_ids = $this->taskFinder->getAllIds($src_project_id, array(Task::STATUS_OPEN, Task::STATUS_CLOSED)); + + foreach ($task_ids as $task_id) { + if (! $this->taskDuplication->duplicateToProject($task_id, $dst_project_id)) { + return false; + } + } + + return true; + } } diff --git a/app/Model/TaskAnalytic.php b/app/Model/TaskAnalytic.php index bdfec3cb..cff56744 100644 --- a/app/Model/TaskAnalytic.php +++ b/app/Model/TaskAnalytic.php @@ -48,7 +48,7 @@ class TaskAnalytic extends Base public function getTimeSpentByColumn(array $task) { $result = array(); - $columns = $this->board->getColumnsList($task['project_id']); + $columns = $this->column->getList($task['project_id']); $sums = $this->transition->getTimeSpentByTask($task['id']); foreach ($columns as $column_id => $column_title) { diff --git a/app/Model/TaskCreation.php b/app/Model/TaskCreation.php index 975cc7a3..2d2e5504 100644 --- a/app/Model/TaskCreation.php +++ b/app/Model/TaskCreation.php @@ -49,13 +49,14 @@ class TaskCreation extends Base */ public function prepare(array &$values) { - $this->dateParser->convert($values, array('date_due')); - $this->dateParser->convert($values, array('date_started'), true); - $this->removeFields($values, array('another_task')); - $this->resetFields($values, array('date_started', 'creator_id', 'owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated')); + $values = $this->dateParser->convert($values, array('date_due')); + $values = $this->dateParser->convert($values, array('date_started'), true); + + $this->helper->model->removeFields($values, array('another_task')); + $this->helper->model->resetFields($values, array('date_started', 'creator_id', 'owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated')); if (empty($values['column_id'])) { - $values['column_id'] = $this->board->getFirstColumn($values['project_id']); + $values['column_id'] = $this->column->getFirstColumnId($values['project_id']); } if (empty($values['color_id'])) { diff --git a/app/Model/TaskDuplication.php b/app/Model/TaskDuplication.php index e81fb232..ebdd4d29 100644 --- a/app/Model/TaskDuplication.php +++ b/app/Model/TaskDuplication.php @@ -64,7 +64,7 @@ class TaskDuplication extends Base if ($values['recurrence_status'] == Task::RECURRING_STATUS_PENDING) { $values['recurrence_parent'] = $task_id; - $values['column_id'] = $this->board->getFirstColumn($values['project_id']); + $values['column_id'] = $this->column->getFirstColumnId($values['project_id']); $this->calculateRecurringTaskDueDate($values); $recurring_task_id = $this->save($task_id, $values); @@ -155,6 +155,7 @@ class TaskDuplication extends Base * * @access public * @param array $values + * @return array */ public function checkDestinationProjectValues(array &$values) { @@ -181,12 +182,12 @@ class TaskDuplication extends Base // Check if the column exists for the destination project if ($values['column_id'] > 0) { - $values['column_id'] = $this->board->getColumnIdByTitle( + $values['column_id'] = $this->column->getColumnIdByTitle( $values['project_id'], - $this->board->getColumnTitleById($values['column_id']) + $this->column->getColumnTitleById($values['column_id']) ); - $values['column_id'] = $values['column_id'] ?: $this->board->getFirstColumn($values['project_id']); + $values['column_id'] = $values['column_id'] ?: $this->column->getFirstColumnId($values['project_id']); } return $values; diff --git a/app/Model/TaskExport.php b/app/Model/TaskExport.php deleted file mode 100644 index 278c0897..00000000 --- a/app/Model/TaskExport.php +++ /dev/null @@ -1,145 +0,0 @@ -<?php - -namespace Kanboard\Model; - -use PDO; - -/** - * Task Export model - * - * @package model - * @author Frederic Guillot - */ -class TaskExport extends Base -{ - /** - * Fetch tasks and return the prepared CSV - * - * @access public - * @param integer $project_id Project id - * @param mixed $from Start date (timestamp or user formatted date) - * @param mixed $to End date (timestamp or user formatted date) - * @return array - */ - public function export($project_id, $from, $to) - { - $tasks = $this->getTasks($project_id, $from, $to); - $swimlanes = $this->swimlane->getList($project_id); - $results = array($this->getColumns()); - - foreach ($tasks as &$task) { - $results[] = array_values($this->format($task, $swimlanes)); - } - - return $results; - } - - /** - * Get the list of tasks for a given project and date range - * - * @access public - * @param integer $project_id Project id - * @param mixed $from Start date (timestamp or user formatted date) - * @param mixed $to End date (timestamp or user formatted date) - * @return array - */ - public function getTasks($project_id, $from, $to) - { - $sql = ' - SELECT - tasks.id, - projects.name AS project_name, - tasks.is_active, - project_has_categories.name AS category_name, - tasks.swimlane_id, - columns.title AS column_title, - tasks.position, - tasks.color_id, - tasks.date_due, - creators.username AS creator_username, - users.username AS assignee_username, - users.name AS assignee_name, - tasks.score, - tasks.title, - tasks.date_creation, - tasks.date_modification, - tasks.date_completed, - tasks.date_started, - tasks.time_estimated, - tasks.time_spent - FROM tasks - LEFT JOIN users ON users.id = tasks.owner_id - LEFT JOIN users AS creators ON creators.id = tasks.creator_id - LEFT JOIN project_has_categories ON project_has_categories.id = tasks.category_id - LEFT JOIN columns ON columns.id = tasks.column_id - LEFT JOIN projects ON projects.id = tasks.project_id - WHERE tasks.date_creation >= ? AND tasks.date_creation <= ? AND tasks.project_id = ? - ORDER BY tasks.id ASC - '; - - if (! is_numeric($from)) { - $from = $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($from)); - } - - if (! is_numeric($to)) { - $to = $this->dateParser->removeTimeFromTimestamp(strtotime('+1 day', $this->dateParser->getTimestamp($to))); - } - - $rq = $this->db->execute($sql, array($from, $to, $project_id)); - return $rq->fetchAll(PDO::FETCH_ASSOC); - } - - /** - * Format the output of a task array - * - * @access public - * @param array $task Task properties - * @param array $swimlanes List of swimlanes - * @return array - */ - public function format(array &$task, array &$swimlanes) - { - $colors = $this->color->getList(); - - $task['is_active'] = $task['is_active'] == Task::STATUS_OPEN ? e('Open') : e('Closed'); - $task['color_id'] = $colors[$task['color_id']]; - $task['score'] = $task['score'] ?: 0; - $task['swimlane_id'] = isset($swimlanes[$task['swimlane_id']]) ? $swimlanes[$task['swimlane_id']] : '?'; - - $this->dateParser->format($task, array('date_due', 'date_modification', 'date_creation', 'date_started', 'date_completed'), 'Y-m-d'); - - return $task; - } - - /** - * Get column titles - * - * @access public - * @return string[] - */ - public function getColumns() - { - return array( - e('Task Id'), - e('Project'), - e('Status'), - e('Category'), - e('Swimlane'), - e('Column'), - e('Position'), - e('Color'), - e('Due date'), - e('Creator'), - e('Assignee Username'), - e('Assignee Name'), - e('Complexity'), - e('Title'), - e('Creation date'), - e('Modification date'), - e('Completion date'), - e('Start date'), - e('Time estimated'), - e('Time spent'), - ); - } -} diff --git a/app/Model/TaskExternalLink.php b/app/Model/TaskExternalLink.php new file mode 100644 index 00000000..f2c756b4 --- /dev/null +++ b/app/Model/TaskExternalLink.php @@ -0,0 +1,99 @@ +<?php + +namespace Kanboard\Model; + +/** + * Task External Link Model + * + * @package model + * @author Frederic Guillot + */ +class TaskExternalLink extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'task_has_external_links'; + + /** + * Get all links + * + * @access public + * @param integer $task_id + * @return array + */ + public function getAll($task_id) + { + $types = $this->externalLinkManager->getTypes(); + + $links = $this->db->table(self::TABLE) + ->columns(self::TABLE.'.*', User::TABLE.'.name AS creator_name', User::TABLE.'.username AS creator_username') + ->eq('task_id', $task_id) + ->asc('title') + ->join(User::TABLE, 'id', 'creator_id') + ->findAll(); + + foreach ($links as &$link) { + $link['dependency_label'] = $this->externalLinkManager->getDependencyLabel($link['link_type'], $link['dependency']); + $link['type'] = isset($types[$link['link_type']]) ? $types[$link['link_type']] : t('Unknown'); + } + + return $links; + } + + /** + * Get link + * + * @access public + * @param integer $link_id + * @return array + */ + public function getById($link_id) + { + return $this->db->table(self::TABLE)->eq('id', $link_id)->findOne(); + } + + /** + * Add a new link in the database + * + * @access public + * @param array $values Form values + * @return boolean|integer + */ + public function create(array $values) + { + unset($values['id']); + $values['creator_id'] = $this->userSession->getId(); + $values['date_creation'] = time(); + $values['date_modification'] = $values['date_creation']; + + return $this->persist(self::TABLE, $values); + } + + /** + * Modify external link + * + * @access public + * @param array $values Form values + * @return boolean + */ + public function update(array $values) + { + $values['date_modification'] = time(); + return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); + } + + /** + * Remove a link + * + * @access public + * @param integer $link_id + * @return boolean + */ + public function remove($link_id) + { + return $this->db->table(self::TABLE)->eq('id', $link_id)->remove(); + } +} diff --git a/app/Model/TaskFile.php b/app/Model/TaskFile.php new file mode 100644 index 00000000..45a3b97f --- /dev/null +++ b/app/Model/TaskFile.php @@ -0,0 +1,54 @@ +<?php + +namespace Kanboard\Model; + +/** + * Task File Model + * + * @package model + * @author Frederic Guillot + */ +class TaskFile extends File +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'task_has_files'; + + /** + * SQL foreign key + * + * @var string + */ + const FOREIGN_KEY = 'task_id'; + + /** + * Path prefix + * + * @var string + */ + const PATH_PREFIX = 'tasks'; + + /** + * Events + * + * @var string + */ + const EVENT_CREATE = 'task.file.create'; + + /** + * Handle screenshot upload + * + * @access public + * @param integer $task_id Task id + * @param string $blob Base64 encoded image + * @return bool|integer + */ + public function uploadScreenshot($task_id, $blob) + { + $original_filename = e('Screenshot taken %s', $this->helper->dt->datetime(time())).'.png'; + return $this->uploadContent($task_id, $original_filename, $blob); + } +} diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php index 7ceb4a97..1883298d 100644 --- a/app/Model/TaskFilter.php +++ b/app/Model/TaskFilter.php @@ -469,7 +469,7 @@ class TaskFilter extends Base $this->query->beginOr(); foreach ($values as $project) { - $this->query->ilike(Board::TABLE.'.title', $project); + $this->query->ilike(Column::TABLE.'.title', $project); } $this->query->closeOr(); diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php index 1c83136b..0492a9bf 100644 --- a/app/Model/TaskFinder.php +++ b/app/Model/TaskFinder.php @@ -38,14 +38,14 @@ class TaskFinder extends Base Task::TABLE.'.time_spent', Task::TABLE.'.time_estimated', Project::TABLE.'.name AS project_name', - Board::TABLE.'.title AS column_name', + Column::TABLE.'.title AS column_name', User::TABLE.'.username AS assignee_username', User::TABLE.'.name AS assignee_name' ) ->eq(Task::TABLE.'.is_active', $is_active) ->in(Project::TABLE.'.id', $project_ids) ->join(Project::TABLE, 'id', 'project_id') - ->join(Board::TABLE, 'id', 'column_id', Task::TABLE) + ->join(Column::TABLE, 'id', 'column_id', Task::TABLE) ->join(User::TABLE, 'id', 'owner_id', Task::TABLE); } @@ -88,11 +88,12 @@ class TaskFinder extends Base return $this->db ->table(Task::TABLE) ->columns( - '(SELECT count(*) FROM '.Comment::TABLE.' WHERE task_id=tasks.id) AS nb_comments', - '(SELECT count(*) FROM '.File::TABLE.' WHERE task_id=tasks.id) AS nb_files', - '(SELECT count(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id) AS nb_subtasks', - '(SELECT count(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks', - '(SELECT count(*) FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id) AS nb_links', + '(SELECT COUNT(*) FROM '.Comment::TABLE.' WHERE task_id=tasks.id) AS nb_comments', + '(SELECT COUNT(*) FROM '.TaskFile::TABLE.' WHERE task_id=tasks.id) AS nb_files', + '(SELECT COUNT(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id) AS nb_subtasks', + '(SELECT COUNT(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks', + '(SELECT COUNT(*) FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id) AS nb_links', + '(SELECT COUNT(*) FROM '.TaskExternalLink::TABLE.' WHERE '.TaskExternalLink::TABLE.'.task_id = tasks.id) AS nb_external_links', '(SELECT DISTINCT 1 FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id AND '.TaskLink::TABLE.'.link_id = 9) AS is_milestone', 'tasks.id', 'tasks.reference', @@ -128,15 +129,15 @@ class TaskFinder extends Base User::TABLE.'.name AS assignee_name', Category::TABLE.'.name AS category_name', Category::TABLE.'.description AS category_description', - Board::TABLE.'.title AS column_name', - Board::TABLE.'.position AS column_position', + Column::TABLE.'.title AS column_name', + Column::TABLE.'.position AS column_position', Swimlane::TABLE.'.name AS swimlane_name', Project::TABLE.'.default_swimlane', Project::TABLE.'.name AS project_name' ) ->join(User::TABLE, 'id', 'owner_id', Task::TABLE) ->join(Category::TABLE, 'id', 'category_id', Task::TABLE) - ->join(Board::TABLE, 'id', 'column_id', Task::TABLE) + ->join(Column::TABLE, 'id', 'column_id', Task::TABLE) ->join(Swimlane::TABLE, 'id', 'swimlane_id', Task::TABLE) ->join(Project::TABLE, 'id', 'project_id', Task::TABLE); } @@ -179,6 +180,23 @@ class TaskFinder extends Base } /** + * Get all tasks for a given project and status + * + * @access public + * @param integer $project_id + * @param array $status + * @return array + */ + public function getAllIds($project_id, array $status = array(Task::STATUS_OPEN)) + { + return $this->db + ->table(Task::TABLE) + ->eq(Task::TABLE.'.project_id', $project_id) + ->in(Task::TABLE.'.is_active', $status) + ->findAllByColumn('id'); + } + + /** * Get overdue tasks query * * @access public diff --git a/app/Model/TaskImport.php b/app/Model/TaskImport.php deleted file mode 100644 index e8dd1946..00000000 --- a/app/Model/TaskImport.php +++ /dev/null @@ -1,156 +0,0 @@ -<?php - -namespace Kanboard\Model; - -use Kanboard\Core\Csv; -use SimpleValidator\Validator; -use SimpleValidator\Validators; - -/** - * Task Import - * - * @package model - * @author Frederic Guillot - */ -class TaskImport extends Base -{ - /** - * Number of successful import - * - * @access public - * @var integer - */ - public $counter = 0; - - /** - * Project id to import tasks - * - * @access public - * @var integer - */ - public $projectId; - - /** - * Get mapping between CSV header and SQL columns - * - * @access public - * @return array - */ - public function getColumnMapping() - { - return array( - 'reference' => 'Reference', - 'title' => 'Title', - 'description' => 'Description', - 'assignee' => 'Assignee Username', - 'creator' => 'Creator Username', - 'color' => 'Color Name', - 'column' => 'Column Name', - 'category' => 'Category Name', - 'swimlane' => 'Swimlane Name', - 'score' => 'Complexity', - 'time_estimated' => 'Time Estimated', - 'time_spent' => 'Time Spent', - 'date_due' => 'Due Date', - 'is_active' => 'Closed', - ); - } - - /** - * Import a single row - * - * @access public - * @param array $row - * @param integer $line_number - */ - public function import(array $row, $line_number) - { - $row = $this->prepare($row); - - if ($this->validateCreation($row)) { - if ($this->taskCreation->create($row) > 0) { - $this->logger->debug('TaskImport: imported successfully line '.$line_number); - $this->counter++; - } else { - $this->logger->error('TaskImport: creation error at line '.$line_number); - } - } else { - $this->logger->error('TaskImport: validation error at line '.$line_number); - } - } - - /** - * Format row before validation - * - * @access public - * @param array $row - * @return array - */ - public function prepare(array $row) - { - $values = array(); - $values['project_id'] = $this->projectId; - $values['reference'] = $row['reference']; - $values['title'] = $row['title']; - $values['description'] = $row['description']; - $values['is_active'] = Csv::getBooleanValue($row['is_active']) == 1 ? 0 : 1; - $values['score'] = (int) $row['score']; - $values['time_estimated'] = (float) $row['time_estimated']; - $values['time_spent'] = (float) $row['time_spent']; - - if (! empty($row['assignee'])) { - $values['owner_id'] = $this->user->getIdByUsername($row['assignee']); - } - - if (! empty($row['creator'])) { - $values['creator_id'] = $this->user->getIdByUsername($row['creator']); - } - - if (! empty($row['color'])) { - $values['color_id'] = $this->color->find($row['color']); - } - - if (! empty($row['column'])) { - $values['column_id'] = $this->board->getColumnIdByTitle($this->projectId, $row['column']); - } - - if (! empty($row['category'])) { - $values['category_id'] = $this->category->getIdByName($this->projectId, $row['category']); - } - - if (! empty($row['swimlane'])) { - $values['swimlane_id'] = $this->swimlane->getIdByName($this->projectId, $row['swimlane']); - } - - if (! empty($row['date_due'])) { - $values['date_due'] = $this->dateParser->getTimestampFromIsoFormat($row['date_due']); - } - - $this->removeEmptyFields( - $values, - array('owner_id', 'creator_id', 'color_id', 'column_id', 'category_id', 'swimlane_id', 'date_due') - ); - - return $values; - } - - /** - * Validate user creation - * - * @access public - * @param array $values - * @return boolean - */ - public function validateCreation(array $values) - { - $v = new Validator($values, array( - new Validators\Integer('project_id', t('This value must be an integer')), - new Validators\Required('project_id', t('The project is required')), - new Validators\Required('title', t('The title is required')), - new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200), - new Validators\MaxLength('reference', t('The maximum length is %d characters', 50), 50), - )); - - return $v->execute(); - } -} diff --git a/app/Model/TaskLink.php b/app/Model/TaskLink.php index 87aae55e..e46ea476 100644 --- a/app/Model/TaskLink.php +++ b/app/Model/TaskLink.php @@ -75,22 +75,24 @@ class TaskLink extends Base Task::TABLE.'.title', Task::TABLE.'.is_active', Task::TABLE.'.project_id', + Task::TABLE.'.column_id', + Task::TABLE.'.color_id', Task::TABLE.'.time_spent AS task_time_spent', Task::TABLE.'.time_estimated AS task_time_estimated', Task::TABLE.'.owner_id AS task_assignee_id', User::TABLE.'.username AS task_assignee_username', User::TABLE.'.name AS task_assignee_name', - Board::TABLE.'.title AS column_title', + Column::TABLE.'.title AS column_title', Project::TABLE.'.name AS project_name' ) ->eq(self::TABLE.'.task_id', $task_id) ->join(Link::TABLE, 'id', 'link_id') ->join(Task::TABLE, 'id', 'opposite_task_id') - ->join(Board::TABLE, 'id', 'column_id', Task::TABLE) + ->join(Column::TABLE, 'id', 'column_id', Task::TABLE) ->join(User::TABLE, 'id', 'owner_id', Task::TABLE) ->join(Project::TABLE, 'id', 'project_id', Task::TABLE) ->asc(Link::TABLE.'.id') - ->desc(Board::TABLE.'.position') + ->desc(Column::TABLE.'.position') ->desc(Task::TABLE.'.is_active') ->asc(Task::TABLE.'.position') ->asc(Task::TABLE.'.id') diff --git a/app/Model/TaskModification.php b/app/Model/TaskModification.php index eee7b2e0..a77b78a4 100644 --- a/app/Model/TaskModification.php +++ b/app/Model/TaskModification.php @@ -84,11 +84,12 @@ class TaskModification extends Base */ public function prepare(array &$values) { - $this->dateParser->convert($values, array('date_due')); - $this->dateParser->convert($values, array('date_started'), true); - $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('priority', 'is_active', 'recurrence_status', 'recurrence_trigger', 'recurrence_factor', 'recurrence_timeframe', 'recurrence_basedate')); + $values = $this->dateParser->convert($values, array('date_due')); + $values = $this->dateParser->convert($values, array('date_started'), true); + + $this->helper->model->removeFields($values, array('another_task', 'id')); + $this->helper->model->resetFields($values, array('date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent')); + $this->helper->model->convertIntegerFields($values, array('priority', 'is_active', 'recurrence_status', 'recurrence_trigger', 'recurrence_factor', 'recurrence_timeframe', 'recurrence_basedate')); $values['date_modification'] = time(); } diff --git a/app/Model/TaskPermission.php b/app/Model/TaskPermission.php index fac2153e..b1e02589 100644 --- a/app/Model/TaskPermission.php +++ b/app/Model/TaskPermission.php @@ -18,7 +18,8 @@ class TaskPermission extends Base * Regular users can't remove tasks from other people * * @public - * @return boolean + * @param array $task + * @return bool */ public function canRemoveTask(array $task) { diff --git a/app/Model/Transition.php b/app/Model/Transition.php index b1f8f678..870d95fd 100644 --- a/app/Model/Transition.php +++ b/app/Model/Transition.php @@ -3,7 +3,7 @@ namespace Kanboard\Model; /** - * Transition model + * Transition * * @package model * @author Frederic Guillot @@ -21,20 +21,22 @@ class Transition extends Base * Save transition event * * @access public - * @param integer $user_id - * @param array $task - * @return boolean + * @param integer $user_id + * @param array $task_event + * @return bool */ - public function save($user_id, array $task) + public function save($user_id, array $task_event) { + $time = time(); + return $this->db->table(self::TABLE)->insert(array( 'user_id' => $user_id, - 'project_id' => $task['project_id'], - 'task_id' => $task['task_id'], - 'src_column_id' => $task['src_column_id'], - 'dst_column_id' => $task['dst_column_id'], - 'date' => time(), - 'time_spent' => time() - $task['date_moved'] + 'project_id' => $task_event['project_id'], + 'task_id' => $task_event['task_id'], + 'src_column_id' => $task_event['src_column_id'], + 'dst_column_id' => $task_event['dst_column_id'], + 'date' => $time, + 'time_spent' => $time - $task_event['date_moved'] )); } @@ -76,8 +78,8 @@ class Transition extends Base ->eq('task_id', $task_id) ->desc('date') ->join(User::TABLE, 'id', 'user_id') - ->join(Board::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') - ->join(Board::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') + ->join(Column::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') + ->join(Column::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') ->findAll(); } @@ -116,71 +118,11 @@ class Transition extends Base ->lte('date', $to) ->eq(self::TABLE.'.project_id', $project_id) ->desc('date') + ->desc(self::TABLE.'.id') ->join(Task::TABLE, 'id', 'task_id') ->join(User::TABLE, 'id', 'user_id') - ->join(Board::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') - ->join(Board::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') + ->join(Column::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') + ->join(Column::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') ->findAll(); } - - /** - * Get project export - * - * @access public - * @param integer $project_id Project id - * @param mixed $from Start date (timestamp or user formatted date) - * @param mixed $to End date (timestamp or user formatted date) - * @return array - */ - public function export($project_id, $from, $to) - { - $results = array($this->getColumns()); - $transitions = $this->getAllByProjectAndDate($project_id, $from, $to); - - foreach ($transitions as $transition) { - $results[] = $this->format($transition); - } - - return $results; - } - - /** - * Get column titles - * - * @access public - * @return string[] - */ - public function getColumns() - { - return array( - e('Id'), - e('Task Title'), - e('Source column'), - e('Destination column'), - e('Executer'), - e('Date'), - e('Time spent'), - ); - } - - /** - * Format the output of a transition array - * - * @access public - * @param array $transition - * @return array - */ - public function format(array $transition) - { - $values = array(); - $values[] = $transition['id']; - $values[] = $transition['title']; - $values[] = $transition['src_column']; - $values[] = $transition['dst_column']; - $values[] = $transition['name'] ?: $transition['username']; - $values[] = date('Y-m-d H:i', $transition['date']); - $values[] = round($transition['time_spent'] / 3600, 2); - - return $values; - } } diff --git a/app/Model/User.php b/app/Model/User.php index ac0e7491..0e11422b 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -41,6 +41,18 @@ class User extends Base } /** + * Return true if the user is active + * + * @access public + * @param integer $user_id User id + * @return boolean + */ + public function isActive($user_id) + { + return $this->db->table(self::TABLE)->eq('id', $user_id)->eq('is_active', 1)->exists(); + } + + /** * Get query to fetch all users * * @access public @@ -48,20 +60,7 @@ class User extends Base */ public function getQuery() { - return $this->db - ->table(self::TABLE) - ->columns( - 'id', - 'username', - 'name', - 'email', - 'role', - 'is_ldap_user', - 'notifications_enabled', - 'google_id', - 'github_id', - 'twofactor_activated' - ); + return $this->db->table(self::TABLE); } /** @@ -138,7 +137,7 @@ class User extends Base * * @access public * @param string $username Username - * @return array + * @return integer */ public function getIdByUsername($username) { @@ -206,9 +205,9 @@ class User extends Base * @param boolean $prepend Prepend "All users" * @return array */ - public function getList($prepend = false) + public function getActiveUsersList($prepend = false) { - $users = $this->db->table(self::TABLE)->columns('id', 'username', 'name')->findAll(); + $users = $this->db->table(self::TABLE)->eq('is_active', 1)->columns('id', 'username', 'name')->findAll(); $listing = $this->prepareList($users); if ($prepend) { @@ -254,10 +253,10 @@ class User extends Base } } - $this->removeFields($values, array('confirmation', 'current_password')); - $this->resetFields($values, array('is_ldap_user', 'disable_login_form')); - $this->convertNullFields($values, array('gitlab_id')); - $this->convertIntegerFields($values, array('gitlab_id')); + $this->helper->model->removeFields($values, array('confirmation', 'current_password')); + $this->helper->model->resetFields($values, array('is_ldap_user', 'disable_login_form')); + $this->helper->model->convertNullFields($values, array('gitlab_id')); + $this->helper->model->convertIntegerFields($values, array('gitlab_id')); } /** @@ -278,7 +277,7 @@ class User extends Base * * @access public * @param array $values Form values - * @return array + * @return boolean */ public function update(array $values) { @@ -294,6 +293,30 @@ class User extends Base } /** + * Disable a specific user + * + * @access public + * @param integer $user_id + * @return boolean + */ + public function disable($user_id) + { + return $this->db->table(self::TABLE)->eq('id', $user_id)->update(array('is_active' => 0)); + } + + /** + * Enable a specific user + * + * @access public + * @param integer $user_id + * @return boolean + */ + public function enable($user_id) + { + return $this->db->table(self::TABLE)->eq('id', $user_id)->update(array('is_active' => 1)); + } + + /** * Remove a specific user * * @access public diff --git a/app/Model/UserImport.php b/app/Model/UserImport.php deleted file mode 100644 index 0ec4e802..00000000 --- a/app/Model/UserImport.php +++ /dev/null @@ -1,118 +0,0 @@ -<?php - -namespace Kanboard\Model; - -use SimpleValidator\Validator; -use SimpleValidator\Validators; -use Kanboard\Core\Security\Role; -use Kanboard\Core\Csv; - -/** - * User Import - * - * @package model - * @author Frederic Guillot - */ -class UserImport extends Base -{ - /** - * Number of successful import - * - * @access public - * @var integer - */ - public $counter = 0; - - /** - * Get mapping between CSV header and SQL columns - * - * @access public - * @return array - */ - public function getColumnMapping() - { - return array( - 'username' => 'Username', - 'password' => 'Password', - 'email' => 'Email', - 'name' => 'Full Name', - 'is_admin' => 'Administrator', - 'is_manager' => 'Manager', - 'is_ldap_user' => 'Remote User', - ); - } - - /** - * Import a single row - * - * @access public - * @param array $row - * @param integer $line_number - */ - public function import(array $row, $line_number) - { - $row = $this->prepare($row); - - if ($this->validateCreation($row)) { - if ($this->user->create($row)) { - $this->logger->debug('UserImport: imported successfully line '.$line_number); - $this->counter++; - } else { - $this->logger->error('UserImport: creation error at line '.$line_number); - } - } else { - $this->logger->error('UserImport: validation error at line '.$line_number); - } - } - - /** - * Format row before validation - * - * @access public - * @param array $row - * @return array - */ - public function prepare(array $row) - { - $row['username'] = strtolower($row['username']); - - foreach (array('is_admin', 'is_manager', 'is_ldap_user') as $field) { - $row[$field] = Csv::getBooleanValue($row[$field]); - } - - if ($row['is_admin'] == 1) { - $row['role'] = Role::APP_ADMIN; - } elseif ($row['is_manager'] == 1) { - $row['role'] = Role::APP_MANAGER; - } else { - $row['role'] = Role::APP_USER; - } - - unset($row['is_admin']); - unset($row['is_manager']); - - $this->removeEmptyFields($row, array('password', 'email', 'name')); - - return $row; - } - - /** - * Validate user creation - * - * @access public - * @param array $values - * @return boolean - */ - public function validateCreation(array $values) - { - $v = new Validator($values, array( - new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), - new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), User::TABLE, 'id'), - new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6), - new Validators\Email('email', t('Email address invalid')), - new Validators\Integer('is_ldap_user', t('This value must be an integer')), - )); - - return $v->execute(); - } -} |