看板初始化提交

This commit is contained in:
zephyr
2026-06-01 21:23:12 -07:00
commit 27411ebedc
1827 changed files with 192340 additions and 0 deletions
+202
View File
@@ -0,0 +1,202 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Action Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ActionModel extends Base
{
/**
* SQL table name for actions
*
* @var string
*/
const TABLE = 'actions';
/**
* Return actions and parameters for a given user
*
* @access public
* @param integer $user_id
* @return array
*/
public function getAllByUser($user_id)
{
$project_ids = $this->projectPermissionModel->getActiveProjectIds($user_id);
$actions = array();
if (! empty($project_ids)) {
$actions = $this->db->table(self::TABLE)->in('project_id', $project_ids)->findAll();
$params = $this->actionParameterModel->getAllByActions(array_column($actions, 'id'));
$this->attachParamsToActions($actions, $params);
}
return $actions;
}
/**
* Return actions and parameters for a given project
*
* @access public
* @param integer $project_id
* @return array
*/
public function getAllByProject($project_id)
{
$actions = $this->db->table(self::TABLE)->eq('project_id', $project_id)->findAll();
$params = $this->actionParameterModel->getAllByActions(array_column($actions, 'id'));
return $this->attachParamsToActions($actions, $params);
}
/**
* Return all actions and parameters
*
* @access public
* @return array
*/
public function getAll()
{
$actions = $this->db->table(self::TABLE)->findAll();
$params = $this->actionParameterModel->getAll();
return $this->attachParamsToActions($actions, $params);
}
/**
* Fetch an action
*
* @access public
* @param integer $action_id
* @return array
*/
public function getById($action_id)
{
$action = $this->db->table(self::TABLE)->eq('id', $action_id)->findOne();
if (! empty($action)) {
$action['params'] = $this->actionParameterModel->getAllByAction($action_id);
}
return $action;
}
/**
* Get the projectId by the actionId
*
* @access public
* @param integer $action_id
* @return integer
*/
public function getProjectId($action_id)
{
return $this->db->table(self::TABLE)->eq('id', $action_id)->findOneColumn('project_id') ?: 0;
}
/**
* Attach parameters to actions
*
* @access private
* @param array &$actions
* @param array &$params
* @return array
*/
private function attachParamsToActions(array &$actions, array &$params)
{
foreach ($actions as &$action) {
$action['params'] = isset($params[$action['id']]) ? $params[$action['id']] : array();
}
return $actions;
}
/**
* Remove an action
*
* @access public
* @param integer $action_id
* @return bool
*/
public function remove($action_id)
{
return $this->db->table(self::TABLE)->eq('id', $action_id)->remove();
}
/**
* Create an action
*
* @access public
* @param array $values Required parameters to save an action
* @return boolean|integer
*/
public function create(array $values)
{
$this->db->startTransaction();
$action = array(
'project_id' => $values['project_id'],
'event_name' => $values['event_name'],
'action_name' => $values['action_name'],
);
if (! $this->db->table(self::TABLE)->insert($action)) {
$this->db->cancelTransaction();
return false;
}
$action_id = $this->db->getLastId();
if (! $this->actionParameterModel->create($action_id, $values)) {
$this->db->cancelTransaction();
return false;
}
$this->db->closeTransaction();
return $action_id;
}
/**
* Copy actions from a project to another one (skip actions that cannot resolve parameters)
*
* @author Antonio Rabelo
* @param integer $src_project_id Source project id
* @param integer $dst_project_id Destination project id
* @return boolean
*/
public function duplicate($src_project_id, $dst_project_id)
{
$actions = $this->actionModel->getAllByProject($src_project_id);
foreach ($actions as $action) {
$this->db->startTransaction();
$values = array(
'project_id' => $dst_project_id,
'event_name' => $action['event_name'],
'action_name' => $action['action_name'],
);
if (! $this->db->table(self::TABLE)->insert($values)) {
$this->db->cancelTransaction();
continue;
}
$action_id = $this->db->getLastId();
if (! $this->actionParameterModel->duplicateParameters($dst_project_id, $action_id, $action['params'])) {
$this->logger->error('Action::duplicate => skip action '.$action['action_name'].' '.$action['id']);
$this->db->cancelTransaction();
continue;
}
$this->db->closeTransaction();
}
return true;
}
}
+173
View File
@@ -0,0 +1,173 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Action Parameter Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ActionParameterModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'action_has_params';
/**
* Get all action params
*
* @access public
* @return array
*/
public function getAll()
{
$params = $this->db->table(self::TABLE)->findAll();
return $this->toDictionary($params);
}
/**
* Get all params for a list of actions
*
* @access public
* @param array $action_ids
* @return array
*/
public function getAllByActions(array $action_ids)
{
$params = $this->db->table(self::TABLE)->in('action_id', $action_ids)->findAll();
return $this->toDictionary($params);
}
/**
* Build params dictionary
*
* @access private
* @param array $params
* @return array
*/
private function toDictionary(array $params)
{
$result = array();
foreach ($params as $param) {
$result[$param['action_id']][$param['name']] = $param['value'];
}
return $result;
}
/**
* Get all action params for a given action
*
* @access public
* @param integer $action_id
* @return array
*/
public function getAllByAction($action_id)
{
return $this->db->hashtable(self::TABLE)->eq('action_id', $action_id)->getAll('name', 'value');
}
/**
* Insert new parameters for an action
*
* @access public
* @param integer $action_id
* @param array $values
* @return boolean
*/
public function create($action_id, array $values)
{
foreach ($values['params'] as $name => $value) {
$param = array(
'action_id' => $action_id,
'name' => $name,
'value' => $value,
);
if (! $this->db->table(self::TABLE)->save($param)) {
return false;
}
}
return true;
}
/**
* Duplicate action parameters
*
* @access public
* @param integer $project_id
* @param integer $action_id
* @param array $params
* @return boolean
*/
public function duplicateParameters($project_id, $action_id, array $params)
{
foreach ($params as $name => $value) {
$value = $this->resolveParameter($project_id, $name, $value);
if ($value === false) {
$this->logger->error('ActionParameter::duplicateParameters => unable to resolve '.$name.'='.$value);
return false;
}
$values = array(
'action_id' => $action_id,
'name' => $name,
'value' => $value,
);
if (! $this->db->table(self::TABLE)->insert($values)) {
return false;
}
}
return true;
}
/**
* Resolve action parameter values according to another project
*
* @access private
* @param integer $project_id
* @param string $name
* @param string $value
* @return mixed
*/
private function resolveParameter($project_id, $name, $value)
{
switch ($name) {
case 'project_id':
return $value != $project_id ? $value : false;
case 'category_id':
if ($value == 0) {
return 0;
}
return $this->categoryModel->getIdByName($project_id, $this->categoryModel->getNameById($value)) ?: false;
case 'src_column_id':
case 'dest_column_id':
case 'dst_column_id':
case 'column_id':
$column = $this->columnModel->getById($value);
return empty($column) ? false : ($this->columnModel->getColumnIdByTitle($project_id, $column['title']) ?: false);
case 'user_id':
case 'owner_id':
if ($value == 0) {
return 0;
}
return $this->projectPermissionModel->isAssignable($project_id, $value) ? $value : false;
case 'swimlane_id':
$column = $this->swimlaneModel->getById($value);
return empty($column) ? false : ($this->swimlaneModel->getIdByName($project_id, $column['name']) ?: false);
default:
return $value;
}
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php
namespace Kanboard\Model;
use Exception;
use Kanboard\Core\Base;
/**
* Avatar File
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class AvatarFileModel extends Base
{
/**
* Path prefix
*
* @var string
*/
const PATH_PREFIX = 'avatars';
/**
* Get image filename
*
* @access public
* @param integer $user_id
* @return string
*/
public function getFilename($user_id)
{
return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->findOneColumn('avatar_path');
}
/**
* Add avatar in the user profile
*
* @access public
* @param integer $user_id Foreign key
* @param string $path Path on the disk
* @return bool
*/
public function create($user_id, $path)
{
$result = $this->db->table(UserModel::TABLE)->eq('id', $user_id)->update(array(
'avatar_path' => $path,
));
$this->userSession->refresh($user_id);
return $result;
}
/**
* Remove avatar from the user profile
*
* @access public
* @param integer $user_id Foreign key
* @return bool
*/
public function remove($user_id)
{
try {
$filename = $this->getFilename($user_id);
if (! empty($filename)) {
$this->objectStorage->remove($filename);
return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->update(array('avatar_path' => ''));
}
} catch (Exception $e) {
$this->logger->error($e->getMessage());
return false;
}
return true;
}
/**
* Upload avatar image file
*
* @access public
* @param integer $user_id
* @param array $file
* @return boolean
*/
public function uploadImageFile($user_id, array $file)
{
try {
if ($file['error'] == UPLOAD_ERR_OK && $file['size'] > 0) {
$destinationFilename = $this->generatePath($user_id, $file['name']);
$this->objectStorage->moveUploadedFile($file['tmp_name'], $destinationFilename);
$this->create($user_id, $destinationFilename);
} else {
throw new Exception('File not uploaded: '.var_export($file['error'], true));
}
} catch (Exception $e) {
$this->logger->error($e->getMessage());
return false;
}
return true;
}
/**
* Upload avatar image content
*
* @access public
* @param integer $user_id
* @param string $blob
* @return boolean
*/
public function uploadImageContent($user_id, &$blob)
{
try {
$destinationFilename = $this->generatePath($user_id, 'imageContent');
$this->objectStorage->put($destinationFilename, $blob);
$this->create($user_id, $destinationFilename);
} catch (Exception $e) {
$this->logger->error($e->getMessage());
return false;
}
return true;
}
/**
* Generate the path for a new filename
*
* @access public
* @param integer $user_id
* @param string $filename
* @return string
*/
public function generatePath($user_id, $filename)
{
return implode(DIRECTORY_SEPARATOR, array(self::PATH_PREFIX, $user_id, hash('sha1', $filename.time())));
}
/**
* Check if a filename is an image (file types that can be shown as avatar)
*
* @access public
* @param string $filename Filename
* @return bool
*/
public function isAvatarImage($filename)
{
switch (get_file_extension($filename)) {
case 'jpeg':
case 'jpg':
case 'png':
case 'gif':
return true;
}
return false;
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Board model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class BoardModel extends Base
{
/**
* Get Kanboard default columns
*
* @access public
* @return string[]
*/
public function getDefaultColumns()
{
return array(t('Backlog'), t('Ready'), t('Work in progress'), t('Done'));
}
/**
* Get user default columns
*
* @access public
* @return array
*/
public function getUserColumns()
{
$column_names = array_unique(explode_csv_field($this->configModel->get('board_columns', implode(',', $this->getDefaultColumns()))));
$columns = array();
foreach ($column_names as $column_name) {
$columns[] = array(
'title' => $column_name,
'task_limit' => 0,
'description' => '',
'hide_in_dashboard' => 0,
);
}
return $columns;
}
/**
* Create a board with default columns, must be executed inside a transaction
*
* @access public
* @param integer $project_id Project id
* @param array $columns Column parameters [ 'title' => 'boo', 'task_limit' => 2 ... ]
* @return boolean
*/
public function create($project_id, array $columns)
{
$position = 0;
foreach ($columns as $column) {
$values = array(
'title' => $column['title'],
'position' => ++$position,
'project_id' => $project_id,
'task_limit' => $column['task_limit'],
'description' => $column['description'],
'hide_in_dashboard' => $column['hide_in_dashboard'] ?: 0, // Avoid SQL error with Postgres
);
if (! $this->db->table(ColumnModel::TABLE)->save($values)) {
return false;
}
}
return true;
}
/**
* Copy board columns from a project to another one
*
* @author Antonio Rabelo
* @param integer $project_from Project Template
* @param integer $project_to Project that receives the copy
* @return boolean
*/
public function duplicate($project_from, $project_to)
{
$columns = $this->db->table(ColumnModel::TABLE)
->columns('title', 'task_limit', 'description', 'hide_in_dashboard')
->eq('project_id', $project_from)
->asc('position')
->findAll();
return $this->boardModel->create($project_to, $columns);
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Captcha model
*
* @package Kanboard\Model
*
* {"<IP_ADDRESS>": {"failed_login": <COUNT>, "expiration_date": <TIMESTAMP>}}
*/
class CaptchaModel extends Base
{
public function incrementFailedLogin($ipAddress)
{
$data = $this->getCaptchaData();
if (!isset($data[$ipAddress])) {
$data[$ipAddress] = ['failed_login' => 0, 'expiration_date' => 0];
}
$data[$ipAddress]['failed_login']++;
if ($data[$ipAddress]['failed_login'] >= BRUTEFORCE_CAPTCHA) {
$data[$ipAddress]['lock_expiration_date'] = time() + BRUTEFORCE_LOCKDOWN_DURATION;
}
$this->setCaptchaData($data);
}
public function resetFailedLogin($ipAddress)
{
$data = $this->getCaptchaData();
if (isset($data[$ipAddress])) {
unset($data[$ipAddress]);
$this->setCaptchaData($data);
}
}
public function isLocked($ipAddress)
{
$data = $this->getCaptchaData();
if (isset($data[$ipAddress]) && isset($data[$ipAddress]['lock_expiration_date'])) {
return $data[$ipAddress]['lock_expiration_date'] > time();
}
return false;
}
protected function getCaptchaData()
{
$rawData = $this->configModel->getOption('captcha_data', '{}');
$data = json_decode($rawData, true);
if (!is_array($data)) {
$data = [];
}
return $data;
}
protected function setCaptchaData(array $data)
{
$this->configModel->save(['captcha_data' => json_encode($data)]);
}
}
+228
View File
@@ -0,0 +1,228 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Category model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class CategoryModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'project_has_categories';
/**
* Return true if a category exists for a given project
*
* @access public
* @param integer $category_id Category id
* @return boolean
*/
public function exists($category_id)
{
return $this->db->table(self::TABLE)->eq('id', $category_id)->exists();
}
/**
* Get a category by the id
*
* @access public
* @param integer $category_id Category id
* @return array
*/
public function getById($category_id)
{
return $this->db->table(self::TABLE)->eq('id', $category_id)->findOne();
}
/**
* Get the category name by the id
*
* @access public
* @param integer $category_id Category id
* @return string
*/
public function getNameById($category_id)
{
return $this->db->table(self::TABLE)->eq('id', $category_id)->findOneColumn('name') ?: '';
}
/**
* Get the projectId by the category id
*
* @access public
* @param integer $category_id Category id
* @return integer
*/
public function getProjectId($category_id)
{
return $this->db->table(self::TABLE)->eq('id', $category_id)->findOneColumn('project_id') ?: 0;
}
/**
* Get a category id by the category name and project id
*
* @access public
* @param integer $project_id Project id
* @param string $category_name Category name
* @return integer
*/
public function getIdByName($project_id, $category_name)
{
return (int) $this->db->table(self::TABLE)
->eq('project_id', $project_id)
->eq('name', $category_name)
->findOneColumn('id');
}
/**
* Return the list of all categories
*
* @access public
* @param integer $project_id Project id
* @param bool $prepend_none If true, prepend to the list the value 'None'
* @param bool $prepend_all If true, prepend to the list the value 'All'
* @return array
*/
public function getList($project_id, $prepend_none = true, $prepend_all = false)
{
$listing = $this->db->hashtable(self::TABLE)
->eq('project_id', $project_id)
->asc('name')
->getAll('id', 'name');
$prepend = array();
if ($prepend_all) {
$prepend[-1] = t('All categories');
}
if ($prepend_none) {
$prepend[0] = t('No category');
}
return $prepend + $listing;
}
/**
* Return all categories 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('name')
->findAll();
}
/**
* 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 = array_unique(explode_csv_field($this->configModel->get('project_categories')));
foreach ($categories as $category) {
$results[] = $this->db->table(self::TABLE)->insert(array(
'project_id' => $project_id,
'name' => $category,
));
}
return in_array(false, $results, true);
}
/**
* Create a category (run inside a transaction)
*
* @access public
* @param array $values Form values
* @return bool|integer
*/
public function create(array $values)
{
return $this->db->table(self::TABLE)->persist($values);
}
/**
* Update a category
*
* @access public
* @param array $values Form values
* @return bool
*/
public function update(array $values)
{
$updates = $values;
unset($updates['id']);
return $this->db->table(self::TABLE)->eq('id', $values['id'])->save($updates);
}
/**
* Remove a category
*
* @access public
* @param integer $category_id Category id
* @return bool
*/
public function remove($category_id)
{
$this->db->startTransaction();
$this->db->table(TaskModel::TABLE)->eq('category_id', $category_id)->update(array('category_id' => 0));
if (! $this->db->table(self::TABLE)->eq('id', $category_id)->remove()) {
$this->db->cancelTransaction();
return false;
}
$this->db->closeTransaction();
return true;
}
/**
* Duplicate categories from a project to another one, must be executed inside a transaction
*
* @author Antonio Rabelo
* @param integer $src_project_id Source 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', 'description', 'color_id')
->eq('project_id', $src_project_id)
->asc('name')
->findAll();
foreach ($categories as $category) {
$category['project_id'] = $dst_project_id;
if (! $this->db->table(self::TABLE)->save($category)) {
return false;
}
}
return true;
}
}
+231
View File
@@ -0,0 +1,231 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Color model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ColorModel extends Base
{
/**
* Default colors
*
* @access protected
* @var array
*/
protected $default_colors = array(
'yellow' => array(
'name' => 'Yellow',
'background' => 'rgb(245, 247, 196)',
'border' => 'rgb(223, 227, 45)',
),
'blue' => array(
'name' => 'Blue',
'background' => 'rgb(219, 235, 255)',
'border' => 'rgb(168, 207, 255)',
),
'green' => array(
'name' => 'Green',
'background' => 'rgb(189, 244, 203)',
'border' => 'rgb(74, 227, 113)',
),
'purple' => array(
'name' => 'Purple',
'background' => 'rgb(223, 176, 255)',
'border' => 'rgb(205, 133, 254)',
),
'red' => array(
'name' => 'Red',
'background' => 'rgb(255, 187, 187)',
'border' => 'rgb(255, 151, 151)',
),
'orange' => array(
'name' => 'Orange',
'background' => 'rgb(255, 215, 179)',
'border' => 'rgb(255, 172, 98)',
),
'grey' => array(
'name' => 'Grey',
'background' => 'rgb(238, 238, 238)',
'border' => 'rgb(204, 204, 204)',
),
'brown' => array(
'name' => 'Brown',
'background' => '#d7ccc8',
'border' => '#4e342e',
),
'deep_orange' => array(
'name' => 'Deep Orange',
'background' => '#ffab91',
'border' => '#e64a19',
),
'dark_grey' => array(
'name' => 'Dark Grey',
'background' => '#cfd8dc',
'border' => '#455a64',
),
'pink' => array(
'name' => 'Pink',
'background' => '#f48fb1',
'border' => '#d81b60',
),
'teal' => array(
'name' => 'Teal',
'background' => '#80cbc4',
'border' => '#00695c',
),
'cyan' => array(
'name' => 'Cyan',
'background' => '#b2ebf2',
'border' => '#00bcd4',
),
'lime' => array(
'name' => 'Lime',
'background' => '#e6ee9c',
'border' => '#afb42b',
),
'light_green' => array(
'name' => 'Light Green',
'background' => '#dcedc8',
'border' => '#689f38',
),
'amber' => array(
'name' => 'Amber',
'background' => '#ffe082',
'border' => '#ffa000',
),
);
/**
* Find a color id from the name or the id
*
* @access public
* @param string $color
* @return string
*/
public function find($color)
{
$color = strtolower($color);
foreach ($this->default_colors as $color_id => $params) {
if ($color_id === $color) {
return $color_id;
} elseif ($color === strtolower($params['name'])) {
return $color_id;
}
}
return '';
}
/**
* Get color properties
*
* @access public
* @param string $color_id
* @return array
*/
public function getColorProperties($color_id)
{
if (isset($this->default_colors[$color_id])) {
return $this->default_colors[$color_id];
}
return $this->default_colors[$this->getDefaultColor()];
}
/**
* Get available colors
*
* @access public
* @param bool $prepend
* @return array
*/
public function getList($prepend = false)
{
$listing = $prepend ? array('' => t('All colors')) : array();
foreach ($this->default_colors as $color_id => $color) {
$listing[$color_id] = t($color['name']);
}
$this->hook->reference('model:color:get-list', $listing);
return $listing;
}
/**
* Get the default color
*
* @access public
* @return string
*/
public function getDefaultColor()
{
return $this->configModel->get('default_color', 'yellow');
}
/**
* Get the default colors
*
* @access public
* @return array
*/
public function getDefaultColors()
{
return $this->default_colors;
}
/**
* Get border color from string
*
* @access public
* @param string $color_id Color id
* @return string
*/
public function getBorderColor($color_id)
{
$color = $this->getColorProperties($color_id);
return $color['border'];
}
/**
* Get background color from the color_id
*
* @access public
* @param string $color_id Color id
* @return string
*/
public function getBackgroundColor($color_id)
{
$color = $this->getColorProperties($color_id);
return $color['background'];
}
/**
* Get CSS stylesheet of all colors
*
* @access public
* @return string
*/
public function getCss()
{
$buffer = '';
foreach ($this->default_colors as $color => $values) {
$buffer .= '.task-board.color-'.$color.', .task-summary-container.color-'.$color.', .color-picker-square.color-'.$color.', .task-board-category.color-'.$color.', .table-list-category.color-'.$color.', .task-tag.color-'.$color.' {';
$buffer .= 'background-color: '.$values['background'].';';
$buffer .= 'border-color: '.$values['border'];
$buffer .= '}';
$buffer .= 'td.color-'.$color.' { background-color: '.$values['background'].'}';
$buffer .= '.table-list-row.color-'.$color.' {border-left: 5px solid '.$values['border'].'}';
}
return $buffer;
}
}
+262
View File
@@ -0,0 +1,262 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Column Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ColumnModel 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 projectId by the columnId
*
* @access public
* @param integer $column_id Column id
* @return integer
*/
public function getProjectId($column_id)
{
return $this->db->table(self::TABLE)->eq('id', $column_id)->findOneColumn('project_id');
}
/**
* 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 all columns with opened task count only
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getAllWithOpenedTaskCount($project_id)
{
return $this->db->table(self::TABLE)
->columns('id', 'title', 'position', 'task_limit', 'description', 'hide_in_dashboard', 'project_id')
->subquery("SELECT COUNT(*) FROM ".TaskModel::TABLE." WHERE column_id=".self::TABLE.".id AND is_active='1'", 'nb_open_tasks')
->eq('project_id', $project_id)
->asc('position')
->findAll();
}
/**
* Get all columns with task count
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getAllWithTaskCount($project_id)
{
return $this->db->table(self::TABLE)
->columns('id', 'title', 'position', 'task_limit', 'description', 'hide_in_dashboard', 'project_id')
->subquery("SELECT COUNT(*) FROM ".TaskModel::TABLE." WHERE column_id=".self::TABLE.".id AND is_active='1'", 'nb_open_tasks')
->subquery("SELECT COUNT(*) FROM ".TaskModel::TABLE." WHERE column_id=".self::TABLE.".id AND is_active='0'", 'nb_closed_tasks')
->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
* @param integer $hide_in_dashboard
* @return bool|int
*/
public function create($project_id, $title, $task_limit = 0, $description = '', $hide_in_dashboard = 0)
{
$values = array(
'project_id' => $project_id,
'title' => $title,
'task_limit' => intval($task_limit),
'position' => $this->getLastColumnPosition($project_id) + 1,
'hide_in_dashboard' => $hide_in_dashboard,
'description' => $description,
);
return $this->db->table(self::TABLE)->persist($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
* @param integer $hide_in_dashboard
* @return boolean
*/
public function update($column_id, $title, $task_limit = 0, $description = '', $hide_in_dashboard = 0)
{
return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array(
'title' => $title,
'task_limit' => intval($task_limit),
'hide_in_dashboard' => $hide_in_dashboard,
'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)->eq('project_id', $project_id)->update(array('position' => $position));
return !in_array(false, $results, true);
}
}
+174
View File
@@ -0,0 +1,174 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Class ColumnMoveRestrictionModel
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ColumnMoveRestrictionModel extends Base
{
const TABLE = 'column_has_move_restrictions';
/**
* Fetch one restriction
*
* @param int $project_id
* @param int $restriction_id
* @return array|null
*/
public function getById($project_id, $restriction_id)
{
return $this->db
->table(self::TABLE)
->columns(
self::TABLE.'.restriction_id',
self::TABLE.'.project_id',
self::TABLE.'.role_id',
self::TABLE.'.src_column_id',
self::TABLE.'.dst_column_id',
self::TABLE.'.only_assigned',
'pr.role',
'sc.title as src_column_title',
'dc.title as dst_column_title'
)
->left(ColumnModel::TABLE, 'sc', 'id', self::TABLE, 'src_column_id')
->left(ColumnModel::TABLE, 'dc', 'id', self::TABLE, 'dst_column_id')
->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id')
->eq(self::TABLE.'.project_id', $project_id)
->eq(self::TABLE.'.restriction_id', $restriction_id)
->findOne();
}
/**
* Get all project column restrictions
*
* @param int $project_id
* @return array
*/
public function getAll($project_id)
{
return $this->db
->table(self::TABLE)
->columns(
self::TABLE.'.restriction_id',
self::TABLE.'.project_id',
self::TABLE.'.role_id',
self::TABLE.'.src_column_id',
self::TABLE.'.dst_column_id',
self::TABLE.'.only_assigned',
'pr.role',
'sc.title as src_column_title',
'dc.title as dst_column_title'
)
->left(ColumnModel::TABLE, 'sc', 'id', self::TABLE, 'src_column_id')
->left(ColumnModel::TABLE, 'dc', 'id', self::TABLE, 'dst_column_id')
->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id')
->eq(self::TABLE.'.project_id', $project_id)
->findAll();
}
/**
* Get all sortable column Ids
*
* @param int $project_id
* @param string $role
* @return array
*/
public function getSortableColumns($project_id, $role)
{
return $this->db
->table(self::TABLE)
->columns(self::TABLE.'.src_column_id', self::TABLE.'.dst_column_id', self::TABLE.'.only_assigned')
->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id')
->eq(self::TABLE.'.project_id', $project_id)
->eq('pr.role', $role)
->findAll();
}
/**
* Create a new column restriction
*
* @param int $project_id
* @param int $role_id
* @param int $src_column_id
* @param int $dst_column_id
* @param bool $only_assigned
* @return bool|int
*/
public function create($project_id, $role_id, $src_column_id, $dst_column_id, $only_assigned = false)
{
return $this->db
->table(self::TABLE)
->persist(array(
'project_id' => $project_id,
'role_id' => $role_id,
'src_column_id' => $src_column_id,
'dst_column_id' => $dst_column_id,
'only_assigned' => (int) $only_assigned,
));
}
/**
* Remove a permission
*
* @param int $restriction_id
* @return bool
*/
public function remove($restriction_id)
{
return $this->db->table(self::TABLE)->eq('restriction_id', $restriction_id)->remove();
}
/**
* Copy column_move_restriction models from a custome_role in the src project to the dst custom_role of the dst project
*
* @param integer $project_src_id
* @param integer $project_dst_id
* @param integer $role_src_id
* @param integer $role_dst_id
* @return boolean
*/
public function duplicate($project_src_id, $project_dst_id, $role_src_id, $role_dst_id)
{
$rows = $this->db->table(self::TABLE)
->eq('project_id', $project_src_id)
->eq('role_id', $role_src_id)
->findAll();
foreach ($rows as $row) {
$src_column_title = $this->columnModel->getColumnTitleById($row['src_column_id']);
$dst_column_title = $this->columnModel->getColumnTitleById($row['dst_column_id']);
$src_column_id = $this->columnModel->getColumnIdByTitle($project_dst_id, $src_column_title);
$dst_column_id = $this->columnModel->getColumnIdByTitle($project_dst_id, $dst_column_title);
if (! $dst_column_id) {
$this->logger->error("The column $dst_column_title is not present in project $project_dst_id");
return false;
}
if (! $src_column_id) {
$this->logger->error("The column $src_column_title is not present in project $project_dst_id");
return false;
}
$result = $this->db->table(self::TABLE)->persist(array(
'project_id' => $project_dst_id,
'role_id' => $role_dst_id,
'src_column_id' => $src_column_id,
'dst_column_id' => $dst_column_id,
'only_assigned' => (int) $row['only_assigned'],
));
if (! $result) {
return false;
}
}
return true;
}
}
+192
View File
@@ -0,0 +1,192 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Class ColumnRestrictionModel
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ColumnRestrictionModel extends Base
{
const TABLE = 'column_has_restrictions';
const RULE_ALLOW_TASK_CREATION = 'allow.task_creation';
const RULE_ALLOW_TASK_OPEN_CLOSE = 'allow.task_open_close';
const RULE_BLOCK_TASK_CREATION = 'block.task_creation';
const RULE_BLOCK_TASK_OPEN_CLOSE = 'block.task_open_close';
/**
* Get rules
*
* @return array
*/
public function getRules()
{
return array(
self::RULE_ALLOW_TASK_CREATION => t('Task creation is permitted for this column'),
self::RULE_ALLOW_TASK_OPEN_CLOSE => t('Closing or opening a task is permitted for this column'),
self::RULE_BLOCK_TASK_CREATION => t('Task creation is blocked for this column'),
self::RULE_BLOCK_TASK_OPEN_CLOSE => t('Closing or opening a task is blocked for this column'),
);
}
/**
* Fetch one restriction
*
* @param int $project_id
* @param int $restriction_id
* @return array|null
*/
public function getById($project_id, $restriction_id)
{
return $this->db
->table(self::TABLE)
->columns(
'restriction_id',
'project_id',
'role_id',
'column_id',
'rule',
'pr.role',
'c.title as column_title'
)
->left(ColumnModel::TABLE, 'c', 'id', self::TABLE, 'column_id')
->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id')
->eq(self::TABLE.'.project_id', $project_id)
->eq(self::TABLE.'.restriction_id', $restriction_id)
->findOne();
}
/**
* Get all project column restrictions
*
* @param int $project_id
* @return array
*/
public function getAll($project_id)
{
$rules = $this->getRules();
$restrictions = $this->db
->table(self::TABLE)
->columns(
'restriction_id',
'project_id',
'role_id',
'column_id',
'rule',
'pr.role',
'c.title as column_title'
)
->left(ColumnModel::TABLE, 'c', 'id', self::TABLE, 'column_id')
->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id')
->eq(self::TABLE.'.project_id', $project_id)
->findAll();
foreach ($restrictions as &$restriction) {
$restriction['title'] = $rules[$restriction['rule']];
}
return $restrictions;
}
/**
* Get restrictions
*
* @param int $project_id
* @param string $role
* @return array
*/
public function getAllByRole($project_id, $role)
{
return $this->db
->table(self::TABLE)
->columns(
'restriction_id',
'project_id',
'role_id',
'column_id',
'rule',
'pr.role'
)
->eq(self::TABLE.'.project_id', $project_id)
->eq('pr.role', $role)
->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id')
->findAll();
}
/**
* Create a new column restriction
*
* @param int $project_id
* @param int $role_id
* @param int $column_id
* @param int $rule
* @return bool|int
*/
public function create($project_id, $role_id, $column_id, $rule)
{
return $this->db
->table(self::TABLE)
->persist(array(
'project_id' => $project_id,
'role_id' => $role_id,
'column_id' => $column_id,
'rule' => $rule,
));
}
/**
* Remove a permission
*
* @param int $restriction_id
* @return bool
*/
public function remove($restriction_id)
{
return $this->db->table(self::TABLE)->eq('restriction_id', $restriction_id)->remove();
}
/**
* Copy column_restriction models from a custome_role in the src project to the dst custom_role of the dst project
*
* @param integer $project_src_id
* @param integer $project_dst_id
* @param integer $role_src_id
* @param integer $role_dst_id
* @return boolean
*/
public function duplicate($project_src_id, $project_dst_id, $role_src_id, $role_dst_id)
{
$rows = $this->db->table(self::TABLE)
->eq('project_id', $project_src_id)
->eq('role_id', $role_src_id)
->findAll();
foreach ($rows as $row) {
$column_title = $this->columnModel->getColumnTitleById($row['column_id']);
$dst_column_id = $this->columnModel->getColumnIdByTitle($project_dst_id, $column_title);
if (! $dst_column_id) {
$this->logger->error("The column $column_title is not present in project $project_dst_id");
return false;
}
$result = $this->db->table(self::TABLE)->persist(array(
'project_id' => $project_dst_id,
'role_id' => $role_dst_id,
'column_id' => $dst_column_id,
'rule' => $row['rule'],
));
if (! $result) {
return false;
}
}
return true;
}
}
+222
View File
@@ -0,0 +1,222 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
use Kanboard\Core\Security\Role;
/**
* Comment model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class CommentModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'comments';
/**
* Events
*
* @var string
*/
const EVENT_UPDATE = 'comment.update';
const EVENT_CREATE = 'comment.create';
const EVENT_DELETE = 'comment.delete';
const EVENT_USER_MENTION = 'comment.user.mention';
/**
* Get projectId from commentId
*
* @access public
* @param integer $comment_id
* @return integer
*/
public function getProjectId($comment_id)
{
return $this->db
->table(self::TABLE)
->eq(self::TABLE.'.id', $comment_id)
->join(TaskModel::TABLE, 'id', 'task_id')
->findOneColumn(TaskModel::TABLE . '.project_id') ?: 0;
}
/**
* Get visibility from commentId
*
* @access public
* @param integer $comment_id
* @return string
*/
public function getVisibility($comment_id)
{
return $this->db
->table(self::TABLE)
->eq(self::TABLE.'.id', $comment_id)
->findOneColumn(self::TABLE . '.visibility') ?: Role::APP_USER;
}
/**
* Get all comments for a given task
*
* @access public
* @param integer $task_id Task id
* @param string $sorting ASC/DESC
* @return array
*/
public function getAll($task_id, $sorting = 'ASC')
{
return $this->db
->table(self::TABLE)
->columns(
self::TABLE.'.id',
self::TABLE.'.date_creation',
self::TABLE.'.date_modification',
self::TABLE.'.task_id',
self::TABLE.'.user_id',
self::TABLE.'.comment',
self::TABLE.'.visibility',
UserModel::TABLE.'.username',
UserModel::TABLE.'.name',
UserModel::TABLE.'.email',
UserModel::TABLE.'.avatar_path'
)
->join(UserModel::TABLE, 'id', 'user_id')
->orderBy(self::TABLE.'.date_creation', $sorting)
->orderBy(self::TABLE.'.id', $sorting)
->eq(self::TABLE.'.task_id', $task_id)
->findAll();
}
/**
* Get a comment
*
* @access public
* @param integer $comment_id Comment id
* @return array
*/
public function getById($comment_id)
{
return $this->db
->table(self::TABLE)
->columns(
self::TABLE.'.id',
self::TABLE.'.task_id',
self::TABLE.'.user_id',
self::TABLE.'.date_creation',
self::TABLE.'.date_modification',
self::TABLE.'.comment',
self::TABLE.'.reference',
self::TABLE.'.visibility',
UserModel::TABLE.'.username',
UserModel::TABLE.'.name',
UserModel::TABLE.'.email',
UserModel::TABLE.'.avatar_path'
)
->join(UserModel::TABLE, 'id', 'user_id')
->eq(self::TABLE.'.id', $comment_id)
->findOne();
}
/**
* Get the number of comments for a given task
*
* @access public
* @param integer $task_id Task id
* @return integer
*/
public function count($task_id)
{
return $this->db
->table(self::TABLE)
->eq(self::TABLE.'.task_id', $task_id)
->count();
}
/**
* Create a new comment
*
* @access public
* @param array $values Form values
* @return boolean|integer
*/
public function create(array $values)
{
$values = $this->clampVisibility($values);
$values['date_creation'] = time();
$values['date_modification'] = time();
$comment_id = $this->db->table(self::TABLE)->persist($values);
if ($comment_id !== false) {
$this->queueManager->push($this->commentEventJob->withParams($comment_id, self::EVENT_CREATE));
}
return $comment_id;
}
/**
* Update a comment in the database
*
* @access public
* @param array $values Form values
* @return boolean
*/
public function update(array $values)
{
$result = $this->db
->table(self::TABLE)
->eq('id', $values['id'])
->update(array('comment' => $values['comment'], 'date_modification' => time()));
if ($result) {
$this->queueManager->push($this->commentEventJob->withParams($values['id'], self::EVENT_UPDATE));
}
return $result;
}
/**
* Clamp the visibility field so it never exceeds the current user's role
*
* @access protected
* @param array $values
* @return array
*/
protected function clampVisibility(array $values)
{
if (! $this->userSession->isLogged()) {
return $values;
}
$visibility = isset($values['visibility']) ? $values['visibility'] : Role::APP_USER;
$userRole = $this->userSession->getRole();
if ($userRole === Role::APP_MANAGER && $visibility === Role::APP_ADMIN) {
$values['visibility'] = Role::APP_MANAGER;
}
if ($userRole === Role::APP_USER && $visibility !== Role::APP_USER) {
$values['visibility'] = Role::APP_USER;
}
return $values;
}
/**
* Remove a comment
*
* @access public
* @param integer $comment_id Comment id
* @return boolean
*/
public function remove($comment_id)
{
$this->commentEventJob->execute($comment_id, self::EVENT_DELETE);
return $this->db->table(self::TABLE)->eq('id', $comment_id)->remove();
}
}
+122
View File
@@ -0,0 +1,122 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Security\Token;
/**
* Config model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ConfigModel extends SettingModel
{
/**
* Get a config variable with in-memory caching
*
* @access public
* @param string $name Parameter name
* @param string $default_value Default value of the parameter
* @return string
*/
public function get($name, $default_value = '')
{
$options = $this->memoryCache->proxy($this, 'getAll');
return isset($options[$name]) && $options[$name] !== '' ? $options[$name] : $default_value;
}
/**
* Optimize the Sqlite database
*
* @access public
* @return boolean
*/
public function optimizeDatabase()
{
return $this->db->getConnection()->exec('VACUUM');
}
/**
* Compress the Sqlite database
*
* @access public
* @return string
*/
public function downloadDatabase()
{
return gzencode(file_get_contents(DB_FILENAME));
}
/**
* Replace database file with uploaded one
*
* @access public
* @param string $file
* @return bool
*/
public function uploadDatabase($file)
{
$this->db->closeConnection();
return file_put_contents(DB_FILENAME, gzdecode(file_get_contents($file))) !== false;
}
/**
* Get the Sqlite database size in bytes
*
* @access public
* @return integer
*/
public function getDatabaseSize()
{
return DB_DRIVER === 'sqlite' ? filesize(DB_FILENAME) : 0;
}
/**
* Get database extra options
*
* @access public
* @return array
*/
public function getDatabaseOptions()
{
if (DB_DRIVER === 'sqlite') {
return [
'journal_mode' => $this->db->getConnection()->query('PRAGMA journal_mode')->fetchColumn(),
'wal_autocheckpoint' => $this->db->getConnection()->query('PRAGMA wal_autocheckpoint')->fetchColumn(),
'synchronous' => $this->db->getConnection()->query('PRAGMA synchronous')->fetchColumn(),
'busy_timeout' => $this->db->getConnection()->query('PRAGMA busy_timeout')->fetchColumn(),
];
}
return [];
}
/**
* Regenerate a token
*
* @access public
* @param string $option Parameter name
* @return boolean
*/
public function regenerateToken($option)
{
return $this->save(array($option => Token::getToken()));
}
/**
* Prepare data before save
*
* @access public
* @param array $values
* @return array
*/
public function prepare(array $values)
{
if (! empty($values['application_url']) && substr($values['application_url'], -1) !== '/') {
$values['application_url'] = $values['application_url'].'/';
}
return $values;
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Currency
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class CurrencyModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'currencies';
/**
* Get available application currencies
*
* @access public
* @return array
*/
public function getCurrencies()
{
return array(
'ARS' => t('ARS - Argentine Peso'),
'AUD' => t('AUD - Australian Dollar'),
'BAM' => t('BAM - Konvertible Mark'),
'BRL' => t('BRL - Brazilian Real'),
'CAD' => t('CAD - Canadian Dollar'),
'CHF' => t('CHF - Swiss Francs'),
'CNY' => t('CNY - Chinese Yuan'),
'COP' => t('COP - Colombian Peso'),
'DKK' => t('DKK - Danish Krona'),
'EUR' => t('EUR - Euro'),
'GBP' => t('GBP - British Pound'),
'HRK' => t('HRK - Kuna'),
'HUF' => t('HUF - Hungarian Forint'),
'INR' => t('INR - Indian Rupee'),
'JPY' => t('JPY - Japanese Yen'),
'MXN' => t('MXN - Mexican Peso'),
'NOK' => t('NOK - Norwegian Krone'),
'NZD' => t('NZD - New Zealand Dollar'),
'PEN' => t('PEN - Peruvian Sol'),
'RSD' => t('RSD - Serbian dinar'),
'RUB' => t('RUB - Russian Ruble'),
'SEK' => t('SEK - Swedish Krona'),
'TRL' => t('TRL - Turkish Lira'),
'USD' => t('USD - US Dollar'),
'VBL' => t('VES - Venezuelan Bolívar'),
'XBT' => t('XBT - Bitcoin'),
);
}
/**
* Get all currency rates
*
* @access public
* @return array
*/
public function getAll()
{
return $this->db->table(self::TABLE)->findAll();
}
/**
* Calculate the price for the reference currency
*
* @access public
* @param string $currency
* @param double $price
* @return double
*/
public function getPrice($currency, $price)
{
static $rates = null;
$reference = $this->configModel->get('application_currency', 'USD');
if ($reference !== $currency) {
$rates = $rates === null ? $this->db->hashtable(self::TABLE)->getAll('currency', 'rate') : $rates;
$rate = isset($rates[$currency]) ? $rates[$currency] : 1;
return $rate * $price;
}
return $price;
}
/**
* Add a new currency rate
*
* @access public
* @param string $currency
* @param float $rate
* @return boolean|integer
*/
public function create($currency, $rate)
{
if ($this->db->table(self::TABLE)->eq('currency', $currency)->exists()) {
return $this->update($currency, $rate);
}
return $this->db->table(self::TABLE)->insert(array('currency' => $currency, 'rate' => $rate));
}
/**
* Update a currency rate
*
* @access public
* @param string $currency
* @param float $rate
* @return boolean
*/
public function update($currency, $rate)
{
return $this->db->table(self::TABLE)->eq('currency', $currency)->update(array('rate' => $rate));
}
}
+141
View File
@@ -0,0 +1,141 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Custom Filter model
*
* @package Kanboard\Model
* @author Timo Litzbarski
*/
class CustomFilterModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'custom_filters';
/**
* Return the list of all allowed custom filters for a user and project
*
* @access public
* @param integer $project_id Project id
* @param integer $user_id User id
* @return array
*/
public function getAll($project_id, $user_id)
{
return $this->db
->table(self::TABLE)
->columns(
UserModel::TABLE.'.name as owner_name',
UserModel::TABLE.'.username as owner_username',
self::TABLE.'.id',
self::TABLE.'.user_id',
self::TABLE.'.project_id',
self::TABLE.'.filter',
self::TABLE.'.name',
self::TABLE.'.is_shared',
self::TABLE.'.append'
)
->asc(self::TABLE.'.name')
->join(UserModel::TABLE, 'id', 'user_id')
->beginOr()
->eq('is_shared', 1)
->eq('user_id', $user_id)
->closeOr()
->eq('project_id', $project_id)
->findAll();
}
/**
* Get custom filter by id
*
* @access private
* @param integer $filter_id
* @return array
*/
public function getById($filter_id)
{
return $this->db->table(self::TABLE)->eq('id', $filter_id)->findOne();
}
/**
* Create a custom filter
*
* @access public
* @param array $values Form values
* @return bool|integer
*/
public function create(array $values)
{
return $this->db->table(self::TABLE)->persist($values);
}
/**
* Update a custom filter
*
* @access public
* @param array $values Form values
* @return bool
*/
public function update(array $values)
{
$updates = $values;
unset($updates['id']);
return $this->db->table(self::TABLE)
->eq('id', $values['id'])
->update($updates);
}
/**
* Remove a custom filter
*
* @access public
* @param integer $filter_id
* @return bool
*/
public function remove($filter_id)
{
return $this->db->table(self::TABLE)->eq('id', $filter_id)->remove();
}
/**
* Duplicate custom filters from a project to another one, must be executed inside a transaction
*
* @param integer $src_project_id Source project id
* @param integer $dst_project_id Destination project id
* @return boolean
*/
public function duplicate($src_project_id, $dst_project_id)
{
$filters = $this->db
->table(self::TABLE)
->columns(
self::TABLE.'.user_id',
self::TABLE.'.filter',
self::TABLE.'.name',
self::TABLE.'.is_shared',
self::TABLE.'.append'
)
->eq('project_id', $src_project_id)
->findAll();
foreach ($filters as $filter) {
$filter['project_id'] = $dst_project_id;
// Avoid SQL error with Postgres
$filter['is_shared'] = $filter['is_shared'] ?: 0;
$filter['append'] = $filter['append'] ?: 0;
if (! $this->db->table(self::TABLE)->save($filter)) {
return false;
}
}
return true;
}
}
+416
View File
@@ -0,0 +1,416 @@
<?php
namespace Kanboard\Model;
use Exception;
use Kanboard\Core\Base;
use Kanboard\Core\Thumbnail;
use Kanboard\Core\ObjectStorage\ObjectStorageException;
/**
* Base File Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
abstract class FileModel extends Base
{
/**
* Get the table
*
* @abstract
* @access protected
* @return string
*/
abstract protected function getTable();
/**
* Define the foreign key
*
* @abstract
* @access protected
* @return string
*/
abstract protected function getForeignKey();
/**
* Get the path prefix
*
* @abstract
* @access protected
* @return string
*/
abstract protected function getPathPrefix();
/**
* Fire file creation event
*
* @abstract
* @access protected
* @param integer $file_id
*/
abstract protected function fireCreationEvent($file_id);
/**
* Fire file destruction event
*
* @abstract
* @access protected
* @param integer $file_id
*/
abstract protected function fireDestructionEvent($file_id);
/**
* Get PicoDb query to get all files
*
* @access protected
* @return \PicoDb\Table
*/
protected function getQuery()
{
return $this->db
->table($this->getTable())
->columns(
$this->getTable().'.id',
$this->getTable().'.name',
$this->getTable().'.path',
$this->getTable().'.is_image',
$this->getTable().'.'.$this->getForeignKey(),
$this->getTable().'.date',
$this->getTable().'.user_id',
$this->getTable().'.size',
UserModel::TABLE.'.username',
UserModel::TABLE.'.name as user_name'
)
->join(UserModel::TABLE, 'id', 'user_id')
->asc($this->getTable().'.name');
}
/**
* Get a file by the id
*
* @access public
* @param integer $file_id File id
* @return array
*/
public function getById($file_id)
{
$file = $this->db->table($this->getTable())->eq('id', $file_id)->findOne();
if ($file) {
$file['etag'] = md5($file['path']);
}
return $file;
}
/**
* Get all files
*
* @access public
* @param integer $id
* @return array
*/
public function getAll($id)
{
$files = $this->getQuery()->eq($this->getForeignKey(), $id)->findAll();
foreach ($files as &$file) {
$file['etag'] = md5($file['path']);
}
return $files;
}
/**
* Get all images
*
* @access public
* @param integer $id
* @return array
*/
public function getAllImages($id)
{
$images = $this->getQuery()->eq($this->getForeignKey(), $id)->eq('is_image', 1)->findAll();
foreach ($images as &$image) {
$image['etag'] = md5($image['path']);
}
return $images;
}
/**
* Get all files without images
*
* @access public
* @param integer $id
* @return array
*/
public function getAllDocuments($id)
{
$files = $this->getQuery()->eq($this->getForeignKey(), $id)->eq('is_image', 0)->findAll();
foreach ($files as &$file) {
$file['etag'] = md5($file['path']);
}
return $files;
}
/**
* Create a file entry in the database
*
* @access public
* @param integer $foreign_key_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($foreign_key_id, $name, $path, $size)
{
$values = array(
$this->getForeignKey() => $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(),
);
$result = $this->db->table($this->getTable())->insert($values);
if ($result) {
$file_id = (int) $this->db->getLastId();
$this->fireCreationEvent($file_id);
return $file_id;
}
return false;
}
/**
* Remove all files
*
* @access public
* @param integer $id
* @return bool
*/
public function removeAll($id)
{
$file_ids = $this->db->table($this->getTable())->eq($this->getForeignKey(), $id)->asc('id')->findAllByColumn('id');
$results = array();
foreach ($file_ids as $file_id) {
$results[] = $this->remove($file_id);
}
return ! in_array(false, $results, true);
}
/**
* Remove a file
*
* @access public
* @param integer $file_id File id
* @return bool
*/
public function remove($file_id)
{
try {
$this->fireDestructionEvent($file_id);
$file = $this->getById($file_id);
// Only remove files from disk attached to a single task.
$multiple_tasks_count = $this->db->table($this->getTable())->eq('path', $file['path'])->count();
if ($multiple_tasks_count === 1) {
$this->objectStorage->remove($file['path']);
if ($file['is_image'] == 1) {
$this->objectStorage->remove($this->getThumbnailPath($file['path']));
}
}
return $this->db->table($this->getTable())->eq('id', $file['id'])->remove();
} catch (ObjectStorageException $e) {
$this->logger->error($e->getMessage());
return false;
}
}
/**
* Check if a filename is an image (file types that can be shown as thumbnail)
*
* @access public
* @param string $filename Filename
* @return bool
*/
public function isImage($filename)
{
switch (get_file_extension($filename)) {
case 'jpeg':
case 'jpg':
case 'png':
case 'gif':
return true;
}
return false;
}
/**
* Generate the path for a thumbnails
*
* @access public
* @param string $key Storage key
* @return string
*/
public function getThumbnailPath($key)
{
return 'thumbnails'.DIRECTORY_SEPARATOR.$key;
}
/**
* Generate the path for a new filename
*
* @access public
* @param integer $id Foreign key
* @param string $filename Filename
* @return string
*/
public function generatePath($id, $filename)
{
if (is_string($id)) {
$id = (int) $id;
}
if (! is_int($id) || $id <= 0) {
throw new Exception('Invalid ID provided for file path generation');
}
return $this->getPathPrefix().DIRECTORY_SEPARATOR.$id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time());
}
/**
* Upload multiple files
*
* @access public
* @param integer $id
* @param array $files
* @return bool
*/
public function uploadFiles($id, array $files)
{
try {
if (empty($files)) {
return false;
}
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 (Exception $e) {
$this->logger->error($e->getMessage());
return false;
}
}
/**
* Upload a file
*
* @access public
* @param integer $id
* @param array $file
* @throws Exception
*/
public function uploadFile($id, array $file)
{
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 $id
* @param string $originalFilename
* @param string $data
* @param bool $isEncoded
* @return bool|int
*/
public function uploadContent($id, $originalFilename, $data, $isEncoded = true)
{
try {
if ($isEncoded) {
$data = base64_decode($data);
}
if (empty($data)) {
$this->logger->error(__METHOD__.': Content upload with no data');
return false;
}
$destinationFilename = $this->generatePath($id, $originalFilename);
$this->objectStorage->put($destinationFilename, $data);
if ($this->isImage($originalFilename)) {
$this->generateThumbnailFromData($destinationFilename, $data);
}
return $this->create(
$id,
$originalFilename,
$destinationFilename,
strlen($data)
);
} catch (ObjectStorageException $e) {
$this->logger->error($e->getMessage());
return false;
}
}
/**
* Generate thumbnail from a blob
*
* @access public
* @param string $destination_filename
* @param string $data
*/
public function generateThumbnailFromData($destination_filename, &$data)
{
$blob = Thumbnail::createFromString($data)
->resize()
->toString();
$this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob);
}
/**
* Generate thumbnail from a local file
*
* @access public
* @param string $uploaded_filename
* @param string $destination_filename
*/
public function generateThumbnailFromFile($uploaded_filename, $destination_filename)
{
$blob = Thumbnail::createFromFile($uploaded_filename)
->resize()
->toString();
$this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob);
}
}
+131
View File
@@ -0,0 +1,131 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Group Member Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class GroupMemberModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'group_has_users';
/**
* Get query to fetch all users
*
* @access public
* @param integer $group_id
* @return \PicoDb\Table
*/
public function getQuery($group_id)
{
return $this->db->table(self::TABLE)
->join(UserModel::TABLE, 'id', 'user_id')
->eq('group_id', $group_id);
}
/**
* Get all users
*
* @access public
* @param integer $group_id
* @return array
*/
public function getMembers($group_id)
{
return $this->getQuery($group_id)->findAll();
}
/**
* Get all not members
*
* @access public
* @param integer $group_id
* @return array
*/
public function getNotMembers($group_id)
{
$subquery = $this->db->table(self::TABLE)
->columns('user_id')
->eq('group_id', $group_id);
return $this->db->table(UserModel::TABLE)
->notInSubquery('id', $subquery)
->eq('is_active', 1)
->findAll();
}
/**
* Add user to a group
*
* @access public
* @param integer $group_id
* @param integer $user_id
* @return boolean
*/
public function addUser($group_id, $user_id)
{
return $this->db->table(self::TABLE)->insert(array(
'group_id' => $group_id,
'user_id' => $user_id,
));
}
/**
* Remove user from a group
*
* @access public
* @param integer $group_id
* @param integer $user_id
* @return boolean
*/
public function removeUser($group_id, $user_id)
{
return $this->db->table(self::TABLE)
->eq('group_id', $group_id)
->eq('user_id', $user_id)
->remove();
}
/**
* Check if a user is member
*
* @access public
* @param integer $group_id
* @param integer $user_id
* @return boolean
*/
public function isMember($group_id, $user_id)
{
return $this->db->table(self::TABLE)
->eq('group_id', $group_id)
->eq('user_id', $user_id)
->exists();
}
/**
* Get all groups for a given user
*
* @access public
* @param integer $user_id
* @return array
*/
public function getGroups($user_id)
{
return $this->db->table(self::TABLE)
->columns(GroupModel::TABLE.'.id', GroupModel::TABLE.'.external_id', GroupModel::TABLE.'.name')
->join(GroupModel::TABLE, 'id', 'group_id')
->eq(self::TABLE.'.user_id', $user_id)
->asc(GroupModel::TABLE.'.name')
->findAll();
}
}
+158
View File
@@ -0,0 +1,158 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Group Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class GroupModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'groups';
/**
* Get query to fetch all groups
*
* @access public
* @return \PicoDb\Table
*/
public function getQuery()
{
return $this->db->table(self::TABLE)
->columns('id', 'name', 'external_id')
->subquery('SELECT COUNT(*) FROM '.GroupMemberModel::TABLE.' WHERE group_id='.self::TABLE.'.id', 'nb_users');
}
/**
* Get a specific group by id
*
* @access public
* @param integer $group_id
* @return array
*/
public function getById($group_id)
{
return $this->db->table(self::TABLE)->eq('id', $group_id)->findOne();
}
/**
* Get a specific group by externalID
*
* @access public
* @param string $external_id
* @return array
*/
public function getByExternalId($external_id)
{
return $this->db->table(self::TABLE)->eq('external_id', $external_id)->findOne();
}
/**
* Get specific groups by externalIDs
*
* @access public
* @param string[] $external_ids
* @return array
*/
public function getByExternalIds(array $external_ids)
{
if (empty($external_ids)) {
return [];
}
return $this->db->table(self::TABLE)->in('external_id', $external_ids)->findAll();
}
/**
* Get all groups
*
* @access public
* @return array
*/
public function getAll()
{
return $this->getQuery()->asc('name')->findAll();
}
/**
* Search groups by name
*
* @access public
* @param string $input
* @return array
*/
public function search($input)
{
return $this->db->table(self::TABLE)->ilike('name', '%'.$input.'%')->asc('name')->findAll();
}
/**
* Remove a group
*
* @access public
* @param integer $group_id
* @return boolean
*/
public function remove($group_id)
{
return $this->db->table(self::TABLE)->eq('id', $group_id)->remove();
}
/**
* Create a new group
*
* @access public
* @param string $name
* @param string $external_id
* @return integer|boolean
*/
public function create($name, $external_id = '')
{
return $this->db->table(self::TABLE)->persist(array(
'name' => $name,
'external_id' => $external_id,
));
}
/**
* Update existing group
*
* @access public
* @param array $values
* @return boolean
*/
public function update(array $values)
{
$updates = $values;
unset($updates['id']);
return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($updates);
}
/**
* Get groupId from externalGroupId and create the group if not found
*
* @access public
* @param string $name
* @param string $external_id
* @return bool|integer
*/
public function getOrCreateExternalGroupId($name, $external_id)
{
$group_id = $this->db->table(self::TABLE)->eq('external_id', $external_id)->findOneColumn('id');
if (empty($group_id)) {
$group_id = $this->create($name, $external_id);
}
return $group_id;
}
}
+73
View File
@@ -0,0 +1,73 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
use Kanboard\Core\Security\Token;
/**
* Class InviteModel
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class InviteModel extends Base
{
const TABLE = 'invites';
public function createInvites(array $emails, $projectId)
{
$emails = array_unique($emails);
$nb = 0;
foreach ($emails as $email) {
$email = trim($email);
if (! empty($email) && $this->createInvite($email, $projectId)) {
$nb++;
}
}
return $nb;
}
protected function createInvite($email, $projectId)
{
$values = array(
'email' => $email,
'project_id' => $projectId,
'token' => Token::getToken(),
);
if ($this->db->table(self::TABLE)->insert($values)) {
$this->sendInvite($values);
return true;
}
return false;
}
protected function sendInvite(array $values)
{
$this->emailClient->send(
$values['email'],
$values['email'],
e('Kanboard Invitation'),
$this->template->render('user_invite/email', array('token' => $values['token']))
);
}
public function getByToken($token)
{
return $this->db->table(self::TABLE)
->eq('token', $token)
->findOne();
}
public function remove($email)
{
return $this->db->table(self::TABLE)
->eq('email', $email)
->remove();
}
}
+237
View File
@@ -0,0 +1,237 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
use Kanboard\Core\Translator;
/**
* Class Language
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class LanguageModel extends Base
{
/**
* Get all language codes
*
* @static
* @access public
* @return string[]
*/
public static function getCodes()
{
return array(
'id_ID',
'bg_BG',
'bs_BA',
'ca_ES',
'cs_CZ',
'da_DK',
'de_DE',
'de_DE_du',
'en_GB',
'en_US',
'es_ES',
'es_VE',
'fr_FR',
'el_GR',
'it_IT',
'hr_HR',
'hu_HU',
'mk_MK',
'my_MY',
'nl_NL',
'nb_NO',
'pl_PL',
'pt_PT',
'pt_BR',
'ro_RO',
'ru_RU',
'sr_Latn_RS',
'fi_FI',
'sk_SK',
'sv_SE',
'tr_TR',
'uk_UA',
'ko_KR',
'zh_CN',
'zh_TW',
'ja_JP',
'th_TH',
'vi_VN',
'fa_IR',
'ar_SY',
);
}
/**
* Find language code
*
* @static
* @access public
* @param string $code
* @return string
*/
public static function findCode($code)
{
$code = str_replace('-', '_', $code);
return in_array($code, self::getCodes()) ? $code : '';
}
/**
* Get available languages
*
* @access public
* @param boolean $prepend Prepend a default value
* @return array
*/
public function getLanguages($prepend = false)
{
// Sorted by value
$languages = array(
'id_ID' => 'Bahasa Indonesia',
'bg_BG' => 'Български',
'bs_BA' => 'Bosanski',
'ca_ES' => 'Català',
'cs_CZ' => 'Čeština',
'da_DK' => 'Dansk',
'de_DE' => 'Deutsch (Sie)',
'de_DE_du' => 'Deutsch (du)',
'en_GB' => 'English (GB)',
'en_US' => 'English (US)',
'es_ES' => 'Español (España)',
'es_VE' => 'Español (Venezuela)',
'fr_FR' => 'Français',
'el_GR' => 'Greek (Ελληνικά)',
'hr_HR' => 'Hrvatski',
'it_IT' => 'Italiano',
'hu_HU' => 'Magyar',
'mk_MK' => 'Македонски',
'my_MY' => 'Melayu',
'nl_NL' => 'Nederlands',
'nb_NO' => 'Norsk',
'pl_PL' => 'Polski',
'pt_PT' => 'Português',
'pt_BR' => 'Português (Brasil)',
'ro_RO' => 'Română',
'ru_RU' => 'Русский',
'sr_Latn_RS' => 'Srpski',
'fi_FI' => 'Suomi',
'sk_SK' => 'Slovenčina',
'sv_SE' => 'Svenska',
'tr_TR' => 'Türkçe',
'uk_UA' => 'Українська',
'ko_KR' => '한국어',
'zh_CN' => '中文(简体)',
'zh_TW' => '中文(繁體)',
'ja_JP' => '日本語',
'th_TH' => 'ไทย',
'vi_VN' => 'Tiếng Việt',
'fa_IR' => 'فارسی',
'ar_SY' => 'عربي',
);
if ($prepend) {
return array('' => t('Application default')) + $languages;
}
return $languages;
}
/**
* Get javascript language code
*
* @access public
* @return string
*/
public function getJsLanguageCode()
{
$languages = array(
'bg_BG' => 'bg',
'cs_CZ' => 'cs',
'ca_ES' => 'ca',
'da_DK' => 'da',
'de_DE' => 'de',
'de_DE_du' => 'de',
'en_GB' => 'en-GB',
'en_US' => 'en',
'es_ES' => 'es',
'es_VE' => 'es',
'fr_FR' => 'fr',
'it_IT' => 'it',
'hr_HR' => 'hr',
'hu_HU' => 'hu',
'nl_NL' => 'nl',
'nb_NO' => 'no',
'pl_PL' => 'pl',
'pt_PT' => 'pt',
'pt_BR' => 'pt-BR',
'ro_RO' => 'ro',
'ru_RU' => 'ru',
'sr_Latn_RS' => 'sr',
'fi_FI' => 'fi',
'sk_SK' => 'sk',
'sv_SE' => 'sv',
'tr_TR' => 'tr',
'uk_UA' => 'uk',
'ko_KR' => 'ko',
'zh_CN' => 'zh-CN',
'zh_TW' => 'zh-TW',
'ja_JP' => 'ja',
'th_TH' => 'th',
'id_ID' => 'id',
'el_GR' => 'el',
'fa_IR' => 'fa',
'vi_VN' => 'vi',
'bs_BA' => 'bs',
'mk_MK' => 'mk',
'my_MY' => 'my',
'ar_SY' => 'ar',
);
$lang = $this->getCurrentLanguage();
return isset($languages[$lang]) ? $languages[$lang] : 'en';
}
/**
* Check if current language requires RTL direction
*
* @access public
* @return bool
*/
public function isRtlLanguage()
{
$rtlJsLanguageCodes = array(
'ar',
'fa',
);
$lang = $this->getJsLanguageCode();
return in_array($lang, $rtlJsLanguageCodes);
}
/**
* Get current language
*
* @access public
* @return string
*/
public function getCurrentLanguage()
{
return $this->userSession->getLanguage() ?: $this->configModel->get('application_language', 'en_US');
}
/**
* Load translations for the current language
*
* @access public
*/
public function loadCurrentLanguage()
{
Translator::load($this->getCurrentLanguage());
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* LastLogin model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class LastLoginModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'last_logins';
/**
* Number of connections to keep for history
*
* @var integer
*/
const NB_LOGINS = 10;
/**
* Create a new record
*
* @access public
* @param string $auth_type Authentication method
* @param integer $user_id User id
* @param string $ip IP Address
* @param string $user_agent User Agent
* @return boolean
*/
public function create($auth_type, $user_id, $ip, $user_agent)
{
$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(),
));
}
/**
* Cleanup login history
*
* @access public
* @param integer $user_id
*/
public function cleanup($user_id)
{
$connections = $this->db
->table(self::TABLE)
->eq('user_id', $user_id)
->desc('id')
->findAllByColumn('id');
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();
}
}
/**
* Get the last connections for a given user
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function getAll($user_id)
{
return $this->db
->table(self::TABLE)
->eq('user_id', $user_id)
->desc('id')
->columns('id', 'auth_type', 'ip', 'user_agent', 'date_creation')
->findAll();
}
}
+178
View File
@@ -0,0 +1,178 @@
<?php
namespace Kanboard\Model;
use PDO;
use Kanboard\Core\Base;
/**
* Link model
*
* @package Kanboard\Model
* @author Olivier Maridat
* @author Frederic Guillot
*/
class LinkModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'links';
/**
* Get a link by id
*
* @access public
* @param integer $link_id Link id
* @return array
*/
public function getById($link_id)
{
return $this->db->table(self::TABLE)->eq('id', $link_id)->findOne();
}
/**
* Get a link by name
*
* @access public
* @param string $label
* @return array
*/
public function getByLabel($label)
{
return $this->db->table(self::TABLE)->eq('label', $label)->findOne();
}
/**
* Get the opposite link id
*
* @access public
* @param integer $link_id Link id
* @return integer
*/
public function getOppositeLinkId($link_id)
{
return $this->db->table(self::TABLE)->eq('id', $link_id)->findOneColumn('opposite_id') ?: $link_id;
}
/**
* Get all links
*
* @access public
* @return array
*/
public function getAll()
{
return $this->db->table(self::TABLE)->findAll();
}
/**
* Get merged links
*
* @access public
* @return array
*/
public function getMergedList()
{
return $this->db
->execute('
SELECT
links.id, links.label, opposite.label as opposite_label
FROM links
LEFT JOIN links AS opposite ON opposite.id=links.opposite_id
')
->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Get label list
*
* @access public
* @param integer $exclude_id Exclude this link
* @param boolean $prepend Prepend default value
* @return array
*/
public function getList($exclude_id = 0, $prepend = true)
{
$labels = $this->db->hashtable(self::TABLE)->neq('id', $exclude_id)->asc('id')->getAll('id', 'label');
foreach ($labels as &$value) {
$value = t($value);
}
return $prepend ? array('') + $labels : $labels;
}
/**
* Create a new link label
*
* @access public
* @param string $label
* @param string $opposite_label
* @return boolean|integer
*/
public function create($label, $opposite_label = '')
{
$this->db->startTransaction();
if (! $this->db->table(self::TABLE)->insert(array('label' => $label))) {
$this->db->cancelTransaction();
return false;
}
$label_id = $this->db->getLastId();
if (! empty($opposite_label)) {
$this->db
->table(self::TABLE)
->insert(array(
'label' => $opposite_label,
'opposite_id' => $label_id,
));
$this->db
->table(self::TABLE)
->eq('id', $label_id)
->update(array(
'opposite_id' => $this->db->getLastId()
));
}
$this->db->closeTransaction();
return (int) $label_id;
}
/**
* Update a link
*
* @access public
* @param array $values
* @return boolean
*/
public function update(array $values)
{
return $this->db
->table(self::TABLE)
->eq('id', $values['id'])
->update(array(
'label' => $values['label'],
'opposite_id' => $values['opposite_id'],
));
}
/**
* Remove a link a the relation to its opposite
*
* @access public
* @param integer $link_id
* @return boolean
*/
public function remove($link_id)
{
$this->db->table(self::TABLE)->eq('opposite_id', $link_id)->update(array('opposite_id' => 0));
return $this->db->table(self::TABLE)->eq('id', $link_id)->remove();
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Metadata
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
abstract class MetadataModel extends Base
{
/**
* Get the table
*
* @abstract
* @access protected
* @return string
*/
abstract protected function getTable();
/**
* Define the entity key
*
* @abstract
* @access protected
* @return string
*/
abstract protected function getEntityKey();
/**
* Get all metadata for the entity
*
* @access public
* @param integer $entity_id
* @return array
*/
public function getAll($entity_id)
{
return $this->db
->hashtable($this->getTable())
->eq($this->getEntityKey(), $entity_id)
->asc('name')
->getAll('name', 'value');
}
/**
* Get a metadata for the given entity
*
* @access public
* @param integer $entity_id
* @param string $name
* @param string $default
* @return mixed
*/
public function get($entity_id, $name, $default = '')
{
return $this->db
->table($this->getTable())
->eq($this->getEntityKey(), $entity_id)
->eq('name', $name)
->findOneColumn('value') ?: $default;
}
/**
* Return true if a metadata exists
*
* @access public
* @param integer $entity_id
* @param string $name
* @return boolean
*/
public function exists($entity_id, $name)
{
return $this->db
->table($this->getTable())
->eq($this->getEntityKey(), $entity_id)
->eq('name', $name)
->exists();
}
/**
* Update or insert new metadata
*
* @access public
* @param integer $entity_id
* @param array $values
* @return boolean
*/
public function save($entity_id, array $values)
{
$results = array();
$user_id = $this->userSession->getId();
$timestamp = time();
$this->db->startTransaction();
foreach ($values as $key => $value) {
if ($this->exists($entity_id, $key)) {
$results[] = $this->db->table($this->getTable())
->eq($this->getEntityKey(), $entity_id)
->eq('name', $key)
->update(array(
'value' => $value,
'changed_on' => $timestamp,
'changed_by' => $user_id,
));
} else {
$results[] = $this->db->table($this->getTable())->insert(array(
'name' => $key,
'value' => $value,
$this->getEntityKey() => $entity_id,
'changed_on' => $timestamp,
'changed_by' => $user_id,
));
}
}
$this->db->closeTransaction();
return ! in_array(false, $results, true);
}
/**
* Remove a metadata
*
* @access public
* @param integer $entity_id
* @param string $name
* @return bool
*/
public function remove($entity_id, $name)
{
return $this->db->table($this->getTable())
->eq($this->getEntityKey(), $entity_id)
->eq('name', $name)
->remove();
}
}
+100
View File
@@ -0,0 +1,100 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
use Kanboard\EventBuilder\CommentEventBuilder;
use Kanboard\EventBuilder\EventIteratorBuilder;
use Kanboard\EventBuilder\SubtaskEventBuilder;
use Kanboard\EventBuilder\TaskEventBuilder;
use Kanboard\EventBuilder\TaskFileEventBuilder;
use Kanboard\EventBuilder\TaskLinkEventBuilder;
/**
* Notification Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class NotificationModel extends Base
{
/**
* Get the event title with author
*
* @access public
* @param string $eventAuthor
* @param string $eventName
* @param array $eventData
* @return string
*/
public function getTitleWithAuthor($eventAuthor, $eventName, array $eventData)
{
foreach ($this->getIteratorBuilder() as $builder) {
$title = $builder->buildTitleWithAuthor($eventAuthor, $eventName, $eventData);
if ($title !== '') {
return $title;
}
}
return e('Notification');
}
/**
* Get the event title without author
*
* @access public
* @param string $eventName
* @param array $eventData
* @return string
*/
public function getTitleWithoutAuthor($eventName, array $eventData)
{
foreach ($this->getIteratorBuilder() as $builder) {
$title = $builder->buildTitleWithoutAuthor($eventName, $eventData);
if ($title !== '') {
return $title;
}
}
return e('Notification');
}
/**
* Get task id from event
*
* @access public
* @param string $eventName
* @param array $eventData
* @return integer
*/
public function getTaskIdFromEvent($eventName, array $eventData)
{
if ($eventName === TaskModel::EVENT_OVERDUE) {
return $eventData['tasks'][0]['id'];
}
return isset($eventData['task']['id']) ? $eventData['task']['id'] : 0;
}
/**
* Get iterator builder
*
* @access protected
* @return EventIteratorBuilder
*/
protected function getIteratorBuilder()
{
$iterator = new EventIteratorBuilder();
$iterator
->withBuilder(TaskEventBuilder::getInstance($this->container))
->withBuilder(CommentEventBuilder::getInstance($this->container))
->withBuilder(SubtaskEventBuilder::getInstance($this->container))
->withBuilder(TaskFileEventBuilder::getInstance($this->container))
->withBuilder(TaskLinkEventBuilder::getInstance($this->container))
;
return $iterator;
}
}
+128
View File
@@ -0,0 +1,128 @@
<?php
namespace Kanboard\Model;
use Pimple\Container;
use Kanboard\Core\Base;
/**
* Notification Type
*
* @package model
* @author Frederic Guillot
*/
abstract class NotificationTypeModel extends Base
{
/**
* Container
*
* @access private
* @var \Pimple\Container
*/
private $classes;
/**
* Notification type labels
*
* @access private
* @var array
*/
private $labels = array();
/**
* Hidden notification types
*
* @access private
* @var array
*/
private $hiddens = array();
/**
* Constructor
*
* @access public
* @param \Pimple\Container $container
*/
public function __construct(Container $container)
{
parent::__construct($container);
$this->classes = new Container;
}
/**
* Add a new notification type
*
* @access public
* @param string $type
* @param string $label
* @param string $class
* @param boolean $hidden
* @return NotificationTypeModel
*/
public function setType($type, $label, $class, $hidden = false)
{
$container = $this->container;
if ($hidden) {
$this->hiddens[] = $type;
} else {
$this->labels[$type] = $label;
}
$this->classes[$type] = function () use ($class, $container) {
return new $class($container);
};
return $this;
}
/**
* Get mail notification type instance
*
* @access public
* @param string $type
* @return \Kanboard\Core\Notification\NotificationInterface
*/
public function getType($type)
{
return $this->classes[$type];
}
/**
* Get all notification types with labels
*
* @access public
* @return array
*/
public function getTypes()
{
return $this->labels;
}
/**
* Get all hidden notification types
*
* @access public
* @return array
*/
public function getHiddenTypes()
{
return $this->hiddens;
}
/**
* Keep only loaded notification types
*
* @access public
* @param string[] $types
* @return array
*/
public function filterTypes(array $types)
{
$classes = $this->classes;
return array_filter($types, function ($type) use ($classes) {
return isset($classes[$type]);
});
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Password Reset Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class PasswordResetModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'password_reset';
/**
* Token duration (30 minutes)
*
* @var integer
*/
const DURATION = 1800;
/**
* Get all tokens
*
* @access public
* @param integer $user_id
* @return array
*/
public function getAll($user_id)
{
return $this->db->table(self::TABLE)->eq('user_id', $user_id)->desc('date_creation')->limit(100)->findAll();
}
/**
* Generate a new reset token for a user
*
* @access public
* @param string $username
* @param integer $expiration
* @return boolean|string
*/
public function create($username, $expiration = 0)
{
$user_id = $this->db->table(UserModel::TABLE)->eq('username', $username)->neq('email', '')->notNull('email')->findOneColumn('id');
if (! $user_id) {
return false;
}
$token = $this->token->getToken();
$result = $this->db->table(self::TABLE)->insert(array(
'token' => $token,
'user_id' => $user_id,
'date_expiration' => $expiration ?: time() + self::DURATION,
'date_creation' => time(),
'ip' => $this->request->getIpAddress(),
'user_agent' => $this->request->getUserAgent(),
'is_active' => 1,
));
return $result ? $token : false;
}
/**
* Get user id from the token
*
* @access public
* @param string $token
* @return integer
*/
public function getUserIdByToken($token)
{
return $this->db->table(self::TABLE)->eq('token', $token)->eq('is_active', 1)->gte('date_expiration', time())->findOneColumn('user_id');
}
/**
* Disable all tokens for a user
*
* @access public
* @param integer $user_id
* @return boolean
*/
public function disable($user_id)
{
return $this->db->table(self::TABLE)->eq('user_id', $user_id)->update(array('is_active' => 0));
}
}
@@ -0,0 +1,52 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
class PredefinedTaskDescriptionModel extends Base
{
const TABLE = 'predefined_task_descriptions';
public function getAll($projectId)
{
return $this->db->table(self::TABLE)->eq('project_id', $projectId)->findAll();
}
public function getList($projectId)
{
return array('' => t('None')) + $this->db->hashtable(self::TABLE)->eq('project_id', $projectId)->getAll('id', 'title');
}
public function getById($projectId, $id)
{
return $this->db->table(self::TABLE)->eq('project_id', $projectId)->eq('id', $id)->findOne();
}
public function getDescriptionById($projectId, $id)
{
return $this->db->table(self::TABLE)->eq('project_id', $projectId)->eq('id', $id)->findOneColumn('description');
}
public function create($projectId, $title, $description)
{
return $this->db->table(self::TABLE)->persist(array(
'project_id' => $projectId,
'title' => $title,
'description' => $description,
));
}
public function update($projectId, $id, $title, $description)
{
return $this->db->table(self::TABLE)->eq('project_id', $projectId)->eq('id', $id)->update(array(
'title' => $title,
'description' => $description,
));
}
public function remove($projectId, $id)
{
return $this->db->table(self::TABLE)->eq('project_id', $projectId)->eq('id', $id)->remove();
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
use PicoDb\Table;
/**
* Project activity model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ProjectActivityModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'project_activities';
/**
* Add a new event for the project
*
* @access public
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param integer $creator_id User id
* @param string $event_name Event name
* @param array $data Event data (will be serialized)
* @return boolean
*/
public function createEvent($project_id, $task_id, $creator_id, $event_name, array $data)
{
return $this->db->table(self::TABLE)->insert(array(
'project_id' => $project_id,
'task_id' => $task_id,
'creator_id' => $creator_id,
'event_name' => $event_name,
'date_creation' => time(),
'data' => json_encode($data),
));
}
/**
* Get query
*
* @access public
* @return Table
*/
public function getQuery()
{
return $this
->db
->table(ProjectActivityModel::TABLE)
->columns(
ProjectActivityModel::TABLE.'.*',
'uc.username AS author_username',
'uc.name AS author_name',
'uc.email',
'uc.avatar_path'
)
->join(TaskModel::TABLE, 'id', 'task_id')
->join(ProjectModel::TABLE, 'id', 'project_id')
->left(UserModel::TABLE, 'uc', 'id', ProjectActivityModel::TABLE, 'creator_id');
}
/**
* Remove old event entries to avoid large table
*
* @access public
* @param integer $ts Timestamp
*/
public function cleanup($ts)
{
$this->db->table(self::TABLE)->lt('date_creation', $ts)->remove();
}
}
+254
View File
@@ -0,0 +1,254 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Project Daily Column Stats
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ProjectDailyColumnStatsModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'project_daily_column_stats';
/**
* Update daily totals for the project and for each column
*
* "total" is the number open of tasks in the column
* "score" is the sum of tasks score in the column
*
* @access public
* @param integer $project_id Project id
* @param string $date Record date (YYYY-MM-DD)
* @return boolean
*/
public function updateTotals($project_id, $date)
{
$this->db->startTransaction();
$this->db->table(self::TABLE)->eq('project_id', $project_id)->eq('day', $date)->remove();
foreach ($this->getStatsByColumns($project_id) as $column_id => $column) {
$this->db->table(self::TABLE)->insert(array(
'day' => $date,
'project_id' => $project_id,
'column_id' => $column_id,
'total' => $column['total'],
'score' => $column['score'],
));
}
$this->db->closeTransaction();
return true;
}
/**
* Count the number of recorded days for the data range
*
* @access public
* @param integer $project_id Project id
* @param string $from Start date (ISO format YYYY-MM-DD)
* @param string $to End date
* @return integer
*/
public function countDays($project_id, $from, $to)
{
return $this->db->table(self::TABLE)
->eq('project_id', $project_id)
->gte('day', $from)
->lte('day', $to)
->findOneColumn('COUNT(DISTINCT day)');
}
/**
* Get aggregated metrics for the project within a data range
*
* [
* ['Date', 'Column1', 'Column2'],
* ['2014-11-16', 2, 5],
* ['2014-11-17', 20, 15],
* ]
*
* @access public
* @param integer $project_id Project id
* @param string $from Start date (ISO format YYYY-MM-DD)
* @param string $to End date
* @param string $field Column to aggregate
* @return array
*/
public function getAggregatedMetrics($project_id, $from, $to, $field = 'total')
{
$columns = $this->columnModel->getList($project_id);
$metrics = $this->getMetrics($project_id, $from, $to);
return $this->buildAggregate($metrics, $columns, $field);
}
/**
* Fetch metrics
*
* @access public
* @param integer $project_id Project id
* @param string $from Start date (ISO format YYYY-MM-DD)
* @param string $to End date
* @return array
*/
public function getMetrics($project_id, $from, $to)
{
return $this->db->table(self::TABLE)
->eq('project_id', $project_id)
->gte('day', $from)
->lte('day', $to)
->asc(self::TABLE.'.day')
->findAll();
}
/**
* Build aggregate
*
* @access private
* @param array $metrics
* @param array $columns
* @param string $field
* @return array
*/
private function buildAggregate(array &$metrics, array &$columns, $field)
{
$column_ids = array_keys($columns);
$days = array_unique(array_column($metrics, 'day'));
$rows = array(array_merge(array(e('Date')), array_values($columns)));
foreach ($days as $day) {
$rows[] = $this->buildRowAggregate($metrics, $column_ids, $day, $field);
}
return $rows;
}
/**
* Build one row of the aggregate
*
* @access private
* @param array $metrics
* @param array $column_ids
* @param string $day
* @param string $field
* @return array
*/
private function buildRowAggregate(array &$metrics, array &$column_ids, $day, $field)
{
$row = array($day);
foreach ($column_ids as $column_id) {
$row[] = $this->findValueInMetrics($metrics, $day, $column_id, $field);
}
return $row;
}
/**
* Find the value in the metrics
*
* @access private
* @param array $metrics
* @param string $day
* @param string $column_id
* @param string $field
* @return integer
*/
private function findValueInMetrics(array &$metrics, $day, $column_id, $field)
{
foreach ($metrics as $metric) {
if ($metric['day'] === $day && $metric['column_id'] == $column_id) {
return (int) $metric[$field];
}
}
return 0;
}
/**
* Get number of tasks and score by columns
*
* @access private
* @param integer $project_id
* @return array
*/
private function getStatsByColumns($project_id)
{
$totals = $this->getTotalByColumns($project_id);
$scores = $this->getScoreByColumns($project_id);
$columns = array();
foreach ($totals as $column_id => $total) {
$columns[$column_id] = array('total' => $total, 'score' => 0);
}
foreach ($scores as $column_id => $score) {
$columns[$column_id]['score'] = (int) $score;
}
return $columns;
}
/**
* Get number of tasks and score by columns
*
* @access private
* @param integer $project_id
* @return array
*/
private function getScoreByColumns($project_id)
{
$stats = $this->db->table(TaskModel::TABLE)
->columns('column_id', 'SUM(score) AS score')
->eq('project_id', $project_id)
->eq('is_active', TaskModel::STATUS_OPEN)
->notNull('score')
->groupBy('column_id')
->findAll();
return array_column($stats, 'score', 'column_id');
}
/**
* Get number of tasks and score by columns
*
* @access private
* @param integer $project_id
* @return array
*/
private function getTotalByColumns($project_id)
{
$stats = $this->db->table(TaskModel::TABLE)
->columns('column_id', 'COUNT(*) AS total')
->eq('project_id', $project_id)
->in('is_active', $this->getTaskStatusConfig())
->groupBy('column_id')
->findAll();
return array_column($stats, 'total', 'column_id');
}
/**
* Get task status to use for total calculation
*
* @access private
* @return array
*/
private function getTaskStatusConfig()
{
if ($this->configModel->get('cfd_include_closed_tasks') == 1) {
return array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED);
}
return array(TaskModel::STATUS_OPEN);
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Project Daily Stats
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ProjectDailyStatsModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'project_daily_stats';
/**
* Update daily totals for the project
*
* @access public
* @param integer $project_id Project id
* @param string $date Record date (YYYY-MM-DD)
* @return boolean
*/
public function updateTotals($project_id, $date)
{
$this->db->startTransaction();
$lead_cycle_time = $this->averageLeadCycleTimeAnalytic->build($project_id);
$this->db->table(self::TABLE)->eq('day', $date)->eq('project_id', $project_id)->remove();
$this->db->table(self::TABLE)->insert(array(
'day' => $date,
'project_id' => $project_id,
'avg_lead_time' => $lead_cycle_time['avg_lead_time'],
'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'],
));
$this->db->closeTransaction();
return true;
}
/**
* Get raw metrics for the project within a data range
*
* @access public
* @param integer $project_id Project id
* @param string $from Start date (ISO format YYYY-MM-DD)
* @param string $to End date
* @return array
*/
public function getRawMetrics($project_id, $from, $to)
{
$metrics = $this->db->table(self::TABLE)
->columns('day', 'avg_lead_time', 'avg_cycle_time')
->eq('project_id', $project_id)
->gte('day', $from)
->lte('day', $to)
->asc('day')
->findAll();
foreach ($metrics as &$metric) {
$metric['avg_lead_time'] = (int) $metric['avg_lead_time'];
$metric['avg_cycle_time'] = (int) $metric['avg_cycle_time'];
}
return $metrics;
}
}
+190
View File
@@ -0,0 +1,190 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
use Kanboard\Core\Security\Role;
/**
* Project Duplication
*
* @package Kanboard\Model
* @author Frederic Guillot
* @author Antonio Rabelo
*/
class ProjectDuplicationModel extends Base
{
/**
* Get list of optional models to duplicate
*
* @access public
* @return string[]
*/
public function getOptionalSelection()
{
return array(
'categoryModel',
'projectRoleModel',
'projectPermissionModel',
'actionModel',
'tagDuplicationModel',
'customFilterModel',
'projectMetadataModel',
'projectTaskDuplicationModel',
);
}
/**
* Get list of all possible models to duplicate
*
* @access public
* @return string[]
*/
public function getPossibleSelection()
{
return array(
'swimlaneModel',
'boardModel',
'categoryModel',
'projectRoleModel',
'projectPermissionModel',
'actionModel',
'swimlaneModel',
'tagDuplicationModel',
'customFilterModel',
'projectMetadataModel',
'projectTaskDuplicationModel',
);
}
/**
* Get a valid project name for the duplication
*
* @access public
* @param string $name Project name
* @param integer $max_length Max length allowed
* @return string
*/
public function getClonedProjectName($name, $max_length = 50)
{
$suffix = ' ('.t('Clone').')';
if (strlen($name.$suffix) > $max_length) {
$name = substr($name, 0, $max_length - strlen($suffix));
}
return $name.$suffix;
}
/**
* Clone a project with all settings
*
* @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
* @param string $identifier Identifier of the project
* @return integer Cloned Project Id
*/
public function duplicate($src_project_id, $selection = array('projectPermissionModel', 'categoryModel', 'actionModel'), $owner_id = 0, $name = null, $private = null, $identifier = null)
{
$this->db->startTransaction();
// Get the cloned project Id
$dst_project_id = $this->copy($src_project_id, $owner_id, $name, $private, $identifier);
if ($dst_project_id === false) {
$this->db->cancelTransaction();
return false;
}
// Clone Swimlanes, Columns, Categories, Permissions and Actions
foreach ($this->getPossibleSelection() as $model) {
// Skip if optional part has not been selected
if (in_array($model, $this->getOptionalSelection()) && ! in_array($model, $selection)) {
continue;
}
// Skip permissions for private projects
if ($private && $model === 'projectPermissionModel') {
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();
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
* @param string $identifier
* @return integer
*/
private function copy($src_project_id, $owner_id = 0, $name = null, $private = null, $identifier = null)
{
$project = $this->projectModel->getById($src_project_id);
$is_private = empty($project['is_private']) ? 0 : 1;
if (! empty($identifier)) {
$identifier = strtoupper($identifier);
}
$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,
'priority_default' => $project['priority_default'],
'priority_start' => $project['priority_start'],
'priority_end' => $project['priority_end'],
'per_swimlane_task_limits' => empty($project['per_swimlane_task_limits']) ? 0 : 1,
'task_limit' => $project['task_limit'],
'identifier' => $identifier,
);
return $this->db->table(ProjectModel::TABLE)->persist($values);
}
/**
* Make sure that the creator of the duplicated project is also 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->projectUserRoleModel->removeUser($dst_project_id, $owner_id);
if (! $this->projectUserRoleModel->addUser($dst_project_id, $owner_id, Role::PROJECT_MANAGER)) {
return false;
}
}
return true;
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
namespace Kanboard\Model;
/**
* Project File Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ProjectFileModel extends FileModel
{
/**
* Table name
*
* @var string
*/
const TABLE = 'project_has_files';
/**
* Events
*
* @var string
*/
const EVENT_CREATE = 'project.file.create';
const EVENT_DESTROY = 'project.file.destroy';
/**
* Get the table
*
* @abstract
* @access protected
* @return string
*/
protected function getTable()
{
return self::TABLE;
}
/**
* Define the foreign key
*
* @abstract
* @access protected
* @return string
*/
protected function getForeignKey()
{
return 'project_id';
}
/**
* Define the path prefix
*
* @abstract
* @access protected
* @return string
*/
protected function getPathPrefix()
{
return 'projects';
}
/**
* Fire file creation event
*
* @access protected
* @param integer $file_id
*/
protected function fireCreationEvent($file_id)
{
$this->queueManager->push($this->projectFileEventJob->withParams($file_id, self::EVENT_CREATE));
}
/**
* Fire file destruction event
*
* @access protected
* @param integer $file_id
*/
protected function fireDestructionEvent($file_id)
{
$this->queueManager->push($this->projectFileEventJob->withParams($file_id, self::EVENT_DESTROY));
}
}
+197
View File
@@ -0,0 +1,197 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
use Kanboard\Core\Security\Role;
/**
* Project Group Role
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ProjectGroupRoleModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'project_has_groups';
/**
* Get the list of project visible by the given user according to groups
*
* @access public
* @param integer $user_id
* @param array $status
* @return array
*/
public function getProjectsByUser($user_id, $status = array(ProjectModel::ACTIVE, ProjectModel::INACTIVE))
{
return $this->db
->hashtable(ProjectModel::TABLE)
->join(self::TABLE, 'project_id', 'id')
->join(GroupMemberModel::TABLE, 'group_id', 'group_id', self::TABLE)
->eq(GroupMemberModel::TABLE.'.user_id', $user_id)
->in(ProjectModel::TABLE.'.is_active', $status)
->getAll(ProjectModel::TABLE.'.id', ProjectModel::TABLE.'.name');
}
/**
* For a given project get the role of the specified user
*
* @access public
* @param integer $project_id
* @param integer $user_id
* @return string
*/
public function getUserRole($project_id, $user_id)
{
$roles = $this->db->table(self::TABLE)
->join(GroupMemberModel::TABLE, 'group_id', 'group_id', self::TABLE)
->eq(GroupMemberModel::TABLE.'.user_id', $user_id)
->eq(self::TABLE.'.project_id', $project_id)
->findAllByColumn('role');
return $this->projectAccessMap->getHighestRole($roles);
}
/**
* Get all groups associated directly to the project
*
* @access public
* @param integer $project_id
* @return array
*/
public function getGroups($project_id)
{
return $this->db->table(self::TABLE)
->columns(GroupModel::TABLE.'.id', GroupModel::TABLE.'.name', self::TABLE.'.role')
->join(GroupModel::TABLE, 'id', 'group_id')
->eq('project_id', $project_id)
->asc('name')
->findAll();
}
/**
* From groups get all users associated to the project
*
* @access public
* @param integer $project_id
* @return array
*/
public function getUsers($project_id)
{
return $this->db->table(self::TABLE)
->columns(
UserModel::TABLE.'.id',
UserModel::TABLE.'.username',
UserModel::TABLE.'.name',
UserModel::TABLE.'.email',
self::TABLE.'.role'
)
->join(GroupMemberModel::TABLE, 'group_id', 'group_id', self::TABLE)
->join(UserModel::TABLE, 'id', 'user_id', GroupMemberModel::TABLE)
->eq(self::TABLE.'.project_id', $project_id)
->asc(UserModel::TABLE.'.username')
->findAll();
}
/**
* From groups get all users assignable to tasks
*
* @access public
* @param integer $project_id
* @return array
*/
public function getAssignableUsers($project_id)
{
return $this->db->table(UserModel::TABLE)
->columns(UserModel::TABLE.'.id', UserModel::TABLE.'.username', UserModel::TABLE.'.name')
->join(GroupMemberModel::TABLE, 'user_id', 'id', UserModel::TABLE)
->join(self::TABLE, 'group_id', 'group_id', GroupMemberModel::TABLE)
->eq(self::TABLE.'.project_id', $project_id)
->eq(UserModel::TABLE.'.is_active', 1)
->neq(self::TABLE.'.role', Role::PROJECT_VIEWER)
->asc(UserModel::TABLE.'.username')
->findAll();
}
/**
* Add a group to the project
*
* @access public
* @param integer $project_id
* @param integer $group_id
* @param string $role
* @return boolean
*/
public function addGroup($project_id, $group_id, $role)
{
return $this->db->table(self::TABLE)->insert(array(
'group_id' => $group_id,
'project_id' => $project_id,
'role' => $role,
));
}
/**
* Remove a group from the project
*
* @access public
* @param integer $project_id
* @param integer $group_id
* @return boolean
*/
public function removeGroup($project_id, $group_id)
{
return $this->db->table(self::TABLE)->eq('group_id', $group_id)->eq('project_id', $project_id)->remove();
}
/**
* Change a group role for the project
*
* @access public
* @param integer $project_id
* @param integer $group_id
* @param string $role
* @return boolean
*/
public function changeGroupRole($project_id, $group_id, $role)
{
return $this->db->table(self::TABLE)
->eq('group_id', $group_id)
->eq('project_id', $project_id)
->update(array(
'role' => $role,
));
}
/**
* Copy group access from a project to another one
*
* @param integer $project_src_id Project Template
* @param integer $project_dst_id Project that receives the copy
* @return boolean
*/
public function duplicate($project_src_id, $project_dst_id)
{
$rows = $this->db->table(self::TABLE)->eq('project_id', $project_src_id)->findAll();
foreach ($rows as $row) {
$result = $this->db->table(self::TABLE)->save(array(
'project_id' => $project_dst_id,
'group_id' => $row['group_id'],
'role' => $row['role'],
));
if (! $result) {
return false;
}
}
return true;
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace Kanboard\Model;
/**
* Project Metadata
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ProjectMetadataModel extends MetadataModel
{
/**
* Get the table
*
* @abstract
* @access protected
* @return string
*/
protected function getTable()
{
return 'project_has_metadata';
}
/**
* Define the entity key
*
* @access protected
* @return string
*/
protected function getEntityKey()
{
return 'project_id';
}
/**
* Helper method to duplicate all metadata 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)
{
$metadata = $this->getAll($src_project_id);
if (! $this->save($dst_project_id, $metadata)) {
return false;
}
return true;
}
}
+611
View File
@@ -0,0 +1,611 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
use Kanboard\Model\TaskModel;
use Kanboard\Model\TaskFileModel;
/**
* Project model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ProjectModel extends Base
{
/**
* SQL table name for projects
*
* @var string
*/
const TABLE = 'projects';
/**
* Value for active project
*
* @var integer
*/
const ACTIVE = 1;
/**
* Value for inactive project
*
* @var integer
*/
const INACTIVE = 0;
/**
* Value for private project
*
* @var integer
*/
const TYPE_PRIVATE = 1;
/**
* Value for team project
*
* @var integer
*/
const TYPE_TEAM = 0;
/**
* Get a project by the id
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getById($project_id)
{
return $this->db->table(self::TABLE)->eq('id', $project_id)->findOne();
}
/**
* Get a project by id with owner name
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getByIdWithOwner($project_id)
{
return $this->db->table(self::TABLE)
->columns(self::TABLE.'.*', UserModel::TABLE.'.username AS owner_username', UserModel::TABLE.'.name AS owner_name')
->eq(self::TABLE.'.id', $project_id)
->join(UserModel::TABLE, 'id', 'owner_id')
->findOne();
}
/**
* Get a project by id with owner name and task count
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getByIdWithOwnerAndTaskCount($project_id)
{
return $this->db->table(self::TABLE)
->columns(
self::TABLE.'.*',
UserModel::TABLE.'.username AS owner_username',
UserModel::TABLE.'.name AS owner_name',
"(SELECT count(*) FROM tasks WHERE tasks.project_id=projects.id AND tasks.is_active='1') AS nb_active_tasks"
)
->eq(self::TABLE.'.id', $project_id)
->join(UserModel::TABLE, 'id', 'owner_id')
->join(TaskModel::TABLE, 'project_id', 'id')
->findOne();
}
/**
* Get a project by the name
*
* @access public
* @param string $name Project name
* @return array
*/
public function getByName($name)
{
return $this->db->table(self::TABLE)->eq('name', $name)->findOne();
}
/**
* Get a project by the identifier (code)
*
* @access public
* @param string $identifier
* @return array|boolean
*/
public function getByIdentifier($identifier)
{
if (empty($identifier)) {
return false;
}
return $this->db->table(self::TABLE)->eq('identifier', strtoupper($identifier))->findOne();
}
/**
* Get a project by the email address
*
* @access public
* @param string $email
* @return array|boolean
*/
public function getByEmail($email)
{
if (empty($email)) {
return false;
}
return $this->db->table(self::TABLE)->eq('email', $email)->findOne();
}
/**
* Fetch project data by using the token
*
* @access public
* @param string $token Token
* @return array|boolean
*/
public function getByToken($token)
{
if (empty($token)) {
return false;
}
return $this->db->table(self::TABLE)->eq('token', $token)->eq('is_public', 1)->findOne();
}
/**
* Return the first project from the database (no sorting)
*
* @access public
* @return array
*/
public function getFirst()
{
return $this->db->table(self::TABLE)->findOne();
}
/**
* Return true if the project is private
*
* @access public
* @param integer $project_id Project id
* @return boolean
*/
public function isPrivate($project_id)
{
return $this->db->table(self::TABLE)->eq('id', $project_id)->eq('is_private', 1)->exists();
}
/**
* Get all projects
*
* @access public
* @return array
*/
public function getAll()
{
return $this->db->table(self::TABLE)->asc('name')->findAll();
}
/**
* Get all projects with given Ids
*
* @access public
* @param integer[] $project_ids
* @return array
*/
public function getAllByIds(array $project_ids)
{
if (empty($project_ids)) {
return array();
}
return $this->db->table(self::TABLE)->in('id', $project_ids)->asc('name')->findAll();
}
/**
* Get all project ids
*
* @access public
* @return array
*/
public function getAllIds()
{
return $this->db->table(self::TABLE)->asc('name')->findAllByColumn('id');
}
/**
* Return the list of all projects
*
* @access public
* @param bool $prependNone
* @param bool $noPrivateProjects
* @return array
*/
public function getList($prependNone = true, $noPrivateProjects = true)
{
if ($noPrivateProjects) {
$projects = $this->db->hashtable(self::TABLE)->eq('is_private', 0)->asc('name')->getAll('id', 'name');
} else {
$projects = $this->db->hashtable(self::TABLE)->asc('name')->getAll('id', 'name');
}
if ($prependNone) {
return array(t('None')) + $projects;
}
return $projects;
}
/**
* Get all projects with all its data for a given status
*
* @access public
* @param integer $status Project status: self::ACTIVE or self:INACTIVE
* @return array
*/
public function getAllByStatus($status)
{
return $this->db
->table(self::TABLE)
->asc('name')
->eq('is_active', $status)
->findAll();
}
/**
* Get a list of project by status
*
* @access public
* @param integer $status Project status: self::ACTIVE or self:INACTIVE
* @return array
*/
public function getListByStatus($status)
{
return $this->db
->hashtable(self::TABLE)
->asc('name')
->eq('is_active', $status)
->getAll('id', 'name');
}
/**
* Return the number of projects by status
*
* @access public
* @param integer $status Status: self::ACTIVE or self:INACTIVE
* @return integer
*/
public function countByStatus($status)
{
return $this->db
->table(self::TABLE)
->eq('is_active', $status)
->count();
}
/**
* Get stats for each column of a project
*
* @access public
* @param array $project
* @return array
*/
public function getColumnStats(array &$project)
{
$project['columns'] = $this->columnModel->getAllWithTaskCount($project['id']);
$project['nb_active_tasks'] = 0;
foreach ($project['columns'] as $column) {
$project['nb_active_tasks'] += $column['nb_open_tasks'];
}
return $project;
}
/**
* Apply column stats to a collection of projects (filter callback)
*
* @access public
* @param array $projects
* @return array
*/
public function applyColumnStats(array $projects)
{
foreach ($projects as &$project) {
$this->getColumnStats($project);
}
return $projects;
}
/**
* Get project summary for a list of project
*
* @access public
* @param array $project_ids List of project id
* @return \PicoDb\Table
*/
public function getQueryColumnStats(array $project_ids)
{
if (empty($project_ids)) {
return $this->db->table(ProjectModel::TABLE)->eq(ProjectModel::TABLE.'.id', 0);
}
return $this->db
->table(ProjectModel::TABLE)
->columns(self::TABLE.'.*', UserModel::TABLE.'.username AS owner_username', UserModel::TABLE.'.name AS owner_name')
->join(UserModel::TABLE, 'id', 'owner_id')
->in(self::TABLE.'.id', $project_ids)
->callback(array($this, 'applyColumnStats'));
}
/**
* Get query for list of project without column statistics
*
* @access public
* @param array $projectIds
* @return \PicoDb\Table
*/
public function getQueryByProjectIds(array $projectIds)
{
if (empty($projectIds)) {
return $this->db->table(ProjectModel::TABLE)->eq(ProjectModel::TABLE.'.id', 0);
}
return $this->db
->table(ProjectModel::TABLE)
->columns(self::TABLE.'.*', UserModel::TABLE.'.username AS owner_username', UserModel::TABLE.'.name AS owner_name')
->join(UserModel::TABLE, 'id', 'owner_id')
->in(self::TABLE.'.id', $projectIds);
}
/**
* Create a project
*
* @access public
* @param array $values Form values
* @param integer $userId User who creates the project
* @param bool $addUser Whether to automatically add the user to the project
* @return int Project id
*/
public function create(array $values, $userId = 0, $addUser = false)
{
if (! empty($userId) && ! $this->userModel->exists($userId)) {
return false;
}
$this->db->startTransaction();
$values['token'] = '';
$values['last_modified'] = time();
$values['is_private'] = empty($values['is_private']) ? 0 : 1;
$values['owner_id'] = $userId;
if (! empty($values['identifier'])) {
$values['identifier'] = strtoupper($values['identifier']);
}
$this->helper->model->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end', 'task_limit'));
if (! $this->db->table(self::TABLE)->save($values)) {
$this->db->cancelTransaction();
return false;
}
$project_id = $this->db->getLastId();
if (! $this->boardModel->create($project_id, $this->boardModel->getUserColumns())) {
$this->db->cancelTransaction();
return false;
}
if (! $this->swimlaneModel->create($project_id, t('Default swimlane'))) {
$this->db->cancelTransaction();
return false;
}
if ($addUser && $userId) {
$this->projectUserRoleModel->addUser($project_id, $userId, Role::PROJECT_MANAGER);
}
$this->categoryModel->createDefaultCategories($project_id);
$this->db->closeTransaction();
return (int) $project_id;
}
/**
* Check if the project have been modified
*
* @access public
* @param integer $project_id Project id
* @param integer $timestamp Timestamp
* @return bool
*/
public function isModifiedSince($project_id, $timestamp)
{
return (bool) $this->db->table(self::TABLE)
->eq('id', $project_id)
->gt('last_modified', $timestamp)
->count();
}
/**
* Update modification date
*
* @access public
* @param integer $project_id Project id
* @return bool
*/
public function updateModificationDate($project_id)
{
return $this->db->table(self::TABLE)->eq('id', $project_id)->update(array(
'last_modified' => time()
));
}
/**
* Update a project
*
* @access public
* @param array $values Form values
* @return bool
*/
public function update(array $values)
{
if (! empty($values['identifier'])) {
$values['identifier'] = strtoupper($values['identifier']);
}
if (! empty($values['start_date'])) {
$values['start_date'] = $this->dateParser->getIsoDate($values['start_date']);
}
if (! empty($values['end_date'])) {
$values['end_date'] = $this->dateParser->getIsoDate($values['end_date']);
}
if (! empty($values['owner_id']) && ! $this->userModel->exists($values['owner_id'])) {
return false;
}
$values['per_swimlane_task_limits'] = empty($values['per_swimlane_task_limits']) ? 0 : 1;
$this->helper->model->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end', 'task_limit'));
$updates = $values;
unset($updates['id']);
return $this->exists($values['id']) &&
$this->db->table(self::TABLE)->eq('id', $values['id'])->save($updates);
}
/**
* Remove a project
*
* @access public
* @param integer $project_id Project id
* @return bool
*/
public function remove($project_id)
{
// Remove all project attachments
$this->projectFileModel->removeAll($project_id);
// Remove all task attachments
$file_ids = $this->db
->table(TaskFileModel::TABLE)
->eq(TaskModel::TABLE.'.project_id', $project_id)
->join(TaskModel::TABLE, 'id', 'task_id', TaskFileModel::TABLE)
->findAllByColumn(TaskFileModel::TABLE.'.id');
foreach ($file_ids as $file_id) {
$this->taskFileModel->remove($file_id);
}
// Remove project
$this->db->table(TagModel::TABLE)->eq('project_id', $project_id)->remove();
return $this->db->table(self::TABLE)->eq('id', $project_id)->remove();
}
/**
* Return true if the project exists
*
* @access public
* @param integer $project_id Project id
* @return boolean
*/
public function exists($project_id)
{
return $this->db->table(self::TABLE)->eq('id', $project_id)->exists();
}
/**
* Enable a project
*
* @access public
* @param integer $project_id Project id
* @return bool
*/
public function enable($project_id)
{
return $this->exists($project_id) &&
$this->db
->table(self::TABLE)
->eq('id', $project_id)
->update(array('is_active' => 1));
}
/**
* Disable a project
*
* @access public
* @param integer $project_id Project id
* @return bool
*/
public function disable($project_id)
{
return $this->exists($project_id) &&
$this->db
->table(self::TABLE)
->eq('id', $project_id)
->update(array('is_active' => 0));
}
/**
* Enable public access for a project
*
* @access public
* @param integer $project_id Project id
* @return bool
*/
public function enablePublicAccess($project_id)
{
return $this->exists($project_id) &&
$this->db
->table(self::TABLE)
->eq('id', $project_id)
->save(array('is_public' => 1, 'token' => Token::getToken()));
}
/**
* Disable public access for a project
*
* @access public
* @param integer $project_id Project id
* @return bool
*/
public function disablePublicAccess($project_id)
{
return $this->exists($project_id) &&
$this->db
->table(self::TABLE)
->eq('id', $project_id)
->save(array('is_public' => 0, 'token' => ''));
}
/**
* Change usage of global tags
*
* @param integer $project_id Project id
* @param bool $global_tags New global tag value
* @return bool
*/
public function changeGlobalTagUsage($project_id, $global_tags)
{
return $this->exists($project_id) &&
$this->db
->table(self::TABLE)
->eq('id', $project_id)
->save(array('enable_global_tags' => $global_tags));
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Project Notification
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ProjectNotificationModel extends Base
{
/**
* Send notifications
*
* @access public
* @param integer $project_id
* @param string $event_name
* @param array $event_data
*/
public function sendNotifications($project_id, $event_name, array $event_data)
{
$project = $this->projectModel->getById($project_id);
$types = array_merge(
$this->projectNotificationTypeModel->getHiddenTypes(),
$this->projectNotificationTypeModel->getSelectedTypes($project_id)
);
foreach ($types as $type) {
$this->projectNotificationTypeModel->getType($type)->notifyProject($project, $event_name, $event_data);
}
}
/**
* Save settings for the given project
*
* @access public
* @param integer $project_id
* @param array $values
*/
public function saveSettings($project_id, array $values)
{
$this->db->startTransaction();
$types = empty($values['notification_types']) ? array() : array_keys($values['notification_types']);
$this->projectNotificationTypeModel->saveSelectedTypes($project_id, $types);
$this->db->closeTransaction();
}
/**
* Read user settings to display the form
*
* @access public
* @param integer $project_id
* @return array
*/
public function readSettings($project_id)
{
return array(
'notification_types' => $this->projectNotificationTypeModel->getSelectedTypes($project_id),
);
}
}
@@ -0,0 +1,57 @@
<?php
namespace Kanboard\Model;
/**
* Project Notification Type
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ProjectNotificationTypeModel extends NotificationTypeModel
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'project_has_notification_types';
/**
* Get selected notification types for a given project
*
* @access public
* @param integer $project_id
* @return array
*/
public function getSelectedTypes($project_id)
{
$types = $this->db
->table(self::TABLE)
->eq('project_id', $project_id)
->asc('notification_type')
->findAllByColumn('notification_type');
return $this->filterTypes($types);
}
/**
* Save notification types for a given project
*
* @access public
* @param integer $project_id
* @param string[] $types
* @return boolean
*/
public function saveSelectedTypes($project_id, array $types)
{
$results = array();
$this->db->table(self::TABLE)->eq('project_id', $project_id)->remove();
foreach ($types as $type) {
$results[] = $this->db->table(self::TABLE)->insert(array('project_id' => $project_id, 'notification_type' => $type));
}
return ! in_array(false, $results, true);
}
}
+199
View File
@@ -0,0 +1,199 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
use Kanboard\Core\Security\Role;
use Kanboard\Filter\ProjectGroupRoleProjectFilter;
use Kanboard\Filter\ProjectGroupRoleUsernameFilter;
use Kanboard\Filter\ProjectUserRoleProjectFilter;
use Kanboard\Filter\ProjectUserRoleUsernameFilter;
/**
* Project Permission
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ProjectPermissionModel extends Base
{
/**
* Get query for project users overview
*
* @access public
* @param array $project_ids
* @param string $role
* @return \PicoDb\Table
*/
public function getQueryByRole(array $project_ids, $role)
{
if (empty($project_ids)) {
$project_ids = array(-1);
}
return $this
->db
->table(ProjectUserRoleModel::TABLE)
->join(UserModel::TABLE, 'id', 'user_id')
->join(ProjectModel::TABLE, 'id', 'project_id')
->eq(ProjectUserRoleModel::TABLE.'.role', $role)
->eq(ProjectModel::TABLE.'.is_private', 0)
->in(ProjectModel::TABLE.'.id', $project_ids)
->columns(
UserModel::TABLE.'.id',
UserModel::TABLE.'.username',
UserModel::TABLE.'.name',
ProjectModel::TABLE.'.name AS project_name',
ProjectModel::TABLE.'.id'
);
}
/**
* Get all usernames (fetch users from groups)
*
* @access public
* @param integer $project_id
* @param string $input
* @return array
*/
public function findUsernames($project_id, $input)
{
$userMembers = $this->projectUserRoleQuery
->withFilter(new ProjectUserRoleProjectFilter($project_id))
->withFilter(new ProjectUserRoleUsernameFilter($input))
->getQuery()
->columns(
UserModel::TABLE.'.id',
UserModel::TABLE.'.username',
UserModel::TABLE.'.name',
UserModel::TABLE.'.email',
UserModel::TABLE.'.avatar_path'
)
->findAll();
$groupMembers = $this->projectGroupRoleQuery
->withFilter(new ProjectGroupRoleProjectFilter($project_id))
->withFilter(new ProjectGroupRoleUsernameFilter($input))
->getQuery()
->columns(
UserModel::TABLE.'.id',
UserModel::TABLE.'.username',
UserModel::TABLE.'.name',
UserModel::TABLE.'.email',
UserModel::TABLE.'.avatar_path'
)
->findAll();
$userMembers = array_column_index_unique($userMembers, 'username');
$groupMembers = array_column_index_unique($groupMembers, 'username');
$members = array_merge($userMembers, $groupMembers);
ksort($members);
return $members;
}
public function getMembers($project_id)
{
$userMembers = $this->projectUserRoleModel->getUsers($project_id);
$groupMembers = $this->projectGroupRoleModel->getUsers($project_id);
$userMembers = array_column_index_unique($userMembers, 'username');
$groupMembers = array_column_index_unique($groupMembers, 'username');
return array_merge($userMembers, $groupMembers);
}
public function getMembersWithEmail($project_id)
{
$members = $this->getMembers($project_id);
return array_filter($members, function (array $user) {
return ! empty($user['email']);
});
}
/**
* Return true if the user is allowed to access a project
*
* @param integer $project_id
* @param integer $user_id
* @return boolean
*/
public function isUserAllowed($project_id, $user_id)
{
if ($this->userSession->isAdmin()) {
return true;
}
return $this->userModel->isActive($user_id) &&
$this->isMember($project_id, $user_id);
}
/**
* Return true if the user is assignable
*
* @access public
* @param integer $project_id
* @param integer $user_id
* @return boolean
*/
public function isAssignable($project_id, $user_id)
{
if ($this->userModel->isActive($user_id)) {
$role = $this->projectUserRoleModel->getUserRole($project_id, $user_id);
return ! empty($role) && $role !== Role::PROJECT_VIEWER;
}
return false;
}
/**
* Return true if the user is member
*
* @access public
* @param integer $project_id
* @param integer $user_id
* @return boolean
*/
public function isMember($project_id, $user_id)
{
return ! empty($this->projectUserRoleModel->getUserRole($project_id, $user_id));
}
/**
* Get active project ids by user
*
* @access public
* @param integer $user_id
* @return array
*/
public function getActiveProjectIds($user_id)
{
return array_keys($this->projectUserRoleModel->getActiveProjectsByUser($user_id));
}
/**
* Get all project ids by user
*
* @access public
* @param integer $user_id
* @return array
*/
public function getProjectIds($user_id)
{
return array_keys($this->projectUserRoleModel->getProjectsByUser($user_id));
}
/**
* Copy permissions to another project
*
* @param integer $project_src_id Project Template
* @param integer $project_dst_id Project that receives the copy
* @return boolean
*/
public function duplicate($project_src_id, $project_dst_id)
{
return $this->projectUserRoleModel->duplicate($project_src_id, $project_dst_id) &&
$this->projectGroupRoleModel->duplicate($project_src_id, $project_dst_id);
}
}
+234
View File
@@ -0,0 +1,234 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
use Kanboard\Core\Security\Role;
/**
* Class ProjectRoleModel
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ProjectRoleModel extends Base
{
const TABLE = 'project_has_roles';
/**
* Get list of project roles
*
* @param int $project_id
* @return array
*/
public function getList($project_id)
{
$defaultRoles = $this->role->getProjectRoles();
$customRoles = $this->db
->hashtable(self::TABLE)
->eq('project_id', $project_id)
->getAll('role', 'role');
return $defaultRoles + $customRoles;
}
/**
* Get a role
*
* @param int $project_id
* @param int $role_id
* @return array|null
*/
public function getById($project_id, $role_id)
{
return $this->db->table(self::TABLE)
->eq('project_id', $project_id)
->eq('role_id', $role_id)
->findOne();
}
/**
* Get all project roles
*
* @param int $project_id
* @return array
*/
public function getAll($project_id)
{
return $this->db->table(self::TABLE)
->eq('project_id', $project_id)
->asc('role')
->findAll();
}
/**
* Get all project roles with restrictions
*
* @param int $project_id
* @return array
*/
public function getAllWithRestrictions($project_id)
{
$roles = $this->getAll($project_id);
$column_restrictions = $this->columnRestrictionModel->getAll($project_id);
$column_restrictions = array_column_index($column_restrictions, 'role_id');
array_merge_relation($roles, $column_restrictions, 'column_restrictions', 'role_id');
$column_move_restrictions = $this->columnMoveRestrictionModel->getAll($project_id);
$column_move_restrictions = array_column_index($column_move_restrictions, 'role_id');
array_merge_relation($roles, $column_move_restrictions, 'column_move_restrictions', 'role_id');
$project_restrictions = $this->projectRoleRestrictionModel->getAll($project_id);
$project_restrictions = array_column_index($project_restrictions, 'role_id');
array_merge_relation($roles, $project_restrictions, 'project_restrictions', 'role_id');
return $roles;
}
/**
* Create a new project role
*
* @param int $project_id
* @param string $role
* @return bool|int
*/
public function create($project_id, $role)
{
return $this->db
->table(self::TABLE)
->persist(array(
'project_id' => $project_id,
'role' => $role,
));
}
/**
* Update a project role
*
* @param int $role_id
* @param int $project_id
* @param string $role
* @return bool
*/
public function update($role_id, $project_id, $role)
{
$this->db->startTransaction();
$previousRole = $this->getById($project_id, $role_id);
$r1 = $this->db
->table(ProjectUserRoleModel::TABLE)
->eq('project_id', $project_id)
->eq('role', $previousRole['role'])
->update(array(
'role' => $role
));
$r2 = $this->db
->table(ProjectGroupRoleModel::TABLE)
->eq('project_id', $project_id)
->eq('role', $previousRole['role'])
->update(array(
'role' => $role
));
$r3 = $this->db
->table(self::TABLE)
->eq('role_id', $role_id)
->eq('project_id', $project_id)
->update(array(
'role' => $role,
));
if ($r1 && $r2 && $r3) {
$this->db->closeTransaction();
return true;
}
$this->db->cancelTransaction();
return false;
}
/**
* Remove a project role
*
* @param int $project_id
* @param int $role_id
* @return bool
*/
public function remove($project_id, $role_id)
{
$this->db->startTransaction();
$role = $this->getById($project_id, $role_id);
$r1 = $this->db
->table(ProjectUserRoleModel::TABLE)
->eq('project_id', $project_id)
->eq('role', $role['role'])
->update(array(
'role' => Role::PROJECT_MEMBER
));
$r2 = $this->db
->table(ProjectGroupRoleModel::TABLE)
->eq('project_id', $project_id)
->eq('role', $role['role'])
->update(array(
'role' => Role::PROJECT_MEMBER
));
$r3 = $this->db
->table(self::TABLE)
->eq('project_id', $project_id)
->eq('role_id', $role_id)
->remove();
if ($r1 && $r2 && $r3) {
$this->db->closeTransaction();
return true;
}
$this->db->cancelTransaction();
return false;
}
/**
* Copy project custom_roles from a project to another one
*
* @param integer $project_src_id
* @param integer $project_dst_id
* @return boolean
*/
public function duplicate($project_src_id, $project_dst_id)
{
$rows = $this->db->table(self::TABLE)->eq('project_id', $project_src_id)->findAll();
foreach ($rows as $row) {
$role_src_id = $row['role_id'];
$role_dst_id = $this->db->table(self::TABLE)->persist(array(
'project_id' => $project_dst_id,
'role' => $row['role'],
));
if (! $role_dst_id) {
return false;
}
if (! $this->columnRestrictionModel->duplicate($project_src_id, $project_dst_id, $role_src_id, $role_dst_id)) {
return false;
}
if (! $this->columnMoveRestrictionModel->duplicate($project_src_id, $project_dst_id, $role_src_id, $role_dst_id)) {
return false;
}
if (! $this->projectRoleRestrictionModel->duplicate($project_src_id, $project_dst_id, $role_src_id, $role_dst_id)) {
return false;
}
}
return true;
}
}
+167
View File
@@ -0,0 +1,167 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Class ProjectRoleRestrictionModel
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ProjectRoleRestrictionModel extends Base
{
const TABLE = 'project_role_has_restrictions';
const RULE_TASK_CREATION = 'task_creation';
const RULE_TASK_SUPPRESSION = 'task_remove';
const RULE_TASK_OPEN_CLOSE = 'task_open_close';
const RULE_TASK_MOVE = 'task_move';
const RULE_TASK_CHANGE_ASSIGNEE = 'task_change_assignee';
const RULE_TASK_UPDATE_ASSIGNED = 'task_update_assigned';
/**
* Get rules
*
* @return array
*/
public function getRules()
{
return array(
self::RULE_TASK_CREATION => t('Task creation is not permitted'),
self::RULE_TASK_SUPPRESSION => t('Task suppression is not permitted'),
self::RULE_TASK_OPEN_CLOSE => t('Closing or opening a task is not permitted'),
self::RULE_TASK_MOVE => t('Moving a task is not permitted'),
self::RULE_TASK_CHANGE_ASSIGNEE => t('Changing assignee is not permitted'),
self::RULE_TASK_UPDATE_ASSIGNED => t('Update only assigned tasks is permitted'),
);
}
/**
* Get a single restriction
*
* @param integer $project_id
* @param integer $restriction_id
* @return array|null
*/
public function getById($project_id, $restriction_id)
{
return $this->db
->table(self::TABLE)
->eq('project_id', $project_id)
->eq('restriction_id', $restriction_id)
->findOne();
}
/**
* Get restrictions
*
* @param int $project_id
* @return array
*/
public function getAll($project_id)
{
$rules = $this->getRules();
$restrictions = $this->db
->table(self::TABLE)
->columns(
'restriction_id',
'project_id',
'role_id',
'rule'
)
->eq(self::TABLE.'.project_id', $project_id)
->findAll();
foreach ($restrictions as &$restriction) {
$restriction['title'] = $rules[$restriction['rule']];
}
return $restrictions;
}
/**
* Get restrictions
*
* @param int $project_id
* @param string $role
* @return array
*/
public function getAllByRole($project_id, $role)
{
return $this->db
->table(self::TABLE)
->columns(
'restriction_id',
'project_id',
'role_id',
'rule',
'pr.role'
)
->eq(self::TABLE.'.project_id', $project_id)
->eq('role', $role)
->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id')
->findAll();
}
/**
* Create a new restriction
*
* @param int $project_id
* @param int $role_id
* @param string $rule
* @return bool|int
*/
public function create($project_id, $role_id, $rule)
{
return $this->db->table(self::TABLE)
->persist(array(
'project_id' => $project_id,
'role_id' => $role_id,
'rule' => $rule,
));
}
/**
* Remove a restriction
*
* @param integer $restriction_id
* @return bool
*/
public function remove($restriction_id)
{
return $this->db->table(self::TABLE)->eq('restriction_id', $restriction_id)->remove();
}
/**
* Copy role restriction models from a custome_role in the src project to the dst custom_role of the dst project
*
* @param integer $project_src_id
* @param integer $project_dst_id
* @param integer $role_src_id
* @param integer $role_dst_id
* @return boolean
*/
public function duplicate($project_src_id, $project_dst_id, $role_src_id, $role_dst_id)
{
$rows = $this->db->table(self::TABLE)
->eq('project_id', $project_src_id)
->eq('role_id', $role_src_id)
->findAll();
foreach ($rows as $row) {
$result = $this->db->table(self::TABLE)->persist(array(
'project_id' => $project_dst_id,
'role_id' => $role_dst_id,
'rule' => $row['rule'],
));
if (! $result) {
return false;
}
}
return true;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Project Task Duplication Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ProjectTaskDuplicationModel extends Base
{
/**
* 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->taskFinderModel->getAllIds($src_project_id, array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED));
foreach ($task_ids as $task_id) {
if (! $this->taskProjectDuplicationModel->duplicateToProject($task_id, $dst_project_id)) {
return false;
}
}
return true;
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Project Task Priority Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ProjectTaskPriorityModel extends Base
{
/**
* Get Priority range from a project
*
* @access public
* @param array $project
* @return array
*/
public function getPriorities(array $project)
{
$range = range($project['priority_start'], $project['priority_end']);
return array_combine($range, $range);
}
/**
* Get task priority settings
*
* @access public
* @param int $project_id
* @return array|null
*/
public function getPrioritySettings($project_id)
{
return $this->db
->table(ProjectModel::TABLE)
->columns('priority_default', 'priority_start', 'priority_end')
->eq('id', $project_id)
->findOne();
}
/**
* Get default task priority
*
* @access public
* @param int $project_id
* @return int
*/
public function getDefaultPriority($project_id)
{
return $this->db->table(ProjectModel::TABLE)->eq('id', $project_id)->findOneColumn('priority_default') ?: 0;
}
/**
* Get priority for a destination project
*
* @access public
* @param integer $dst_project_id
* @param integer $priority
* @return integer
*/
public function getPriorityForProject($dst_project_id, $priority)
{
$settings = $this->getPrioritySettings($dst_project_id);
if ($priority >= $settings['priority_start'] && $priority <= $settings['priority_end']) {
return $priority;
}
return $settings['priority_default'];
}
}
+275
View File
@@ -0,0 +1,275 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
use Kanboard\Core\Security\Role;
/**
* Project User Role
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ProjectUserRoleModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'project_has_users';
/**
* Get the list of active project for the given user
*
* @access public
* @param integer $user_id
* @return array
*/
public function getActiveProjectsByUser($user_id)
{
return $this->getProjectsByUser($user_id, array(ProjectModel::ACTIVE));
}
/**
* Get the list of project visible for the given user
*
* @access public
* @param integer $user_id
* @param array $status
* @return array
*/
public function getProjectsByUser($user_id, $status = array(ProjectModel::ACTIVE, ProjectModel::INACTIVE))
{
$userProjects = $this->db
->hashtable(ProjectModel::TABLE)
->eq(self::TABLE.'.user_id', $user_id)
->in(ProjectModel::TABLE.'.is_active', $status)
->join(self::TABLE, 'project_id', 'id')
->getAll(ProjectModel::TABLE.'.id', ProjectModel::TABLE.'.name');
$groupProjects = $this->projectGroupRoleModel->getProjectsByUser($user_id, $status);
$projects = $userProjects + $groupProjects;
asort($projects);
return $projects;
}
/**
* For a given project get the role of the specified user
*
* @access public
* @param integer $project_id
* @param integer $user_id
* @return string
*/
public function getUserRole($project_id, $user_id)
{
$role = $this->db->table(self::TABLE)->eq('user_id', $user_id)->eq('project_id', $project_id)->findOneColumn('role');
if (empty($role)) {
$role = $this->projectGroupRoleModel->getUserRole($project_id, $user_id);
if (empty($role)) {
$role = ""; // force use of the cache
}
}
return $role;
}
/**
* Get all users associated directly to the project
*
* @access public
* @param integer $project_id
* @return array
*/
public function getUsers($project_id)
{
return $this->db->table(self::TABLE)
->columns(
UserModel::TABLE.'.id',
UserModel::TABLE.'.username',
UserModel::TABLE.'.name',
UserModel::TABLE.'.email',
self::TABLE.'.role'
)
->join(UserModel::TABLE, 'id', 'user_id')
->eq('project_id', $project_id)
->asc(UserModel::TABLE.'.username')
->asc(UserModel::TABLE.'.name')
->findAll();
}
/**
* Get all users (fetch users from groups)
*
* @access public
* @param integer $project_id
* @return array
*/
public function getAllUsers($project_id)
{
$userMembers = $this->getUsers($project_id);
$groupMembers = $this->projectGroupRoleModel->getUsers($project_id);
$members = array_merge($userMembers, $groupMembers);
return $this->userModel->prepareList($members);
}
/**
* Get users grouped by role
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getAllUsersGroupedByRole($project_id)
{
$users = array();
$userMembers = $this->getUsers($project_id);
$groupMembers = $this->projectGroupRoleModel->getUsers($project_id);
$members = array_merge($userMembers, $groupMembers);
foreach ($members as $user) {
if (! isset($users[$user['role']])) {
$users[$user['role']] = array();
}
$users[$user['role']][$user['id']] = $user['name'] ?: $user['username'];
}
return $users;
}
/**
* Get list of users that can be assigned to a task (only Manager and Member)
*
* @access public
* @param integer $project_id
* @return array
*/
public function getAssignableUsers($project_id)
{
$userMembers = $this->db->table(self::TABLE)
->columns(UserModel::TABLE.'.id', UserModel::TABLE.'.username', UserModel::TABLE.'.name')
->join(UserModel::TABLE, 'id', 'user_id')
->eq(UserModel::TABLE.'.is_active', 1)
->eq(self::TABLE.'.project_id', $project_id)
->neq(self::TABLE.'.role', Role::PROJECT_VIEWER)
->findAll();
$groupMembers = $this->projectGroupRoleModel->getAssignableUsers($project_id);
$members = array_merge($userMembers, $groupMembers);
return $this->userModel->prepareList($members);
}
/**
* Get list of users that can be assigned to a task (only Manager and Member)
*
* @access public
* @param integer $project_id Project id
* @param bool $unassigned Prepend the 'Unassigned' value
* @param bool $everybody Prepend the 'Everbody' value
* @param bool $singleUser If there is only one user return only this user
* @return array
*/
public function getAssignableUsersList($project_id, $unassigned = true, $everybody = false, $singleUser = false)
{
$users = $this->getAssignableUsers($project_id);
if ($singleUser && count($users) === 1) {
return $users;
}
if ($unassigned) {
$users = array(t('Unassigned')) + $users;
}
if ($everybody) {
$users = array(UserModel::EVERYBODY_ID => t('Everybody')) + $users;
}
return $users;
}
/**
* Add a user to the project
*
* @access public
* @param integer $project_id
* @param integer $user_id
* @param string $role
* @return boolean
*/
public function addUser($project_id, $user_id, $role)
{
return $this->db->table(self::TABLE)->insert(array(
'user_id' => $user_id,
'project_id' => $project_id,
'role' => $role,
));
}
/**
* Remove a user from the project
*
* @access public
* @param integer $project_id
* @param integer $user_id
* @return boolean
*/
public function removeUser($project_id, $user_id)
{
return $this->db->table(self::TABLE)->eq('user_id', $user_id)->eq('project_id', $project_id)->remove();
}
/**
* Change a user role for the project
*
* @access public
* @param integer $project_id
* @param integer $user_id
* @param string $role
* @return boolean
*/
public function changeUserRole($project_id, $user_id, $role)
{
return $this->db->table(self::TABLE)
->eq('user_id', $user_id)
->eq('project_id', $project_id)
->update(array(
'role' => $role,
));
}
/**
* Copy user access from a project to another one
*
* @param integer $project_src_id
* @param integer $project_dst_id
* @return boolean
*/
public function duplicate($project_src_id, $project_dst_id)
{
$rows = $this->db->table(self::TABLE)->eq('project_id', $project_src_id)->findAll();
foreach ($rows as $row) {
$result = $this->db->table(self::TABLE)->save(array(
'project_id' => $project_dst_id,
'user_id' => $row['user_id'],
'role' => $row['role'],
));
if (! $result) {
return false;
}
}
return true;
}
}
+152
View File
@@ -0,0 +1,152 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
use Kanboard\Core\Security\Token;
/**
* Remember Me Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class RememberMeSessionModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'remember_me';
/**
* Expiration (60 days)
*
* @var integer
*/
const EXPIRATION = 5184000;
/**
* Get a remember me record
*
* @access public
* @param $token
* @param $sequence
* @return mixed
*/
public function find($token, $sequence)
{
return $this->db
->table(self::TABLE)
->eq('token', $token)
->eq('sequence', $sequence)
->gt('expiration', time())
->findOne();
}
/**
* Get all sessions for a given user
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function getAll($user_id)
{
return $this->db
->table(self::TABLE)
->eq('user_id', $user_id)
->desc('date_creation')
->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration')
->findAll();
}
/**
* Create a new RememberMe session
*
* @access public
* @param integer $user_id User id
* @param string $ip IP Address
* @param string $user_agent User Agent
* @return array
*/
public function create($user_id, $ip, $user_agent)
{
$token = hash('sha256', $user_id.$user_agent.$ip.Token::getToken());
$sequence = Token::getToken();
$expiration = time() + self::EXPIRATION;
$this->cleanup($user_id);
$this
->db
->table(self::TABLE)
->insert(array(
'user_id' => $user_id,
'ip' => $ip,
'user_agent' => substr($user_agent, 0, 255),
'token' => $token,
'sequence' => $sequence,
'expiration' => $expiration,
'date_creation' => time(),
));
return array(
'token' => $token,
'sequence' => $sequence,
'expiration' => $expiration,
);
}
/**
* Remove a session record
*
* @access public
* @param integer $session_id Session id
* @return mixed
*/
public function remove($session_id)
{
return $this->db
->table(self::TABLE)
->eq('id', $session_id)
->remove();
}
/**
* Remove old sessions for a given user
*
* @access public
* @param integer $user_id User id
* @return bool
*/
public function cleanup($user_id)
{
return $this->db
->table(self::TABLE)
->eq('user_id', $user_id)
->lt('expiration', time())
->remove();
}
/**
* Return a new sequence token and update the database
*
* @access public
* @param string $token Session token
* @return string
*/
public function updateSequence($token)
{
$sequence = Token::getToken();
$this
->db
->table(self::TABLE)
->eq('token', $token)
->update(array('sequence' => $sequence));
return $sequence;
}
}
+127
View File
@@ -0,0 +1,127 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Application Settings
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
abstract class SettingModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'settings';
/**
* Prepare data before save
*
* @abstract
* @access public
* @param array $values
* @return array
*/
abstract public function prepare(array $values);
/**
* Get all settings
*
* @access public
* @return array
*/
public function getAll()
{
return $this->db->hashtable(self::TABLE)->getAll('option', 'value');
}
/**
* Get a setting value
*
* @access public
* @param string $name
* @param string $default
* @return mixed
*/
public function getOption($name, $default = '')
{
$value = $this->db
->table(self::TABLE)
->eq('option', $name)
->findOneColumn('value');
return $value === null || $value === false || $value === '' ? $default : $value;
}
/**
* Return true if a setting exists
*
* @access public
* @param string $name
* @return boolean
*/
public function exists($name)
{
return $this->db
->table(self::TABLE)
->eq('option', $name)
->exists();
}
/**
* Update or insert new settings
*
* @access public
* @param array $values
* @return boolean
*/
public function save(array $values)
{
$results = array();
$values = $this->prepare($values);
$user_id = $this->userSession->getId();
$timestamp = time();
$this->db->startTransaction();
foreach ($values as $option => $value) {
if ($this->exists($option)) {
$results[] = $this->db->table(self::TABLE)->eq('option', $option)->update(array(
'value' => $value,
'changed_on' => $timestamp,
'changed_by' => $user_id,
));
} else {
$results[] = $this->db->table(self::TABLE)->insert(array(
'option' => $option,
'value' => $value,
'changed_on' => $timestamp,
'changed_by' => $user_id,
));
}
}
$this->db->closeTransaction();
return ! in_array(false, $results, true);
}
/**
* Remove a setting
*
* @access public
* @param string $option
* @return bool
*/
public function remove($option)
{
return $this->db->table(self::TABLE)
->eq('option', $option)
->remove();
}
}
+337
View File
@@ -0,0 +1,337 @@
<?php
namespace Kanboard\Model;
use PicoDb\Database;
use Kanboard\Core\Base;
/**
* Subtask Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class SubtaskModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'subtasks';
/**
* Subtask status
*
* @var integer
*/
const STATUS_TODO = 0;
const STATUS_INPROGRESS = 1;
const STATUS_DONE = 2;
/**
* Events
*
* @var string
*/
const EVENT_UPDATE = 'subtask.update';
const EVENT_CREATE = 'subtask.create';
const EVENT_DELETE = 'subtask.delete';
const EVENT_CREATE_UPDATE = 'subtask.create_update';
/**
* Get projectId from subtaskId
*
* @access public
* @param integer $subtaskId
* @return integer
*/
public function getProjectId($subtaskId)
{
return $this->db
->table(self::TABLE)
->eq(self::TABLE.'.id', $subtaskId)
->join(TaskModel::TABLE, 'id', 'task_id')
->findOneColumn(TaskModel::TABLE . '.project_id') ?: 0;
}
/**
* Get available status
*
* @access public
* @return string[]
*/
public function getStatusList()
{
return array(
self::STATUS_TODO => 'Todo',
self::STATUS_INPROGRESS => 'In progress',
self::STATUS_DONE => 'Done',
);
}
/**
* Get common query
*
* @return \PicoDb\Table
*/
public function getQuery()
{
return $this->db
->table(self::TABLE)
->columns(
self::TABLE.'.*',
UserModel::TABLE.'.username',
UserModel::TABLE.'.name'
)
->subquery($this->subtaskTimeTrackingModel->getTimerQuery($this->userSession->getId()), 'timer_start_date')
->join(UserModel::TABLE, 'id', 'user_id')
->asc(self::TABLE.'.position');
}
/**
* Count by assignee and task status.
*
* @param integer $userId
* @return integer
*/
public function countByAssigneeAndTaskStatus($userId)
{
$query = $this->db->table(self::TABLE)
->eq('user_id', $userId)
->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_OPEN)
->join(TaskModel::TABLE, 'id', 'task_id');
$this->hook->reference('model:subtask:count:query', $query);
return $query->count();
}
/**
* Get all subtasks for a given task
*
* @access public
* @param integer $taskId
* @return array
*/
public function getAll($taskId)
{
return $this->subtaskListFormatter
->withQuery($this->getQuery()->eq('task_id', $taskId))
->format();
}
/**
* Get subtasks for a list of tasks
*
* @param array $taskIds
* @return array
*/
public function getAllByTaskIds(array $taskIds)
{
if (empty($taskIds)) {
return array();
}
return $this->subtaskListFormatter
->withQuery($this->getQuery()->in('task_id', $taskIds))
->format();
}
/**
* Get subtasks for a list of tasks and a given assignee
*
* @param array $taskIds
* @param integer $userId
* @return array
*/
public function getAllByTaskIdsAndAssignee(array $taskIds, $userId)
{
if (empty($taskIds)) {
return array();
}
return $this->subtaskListFormatter
->withQuery($this->getQuery()->in('task_id', $taskIds)->eq(self::TABLE.'.user_id', $userId))
->format();
}
/**
* Get a subtask by the id
*
* @access public
* @param integer $subtaskId
* @return array
*/
public function getById($subtaskId)
{
return $this->db->table(self::TABLE)->eq('id', $subtaskId)->findOne();
}
/**
* Get subtask with additional information
*
* @param integer $subtaskId
* @return array|null
*/
public function getByIdWithDetails($subtaskId)
{
$subtasks = $this->subtaskListFormatter
->withQuery($this->getQuery()->eq(self::TABLE.'.id', $subtaskId))
->format();
if (! empty($subtasks)) {
return $subtasks[0];
}
return null;
}
/**
* Get the position of the last column for a given project
*
* @access public
* @param integer $taskId
* @return integer
*/
public function getLastPosition($taskId)
{
return (int) $this->db
->table(self::TABLE)
->eq('task_id', $taskId)
->desc('position')
->findOneColumn('position');
}
/**
* Create a new subtask
*
* @access public
* @param array $values Form values
* @return bool|integer
*/
public function create(array $values)
{
$this->prepareCreation($values);
$subtaskId = $this->db->table(self::TABLE)->persist($values);
if ($subtaskId !== false) {
$this->subtaskTimeTrackingModel->updateTaskTimeTracking($values['task_id']);
$this->queueManager->push($this->subtaskEventJob->withParams(
$subtaskId,
array(self::EVENT_CREATE_UPDATE, self::EVENT_CREATE)
));
}
return $subtaskId;
}
/**
* Update a subtask
*
* @access public
* @param array $values
* @param bool $fireEvent
* @return bool
*/
public function update(array $values, $fireEvent = true)
{
$this->prepare($values);
$updates = $values;
unset($updates['id']);
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->save($updates);
if ($result) {
$subtask = $this->getById($values['id']);
$this->subtaskTimeTrackingModel->updateTaskTimeTracking($subtask['task_id']);
if ($fireEvent) {
$this->queueManager->push($this->subtaskEventJob->withParams(
$subtask['id'],
array(self::EVENT_CREATE_UPDATE, self::EVENT_UPDATE),
$values
));
}
}
return $result;
}
/**
* Remove
*
* @access public
* @param integer $subtaskId
* @return bool
*/
public function remove($subtaskId)
{
$this->subtaskEventJob->execute($subtaskId, array(self::EVENT_DELETE));
$subtask = $this->getById($subtaskId);
$result = $this->db->table(self::TABLE)->eq('id', $subtaskId)->remove();
$this->subtaskTimeTrackingModel->updateTaskTimeTracking($subtask['task_id']);
return $result;
}
/**
* Duplicate all subtasks to another task
*
* @access public
* @param integer $srcTaskId
* @param integer $dstTaskId
* @return bool
*/
public function duplicate($srcTaskId, $dstTaskId)
{
return $this->db->transaction(function (Database $db) use ($srcTaskId, $dstTaskId) {
$subtasks = $db->table(SubtaskModel::TABLE)
->columns('title', 'time_estimated', 'position', 'user_id')
->eq('task_id', $srcTaskId)
->asc('position')
->findAll();
foreach ($subtasks as &$subtask) {
$subtask['task_id'] = $dstTaskId;
if (! $db->table(SubtaskModel::TABLE)->save($subtask)) {
return false;
}
}
});
}
/**
* Prepare data before insert/update
*
* @access protected
* @param array $values Form values
*/
protected function prepare(array &$values)
{
$this->helper->model->removeFields($values, array('another_subtask'));
$this->helper->model->resetFields($values, array('time_estimated', 'time_spent'));
$this->hook->reference('model:subtask:modification:prepare', $values);
}
/**
* Prepare data before insert
*
* @access protected
* @param array $values Form values
*/
protected function prepareCreation(array &$values)
{
$this->prepare($values);
$values['position'] = $this->getLastPosition($values['task_id']) + 1;
$values['status'] = isset($values['status']) ? $values['status'] : self::STATUS_TODO;
$values['time_estimated'] = isset($values['time_estimated']) ? $values['time_estimated'] : 0;
$values['time_spent'] = isset($values['time_spent']) ? $values['time_spent'] : 0;
$values['user_id'] = isset($values['user_id']) ? $values['user_id'] : 0;
$this->hook->reference('model:subtask:creation:prepare', $values);
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Class SubtaskPositionModel
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class SubtaskPositionModel extends Base
{
/**
* Change subtask position
*
* @access public
* @param integer $task_id
* @param integer $subtask_id
* @param integer $position
* @return boolean
*/
public function changePosition($task_id, $subtask_id, $position)
{
if ($position < 1 || $position > $this->db->table(SubtaskModel::TABLE)->eq('task_id', $task_id)->count()) {
return false;
}
$subtask_ids = $this->db->table(SubtaskModel::TABLE)->eq('task_id', $task_id)->neq('id', $subtask_id)->asc('position')->findAllByColumn('id');
$offset = 1;
$results = array();
foreach ($subtask_ids as $current_subtask_id) {
if ($offset == $position) {
$offset++;
}
$results[] = $this->db->table(SubtaskModel::TABLE)->eq('id', $current_subtask_id)->update(array('position' => $offset));
$offset++;
}
$results[] = $this->db->table(SubtaskModel::TABLE)->eq('id', $subtask_id)->update(array('position' => $position));
return !in_array(false, $results, true);
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Class SubtaskStatusModel
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class SubtaskStatusModel extends Base
{
/**
* Get the subtask in progress for this user
*
* @access public
* @param integer $user_id
* @return array
*/
public function getSubtaskInProgress($user_id)
{
return $this->db->table(SubtaskModel::TABLE)
->eq('status', SubtaskModel::STATUS_INPROGRESS)
->eq('user_id', $user_id)
->findOne();
}
/**
* Return true if the user have a subtask in progress
*
* @access public
* @param integer $user_id
* @return boolean
*/
public function hasSubtaskInProgress($user_id)
{
return $this->configModel->get('subtask_restriction') == 1 &&
$this->db->table(SubtaskModel::TABLE)
->eq('status', SubtaskModel::STATUS_INPROGRESS)
->eq('user_id', $user_id)
->exists();
}
/**
* Change the status of subtask
*
* @access public
* @param integer $subtask_id
* @return boolean|integer
*/
public function toggleStatus($subtask_id)
{
$subtask = $this->subtaskModel->getById($subtask_id);
$status = ($subtask['status'] + 1) % 3;
$values = array(
'id' => $subtask['id'],
'status' => $status,
'task_id' => $subtask['task_id'],
);
if (empty($subtask['user_id']) && $this->userSession->isLogged()) {
$values['user_id'] = $this->userSession->getId();
$subtask['user_id'] = $values['user_id'];
}
$this->subtaskTimeTrackingModel->toggleTimer($subtask_id, $subtask['user_id'], $status);
return $this->subtaskModel->update($values) ? $status : false;
}
/**
* Close all subtasks of a task
*
* @access public
* @param integer $task_id
* @return boolean
*/
public function closeAll($task_id)
{
return $this->db
->table(SubtaskModel::TABLE)
->eq('task_id', $task_id)
->update(array('status' => SubtaskModel::STATUS_DONE));
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Class SubtaskTaskConversionModel
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class SubtaskTaskConversionModel extends Base
{
/**
* Convert a subtask to a task
*
* @access public
* @param integer $project_id
* @param integer $subtask_id
* @return integer
*/
public function convertToTask($project_id, $subtask_id)
{
$subtask = $this->subtaskModel->getById($subtask_id);
$parent_task = $this->taskFinderModel->getById($subtask['task_id']);
$task_id = $this->taskCreationModel->create(array(
'project_id' => $project_id,
'title' => $subtask['title'],
'time_estimated' => $subtask['time_estimated'],
'time_spent' => $subtask['time_spent'],
'owner_id' => $subtask['user_id'],
'swimlane_id' => $parent_task['swimlane_id'],
'priority' => $parent_task['priority'],
'column_id' => $parent_task['column_id'],
'category_id' => $parent_task['category_id'],
'color_id' => $parent_task['color_id']
));
if ($task_id !== false) {
$link = $this->linkModel->getByLabel('is a child of');
if ($link) {
$this->taskLinkModel->create($task_id, $subtask['task_id'], $link['id']);
}
$this->tagDuplicationModel->duplicateTaskTags($parent_task['id'], $task_id);
$this->subtaskModel->remove($subtask_id);
}
return $task_id;
}
}
+291
View File
@@ -0,0 +1,291 @@
<?php
namespace Kanboard\Model;
use DateTime;
use Kanboard\Core\Base;
/**
* Subtask time tracking
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class SubtaskTimeTrackingModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'subtask_time_tracking';
/**
* Get query to check if a timer is started for the given user and subtask
*
* @access public
* @param integer $user_id User id
* @return string
*/
public function getTimerQuery($user_id)
{
$sql = $this->db
->table(self::TABLE)
->columns('start')
->eq($this->db->escapeIdentifier('user_id', self::TABLE), $user_id)
->eq($this->db->escapeIdentifier('end', self::TABLE), 0)
->eq($this->db->escapeIdentifier('subtask_id', self::TABLE), SubtaskModel::TABLE.'.id')
->limit(1)
->buildSelectQuery();
// need to interpolate values into the SQL text for use as a subquery
// in SubtaskModel::getQuery()
$sql = substr_replace($sql, $user_id, strpos($sql, '?'), 1);
$sql = substr_replace($sql, 0, strpos($sql, '?'), 1);
$sql = substr_replace($sql, SubtaskModel::TABLE.'.id', strpos($sql, '?'), 1);
return $sql;
}
/**
* Get query for user timesheet (pagination)
*
* @access public
* @param integer $user_id User id
* @return \PicoDb\Table
*/
public function getUserQuery($user_id)
{
return $this->db
->table(self::TABLE)
->columns(
$this->db->escapeIdentifier('id', self::TABLE),
$this->db->escapeIdentifier('subtask_id', self::TABLE),
$this->db->escapeIdentifier('end', self::TABLE),
$this->db->escapeIdentifier('start', self::TABLE),
$this->db->escapeIdentifier('time_spent', self::TABLE),
$this->db->escapeIdentifier('task_id', SubtaskModel::TABLE),
$this->db->escapeIdentifier('title', SubtaskModel::TABLE).' AS subtask_title',
$this->db->escapeIdentifier('title', TaskModel::TABLE).' AS task_title',
$this->db->escapeIdentifier('project_id', TaskModel::TABLE),
$this->db->escapeIdentifier('color_id', TaskModel::TABLE),
)
->join(SubtaskModel::TABLE, 'id', 'subtask_id')
->join(TaskModel::TABLE, 'id', 'task_id', SubtaskModel::TABLE)
->eq($this->db->escapeIdentifier('user_id', self::TABLE), $user_id);
}
/**
* Get query for task timesheet (pagination)
*
* @access public
* @param integer $task_id Task id
* @return \PicoDb\Table
*/
public function getTaskQuery($task_id)
{
return $this->db
->table(self::TABLE)
->columns(
$this->db->escapeIdentifier('id', self::TABLE),
$this->db->escapeIdentifier('subtask_id', self::TABLE),
$this->db->escapeIdentifier('end', self::TABLE),
$this->db->escapeIdentifier('start', self::TABLE),
$this->db->escapeIdentifier('time_spent', self::TABLE),
$this->db->escapeIdentifier('user_id', self::TABLE),
$this->db->escapeIdentifier('task_id', SubtaskModel::TABLE),
$this->db->escapeIdentifier('title', SubtaskModel::TABLE).' AS subtask_title',
$this->db->escapeIdentifier('project_id', TaskModel::TABLE),
$this->db->escapeIdentifier('username', UserModel::TABLE),
$this->db->escapeIdentifier('name', UserModel::TABLE).' AS user_fullname',
)
->join(SubtaskModel::TABLE, 'id', 'subtask_id')
->join(TaskModel::TABLE, 'id', 'task_id', SubtaskModel::TABLE)
->join(UserModel::TABLE, 'id', 'user_id', self::TABLE)
->eq($this->db->escapeIdentifier('id', TaskModel::TABLE), $task_id);
}
/**
* Get all recorded time slots for a given user
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function getUserTimesheet($user_id)
{
return $this->db
->table(self::TABLE)
->eq('user_id', $user_id)
->findAll();
}
/**
* Return true if a timer is started for this use and subtask
*
* @access public
* @param integer $subtask_id
* @param integer $user_id
* @return boolean
*/
public function hasTimer($subtask_id, $user_id)
{
return $this->db->table(self::TABLE)->eq('subtask_id', $subtask_id)->eq('user_id', $user_id)->eq('end', 0)->exists();
}
/**
* Start or stop timer according to subtask status
*
* @access public
* @param integer $subtask_id
* @param integer $user_id
* @param integer $status
* @return boolean
*/
public function toggleTimer($subtask_id, $user_id, $status)
{
if ($this->configModel->get('subtask_time_tracking') == 1) {
if ($status == SubtaskModel::STATUS_INPROGRESS) {
return $this->subtaskTimeTrackingModel->logStartTime($subtask_id, $user_id);
} elseif ($status == SubtaskModel::STATUS_DONE) {
return $this->subtaskTimeTrackingModel->logEndTime($subtask_id, $user_id);
}
}
return false;
}
/**
* Log start time
*
* @access public
* @param integer $subtask_id
* @param integer $user_id
* @return boolean
*/
public function logStartTime($subtask_id, $user_id)
{
return
! $this->hasTimer($subtask_id, $user_id) &&
$this->db
->table(self::TABLE)
->insert(array('subtask_id' => $subtask_id, 'user_id' => $user_id, 'start' => time(), 'end' => 0));
}
/**
* Log end time
*
* @access public
* @param integer $subtask_id
* @param integer $user_id
* @return boolean
*/
public function logEndTime($subtask_id, $user_id)
{
$time_spent = $this->getTimeSpent($subtask_id, $user_id);
if ($time_spent > 0) {
$this->updateSubtaskTimeSpent($subtask_id, $time_spent);
}
return $this->db
->table(self::TABLE)
->eq('subtask_id', $subtask_id)
->eq('user_id', $user_id)
->eq('end', 0)
->update(array(
'end' => time(),
'time_spent' => $time_spent,
));
}
/**
* Calculate the time spent when the clock is stopped
*
* @access public
* @param integer $subtask_id
* @param integer $user_id
* @return float
*/
public function getTimeSpent($subtask_id, $user_id)
{
$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');
if (empty($start_time)) {
return 0;
}
$end = new DateTime;
$start = new DateTime;
$start->setTimestamp($start_time);
if ($this->hook->exists($hook)) {
return $this->hook->first($hook, array(
'user_id' => $user_id,
'start' => $start,
'end' => $end,
));
}
return $this->dateParser->getHours($start, $end);
}
/**
* Update subtask time spent
*
* @access public
* @param integer $subtask_id
* @param float $time_spent
* @return bool
*/
public function updateSubtaskTimeSpent($subtask_id, $time_spent)
{
$subtask = $this->subtaskModel->getById($subtask_id);
return $this->subtaskModel->update(array(
'id' => $subtask['id'],
'time_spent' => $subtask['time_spent'] + $time_spent,
'task_id' => $subtask['task_id'],
), false);
}
/**
* Update task time tracking based on subtasks time tracking
*
* @access public
* @param integer $task_id Task id
* @return bool
*/
public function updateTaskTimeTracking($task_id)
{
$values = $this->calculateSubtaskTime($task_id);
return $this->db
->table(TaskModel::TABLE)
->eq('id', $task_id)
->update($values);
}
/**
* Sum time spent and time estimated for all subtasks
*
* @access public
* @param integer $task_id Task id
* @return array
*/
public function calculateSubtaskTime($task_id)
{
return $this->db
->table(SubtaskModel::TABLE)
->eq('task_id', $task_id)
->columns(
'SUM(time_spent) AS time_spent',
'SUM(time_estimated) AS time_estimated'
)
->findOne();
}
}
+459
View File
@@ -0,0 +1,459 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Swimlanes
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class SwimlaneModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'swimlanes';
/**
* Value for active swimlanes
*
* @var integer
*/
const ACTIVE = 1;
/**
* Value for inactive swimlanes
*
* @var integer
*/
const INACTIVE = 0;
/**
* Get a swimlane by the id
*
* @access public
* @param integer $swimlaneId
* @return array
*/
public function getById($swimlaneId)
{
return $this->db->table(self::TABLE)->eq('id', $swimlaneId)->findOne();
}
/**
* Get the swimlane name by the id
*
* @access public
* @param integer $swimlaneId
* @return string
*/
public function getNameById($swimlaneId)
{
return $this->db->table(self::TABLE)
->eq('id', $swimlaneId)
->findOneColumn('name');
}
/**
* Get a swimlane id by the project and the name
*
* @access public
* @param integer $projectId Project id
* @param string $name Name
* @return integer
*/
public function getIdByName($projectId, $name)
{
return (int) $this->db->table(self::TABLE)
->eq('project_id', $projectId)
->eq('name', $name)
->findOneColumn('id');
}
/**
* Get a swimlane by the project and the name
*
* @access public
* @param integer $projectId Project id
* @param string $name Swimlane name
* @return array
*/
public function getByName($projectId, $name)
{
return $this->db->table(self::TABLE)
->eq('project_id', $projectId)
->eq('name', $name)
->findOne();
}
/**
* Get first active swimlane for a project
*
* @access public
* @param integer $projectId
* @return array|null
*/
public function getFirstActiveSwimlane($projectId)
{
return $this->db->table(self::TABLE)
->eq('project_id', $projectId)
->eq('is_active', 1)
->asc('position')
->findOne();
}
/**
* Get first active swimlaneId
*
* @access public
* @param int $projectId
* @return int
*/
public function getFirstActiveSwimlaneId($projectId)
{
return (int) $this->db->table(self::TABLE)
->eq('project_id', $projectId)
->eq('is_active', 1)
->asc('position')
->findOneColumn('id');
}
/**
* Get all swimlanes for a given project
*
* @access public
* @param integer $projectId
* @return array
*/
public function getAll($projectId)
{
return $this->db
->table(self::TABLE)
->eq('project_id', $projectId)
->orderBy('position', 'asc')
->findAll();
}
/**
* Get the list of swimlanes by status
*
* @access public
* @param integer $projectId
* @param integer $status
* @return array
*/
public function getAllByStatus($projectId, $status = self::ACTIVE)
{
$query = $this->db
->table(self::TABLE)
->eq('project_id', $projectId)
->eq('is_active', $status);
if ($status == self::ACTIVE) {
$query->asc('position');
} else {
$query->asc('name');
}
return $query->findAll();
}
/**
* Get all swimlanes with task count
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getAllWithTaskCount($project_id)
{
$result = array(
'active' => array(),
'inactive' => array(),
);
$swimlanes = $this->db->table(self::TABLE)
->columns('id', 'name', 'description', 'project_id', 'position', 'is_active', 'task_limit')
->subquery("SELECT COUNT(*) FROM ".TaskModel::TABLE." WHERE swimlane_id=".self::TABLE.".id AND is_active='1'", 'nb_open_tasks')
->subquery("SELECT COUNT(*) FROM ".TaskModel::TABLE." WHERE swimlane_id=".self::TABLE.".id AND is_active='0'", 'nb_closed_tasks')
->eq('project_id', $project_id)
->asc('position')
->asc('name')
->findAll();
foreach ($swimlanes as $swimlane) {
if ($swimlane['is_active']) {
$result['active'][] = $swimlane;
} else {
$result['inactive'][] = $swimlane;
}
}
return $result;
}
/**
* Get list of all swimlanes
*
* @access public
* @param integer $projectId Project id
* @param boolean $prepend Prepend default value
* @param boolean $onlyActive Return only active swimlanes
* @return array
*/
public function getList($projectId, $prepend = false, $onlyActive = false)
{
$swimlanes = array();
if ($prepend) {
$swimlanes[-1] = t('All swimlanes');
}
return $swimlanes + $this->db
->hashtable(self::TABLE)
->eq('project_id', $projectId)
->in('is_active', $onlyActive ? array(self::ACTIVE) : array(self::ACTIVE, self::INACTIVE))
->orderBy('position', 'asc')
->getAll('id', 'name');
}
/**
* Add a new swimlane
*
* @access public
* @param int $projectId
* @param string $name
* @param string $description
* @return bool|int
*/
public function create($projectId, $name, $description = '', $task_limit = 0)
{
if (! $this->projectModel->exists($projectId)) {
return 0;
}
return $this->db->table(self::TABLE)->persist(array(
'project_id' => $projectId,
'name' => $name,
'description' => $description,
'position' => $this->getLastPosition($projectId),
'is_active' => 1,
'task_limit' => intval($task_limit),
));
}
/**
* Update a swimlane
*
* @access public
* @param integer $swimlaneId
* @param array $values
* @return bool
*/
public function update($swimlaneId, array $values)
{
unset($values['id']);
unset($values['project_id']);
return $this->db
->table(self::TABLE)
->eq('id', $swimlaneId)
->update($values);
}
/**
* Get the last position of a swimlane
*
* @access public
* @param integer $projectId
* @return integer
*/
public function getLastPosition($projectId)
{
return $this->db
->table(self::TABLE)
->eq('project_id', $projectId)
->eq('is_active', 1)
->count() + 1;
}
/**
* Disable a swimlane
*
* @access public
* @param integer $projectId
* @param integer $swimlaneId
* @return bool
*/
public function disable($projectId, $swimlaneId)
{
$result = $this->db
->table(self::TABLE)
->eq('id', $swimlaneId)
->eq('project_id', $projectId)
->update(array(
'is_active' => self::INACTIVE,
'position' => 0,
));
if ($result) {
$this->updatePositions($projectId);
}
return $result;
}
/**
* Enable a swimlane
*
* @access public
* @param integer $projectId
* @param integer $swimlaneId
* @return bool
*/
public function enable($projectId, $swimlaneId)
{
return $this->db
->table(self::TABLE)
->eq('id', $swimlaneId)
->eq('project_id', $projectId)
->update(array(
'is_active' => self::ACTIVE,
'position' => $this->getLastPosition($projectId),
));
}
/**
* Remove a swimlane
*
* @access public
* @param integer $projecId
* @param integer $swimlaneId
* @return bool
*/
public function remove($projecId, $swimlaneId)
{
$this->db->startTransaction();
if ($this->db->table(TaskModel::TABLE)->eq('swimlane_id', $swimlaneId)->exists()) {
$this->db->cancelTransaction();
return false;
}
if (! $this->db->table(self::TABLE)->eq('id', $swimlaneId)->eq('project_id', $projecId)->remove()) {
$this->db->cancelTransaction();
return false;
}
$this->updatePositions($projecId);
$this->db->closeTransaction();
return true;
}
/**
* Update swimlane positions after disabling or removing a swimlane
*
* @access public
* @param integer $projectId
* @return boolean
*/
public function updatePositions($projectId)
{
$position = 0;
$swimlanes = $this->db
->table(self::TABLE)
->eq('project_id', $projectId)
->eq('is_active', 1)
->asc('position')
->asc('id')
->findAllByColumn('id');
if (! $swimlanes) {
return false;
}
foreach ($swimlanes as $swimlane_id) {
$this->db->table(self::TABLE)
->eq('id', $swimlane_id)
->update(array('position' => ++$position));
}
return true;
}
/**
* Change swimlane position
*
* @access public
* @param integer $projectId
* @param integer $swimlaneId
* @param integer $position
* @return boolean
*/
public function changePosition($projectId, $swimlaneId, $position)
{
if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('project_id', $projectId)->count()) {
return false;
}
$swimlaneIds = $this->db->table(self::TABLE)
->eq('is_active', 1)
->eq('project_id', $projectId)
->neq('id', $swimlaneId)
->asc('position')
->findAllByColumn('id');
$offset = 1;
$results = array();
foreach ($swimlaneIds as $currentSwimlaneId) {
if ($offset == $position) {
$offset++;
}
$results[] = $this->db->table(self::TABLE)->eq('id', $currentSwimlaneId)->update(array('position' => $offset));
$offset++;
}
$results[] = $this->db->table(self::TABLE)->eq('id', $swimlaneId)->eq('project_id', $projectId)->update(array('position' => $position));
return !in_array(false, $results, true);
}
/**
* Duplicate Swimlane to project
*
* @access public
* @param integer $projectSrcId
* @param integer $projectDstId
* @return boolean
*/
public function duplicate($projectSrcId, $projectDstId)
{
$swimlanes = $this->getAll($projectSrcId);
foreach ($swimlanes as $swimlane) {
if (! $this->db->table(self::TABLE)->eq('project_id', $projectDstId)->eq('name', $swimlane['name'])->exists()) {
$values = array(
'name' => $swimlane['name'],
'description' => $swimlane['description'],
'position' => $swimlane['position'],
'is_active' => $swimlane['is_active'] ? self::ACTIVE : self::INACTIVE, // Avoid SQL error with Postgres
'project_id' => $projectDstId,
);
if (! $this->db->table(self::TABLE)->persist($values)) {
return false;
}
}
}
return true;
}
}
+96
View File
@@ -0,0 +1,96 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Tag Duplication
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TagDuplicationModel extends Base
{
/**
* Duplicate project tags to another project
*
* @access public
* @param integer $src_project_id
* @param integer $dst_project_id
* @return bool
*/
public function duplicate($src_project_id, $dst_project_id)
{
$tags = $this->tagModel->getAllByProject($src_project_id);
$results = array();
foreach ($tags as $tag) {
$results[] = $this->tagModel->create($dst_project_id, $tag['name'], $tag['color_id']);
}
return ! in_array(false, $results, true);
}
/**
* Link tags to the new tasks
*
* @access public
* @param integer $src_task_id
* @param integer $dst_task_id
* @param integer $dst_project_id
*/
public function duplicateTaskTagsToAnotherProject($src_task_id, $dst_task_id, $dst_project_id)
{
$tags = $this->taskTagModel->getTagsByTask($src_task_id);
foreach ($tags as $tag) {
$tag_id = $this->tagModel->getIdByName($dst_project_id, $tag['name']);
if (empty($tag_id)) {
$tag_id = $this->tagModel->create($dst_project_id, $tag['name'], $tag['color_id']);
}
$this->taskTagModel->associateTag($dst_task_id, $tag_id);
}
}
/**
* Duplicate tags to the new task
*
* @access public
* @param integer $src_task_id
* @param integer $dst_task_id
*/
public function duplicateTaskTags($src_task_id, $dst_task_id)
{
$tags = $this->taskTagModel->getTagsByTask($src_task_id);
foreach ($tags as $tag) {
$this->taskTagModel->associateTag($dst_task_id, $tag['id']);
}
}
/**
* Sync tags that are not available in destination project
*
* @access public
* @param integer $task_id
* @param integer $dst_project_id
*/
public function syncTaskTagsToAnotherProject($task_id, $dst_project_id)
{
$tags = $this->taskTagModel->getTagsByTaskNotAvailableInProject($task_id, $dst_project_id);
foreach ($tags as $tag) {
$tag_id = $this->tagModel->getIdByName($dst_project_id, $tag['name']);
if (empty($tag_id)) {
$tag_id = $this->tagModel->create($dst_project_id, $tag['name'], $tag['color_id']);
}
$this->taskTagModel->dissociateTag($task_id, $tag['id']);
$this->taskTagModel->associateTag($task_id, $tag_id);
}
}
}
+217
View File
@@ -0,0 +1,217 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Class TagModel
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TagModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'tags';
/**
* Get all tags
*
* @access public
* @return array
*/
public function getAll()
{
return $this->db->table(self::TABLE)->asc('name')->findAll();
}
/**
* Get all tags by project
*
* @access public
* @param integer $project_id
* @return array
*/
public function getAllByProject($project_id)
{
return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('name')->findAll();
}
/**
* Get all global tags and tags for the given project IDs
*
* @access public
* @param array $project_ids
* @return array
*/
public function getAllByProjectIds(array $project_ids)
{
return $this->db->table(self::TABLE)
->beginOr()
->eq('project_id', 0)
->in('project_id', $project_ids)
->closeOr()
->asc('name')
->findAll();
}
/**
* Get assignable tags for a project
*
* @param integer $project_id Project Id
* @param bool $include_global_tags Flag to include global tags
* @return array
*/
public function getAssignableList($project_id, $include_global_tags = true)
{
if ($include_global_tags) {
return $this->db->hashtable(self::TABLE)
->beginOr()
->eq('project_id', $project_id)
->eq('project_id', 0)
->closeOr()
->asc('name')
->getAll('id', 'name');
} else {
return $this->db->hashtable(self::TABLE)
->beginOr()
->eq('project_id', $project_id)
->closeOr()
->asc('name')
->getAll('id', 'name');
}
}
/**
* Get one tag
*
* @access public
* @param integer $tag_id
* @return array|null
*/
public function getById($tag_id)
{
return $this->db->table(self::TABLE)->eq('id', $tag_id)->findOne();
}
/**
* Get tag id from tag name
*
* @access public
* @param int $project_id
* @param string $tag
* @return integer
*/
public function getIdByName($project_id, $tag)
{
return $this->db
->table(self::TABLE)
->beginOr()
->eq('project_id', 0)
->eq('project_id', $project_id)
->closeOr()
->ilike('name', $tag)
->asc('project_id')
->findOneColumn('id');
}
/**
* Return true if the tag exists
*
* @access public
* @param integer $project_id
* @param string $tag
* @param integer $tag_id
* @return boolean
*/
public function exists($project_id, $tag, $tag_id = 0)
{
return $this->db
->table(self::TABLE)
->neq('id', $tag_id)
->beginOr()
->eq('project_id', 0)
->eq('project_id', $project_id)
->closeOr()
->ilike('name', $tag)
->asc('project_id')
->exists();
}
/**
* Return tag id and create a new tag if necessary
*
* @access public
* @param int $project_id
* @param string $tag
* @return bool|int
*/
public function findOrCreateTag($project_id, $tag)
{
$tag_id = $this->getIdByName($project_id, $tag);
if (empty($tag_id)) {
$tag_id = $this->create($project_id, $tag);
}
return $tag_id;
}
/**
* Add a new tag
*
* @access public
* @param int $project_id
* @param string $tag
* @return bool|int
*/
public function create($project_id, $tag, $color_id = null)
{
return $this->db->table(self::TABLE)->persist(array(
'project_id' => $project_id,
'name' => $tag,
'color_id' => $color_id,
));
}
/**
* Update a tag
*
* @access public
* @param integer $tag_id
* @param string $tag
* @return bool
*/
public function update($tag_id, $tag, $color_id = null, $project_id = null)
{
if ($project_id !== null) {
return $this->db->table(self::TABLE)->eq('id', $tag_id)->update(array(
'name' => $tag,
'color_id' => $color_id,
'project_id' => $project_id,
));
} else {
return $this->db->table(self::TABLE)->eq('id', $tag_id)->update(array(
'name' => $tag,
'color_id' => $color_id,
));
}
}
/**
* Remove a tag
*
* @access public
* @param integer $tag_id
* @return bool
*/
public function remove($tag_id)
{
return $this->db->table(self::TABLE)->eq('id', $tag_id)->remove();
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Task Analytic
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TaskAnalyticModel extends Base
{
/**
* Get the time between date_creation and date_completed or now if empty
*
* @access public
* @param array $task
* @return integer
*/
public function getLeadTime(array $task)
{
return ($task['date_completed'] ?: time()) - $task['date_creation'];
}
/**
* Get the time between date_started and date_completed or now if empty
*
* @access public
* @param array $task
* @return integer
*/
public function getCycleTime(array $task)
{
if (empty($task['date_started'])) {
return 0;
}
return ($task['date_completed'] ?: time()) - $task['date_started'];
}
/**
* Get the average time spent in each column
*
* @access public
* @param array $task
* @return array
*/
public function getTimeSpentByColumn(array $task)
{
$result = array();
$columns = $this->columnModel->getList($task['project_id']);
$sums = $this->transitionModel->getTimeSpentByTask($task['id']);
foreach ($columns as $column_id => $column_title) {
$time_spent = isset($sums[$column_id]) ? $sums[$column_id] : 0;
if ($task['column_id'] == $column_id) {
$time_spent += ($task['date_completed'] ?: time()) - $task['date_moved'];
}
$result[] = array(
'id' => $column_id,
'title' => $column_title,
'time_spent' => $time_spent,
);
}
return $result;
}
}
+94
View File
@@ -0,0 +1,94 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Task Creation
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TaskCreationModel extends Base
{
/**
* Create a task
*
* @access public
* @param array $values Form values
* @return integer
*/
public function create(array $values)
{
$position = empty($values['position']) ? 0 : $values['position'];
$tags = array();
if (isset($values['tags'])) {
$tags = $values['tags'];
unset($values['tags']);
}
$this->prepare($values);
$task_id = $this->db->table(TaskModel::TABLE)->persist($values);
if ($task_id !== false) {
if ($position > 0 && $values['position'] > 1) {
$this->taskPositionModel->movePosition($values['project_id'], $task_id, $values['column_id'], $position, $values['swimlane_id'], false);
}
if (! empty($tags)) {
$this->taskTagModel->save($values['project_id'], $task_id, $tags);
}
$this->queueManager->push($this->taskEventJob->withParams(
$task_id,
array(TaskModel::EVENT_CREATE_UPDATE, TaskModel::EVENT_CREATE)
));
}
$this->hook->reference('model:task:creation:aftersave', $task_id);
return (int) $task_id;
}
/**
* Prepare data
*
* @access protected
* @param array $values Form values
*/
protected function prepare(array &$values)
{
$values = $this->dateParser->convert($values, array('date_due'), true);
$values = $this->dateParser->convert($values, array('date_started'), true);
$this->helper->model->removeFields($values, array('another_task', 'duplicate_multiple_projects'));
$this->helper->model->resetFields($values, array('creator_id', 'owner_id', 'date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent'));
if (empty($values['column_id'])) {
$values['column_id'] = $this->columnModel->getFirstColumnId($values['project_id']);
}
if (empty($values['color_id'])) {
$values['color_id'] = $this->colorModel->getDefaultColor();
}
if (empty($values['title'])) {
$values['title'] = t('Untitled');
}
// Note: Do not override the creator_id if the task is imported
if (empty($values['creator_id']) && $this->userSession->isLogged()) {
$values['creator_id'] = $this->userSession->getId();
}
$values['swimlane_id'] = empty($values['swimlane_id']) ? $this->swimlaneModel->getFirstActiveSwimlaneId($values['project_id']) : $values['swimlane_id'];
$values['date_creation'] = time();
$values['date_modification'] = $values['date_creation'];
$values['date_moved'] = $values['date_creation'];
$values['position'] = $this->taskFinderModel->countByColumnAndSwimlaneId($values['project_id'], $values['column_id'], $values['swimlane_id']) + 1;
$this->hook->reference('model:task:creation:prepare', $values);
}
}
+169
View File
@@ -0,0 +1,169 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Task Duplication
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TaskDuplicationModel extends Base
{
/**
* Fields to copy when duplicating a task
*
* @access protected
* @var string[]
*/
protected $fieldsToDuplicate = array(
'title',
'description',
'date_due',
'color_id',
'project_id',
'column_id',
'owner_id',
'score',
'priority',
'category_id',
'time_estimated',
'swimlane_id',
'recurrence_status',
'recurrence_trigger',
'recurrence_factor',
'recurrence_timeframe',
'recurrence_basedate',
'external_provider',
'external_uri',
'reference',
);
/**
* Duplicate a task to the same project
*
* @access public
* @param integer $task_id Task id
* @return boolean|integer Duplicated task id
*/
public function duplicate($task_id)
{
$values = $this->copyFields($task_id);
$values['title'] = t('[DUPLICATE]').' '.$values['title'];
$new_task_id = $this->save($task_id, $values);
if ($new_task_id !== false) {
$this->tagDuplicationModel->duplicateTaskTags($task_id, $new_task_id);
$this->taskLinkModel->create($new_task_id, $task_id, 4);
$externalLinks = $this->taskExternalLinkModel->getAll($task_id);
foreach ($externalLinks as $externalLink) {
$this->taskExternalLinkModel->create([
'task_id' => $new_task_id,
'creator_id' => $externalLink['creator_id'],
'dependency' => $externalLink['dependency'],
'title' => $externalLink['title'],
'link_type' => $externalLink['link_type'],
'url' => $externalLink['url'],
]);
}
}
$hook_values = ['source_task_id' => $task_id, 'destination_task_id' => $new_task_id];
$this->hook->reference('model:task:duplication:aftersave', $hook_values);
return $new_task_id;
}
/**
* Check if the assignee and the category are available in the destination project
*
* @access public
* @param array $values
* @return array
*/
public function checkDestinationProjectValues(array &$values)
{
// Check if the assigned user is allowed for the destination project
if ($values['owner_id'] > 0 && ! $this->projectPermissionModel->isUserAllowed($values['project_id'], $values['owner_id'])) {
$values['owner_id'] = 0;
}
// Check if the category exists for the destination project
if ($values['category_id'] > 0) {
$values['category_id'] = $this->categoryModel->getIdByName(
$values['project_id'],
$this->categoryModel->getNameById($values['category_id'])
);
}
// Check if the swimlane exists for the destination project
$values['swimlane_id'] = $this->swimlaneModel->getIdByName(
$values['project_id'],
$this->swimlaneModel->getNameById($values['swimlane_id'])
);
if ($values['swimlane_id'] == 0) {
$values['swimlane_id'] = $this->swimlaneModel->getFirstActiveSwimlaneId($values['project_id']);
}
// Check if the column exists for the destination project
if ($values['column_id'] > 0) {
$values['column_id'] = $this->columnModel->getColumnIdByTitle(
$values['project_id'],
$this->columnModel->getColumnTitleById($values['column_id'])
);
$values['column_id'] = $values['column_id'] ?: $this->columnModel->getFirstColumnId($values['project_id']);
}
// Check if priority exists for destination project
$values['priority'] = $this->projectTaskPriorityModel->getPriorityForProject(
$values['project_id'],
empty($values['priority']) ? 0 : $values['priority']
);
return $values;
}
/**
* Duplicate fields for the new task
*
* @access protected
* @param integer $task_id Task id
* @return array
*/
protected function copyFields($task_id)
{
$task = $this->taskFinderModel->getById($task_id);
$values = array();
foreach ($this->fieldsToDuplicate as $field) {
$values[$field] = $task[$field];
}
return $values;
}
/**
* Create the new task and duplicate subtasks
*
* @access protected
* @param integer $task_id Task id
* @param array $values Form values
* @return boolean|integer
*/
protected function save($task_id, array $values)
{
$new_task_id = $this->taskCreationModel->create($values);
if ($new_task_id !== false) {
$this->subtaskModel->duplicate($task_id, $new_task_id);
}
return $new_task_id;
}
}
+103
View File
@@ -0,0 +1,103 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Task External Link Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TaskExternalLinkModel 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.'.*', UserModel::TABLE.'.name AS creator_name', UserModel::TABLE.'.username AS creator_username')
->eq('task_id', $task_id)
->asc('title')
->join(UserModel::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->db->table(self::TABLE)->persist($values);
}
/**
* Modify external link
*
* @access public
* @param array $values Form values
* @return boolean
*/
public function update(array $values)
{
$values['date_modification'] = time();
$updates = $values;
unset($updates['id']);
return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($updates);
}
/**
* 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();
}
}
+115
View File
@@ -0,0 +1,115 @@
<?php
namespace Kanboard\Model;
/**
* Task File Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TaskFileModel extends FileModel
{
/**
* Table name
*
* @var string
*/
const TABLE = 'task_has_files';
/**
* Events
*
* @var string
*/
const EVENT_CREATE = 'task.file.create';
const EVENT_DESTROY = 'task.file.destroy';
/**
* Get the table
*
* @abstract
* @access protected
* @return string
*/
protected function getTable()
{
return self::TABLE;
}
/**
* Define the foreign key
*
* @abstract
* @access protected
* @return string
*/
protected function getForeignKey()
{
return 'task_id';
}
/**
* Define the path prefix
*
* @abstract
* @access protected
* @return string
*/
protected function getPathPrefix()
{
return 'tasks';
}
/**
* Get projectId from fileId
*
* @access public
* @param integer $file_id
* @return integer
*/
public function getProjectId($file_id)
{
return $this->db
->table(self::TABLE)
->eq(self::TABLE.'.id', $file_id)
->join(TaskModel::TABLE, 'id', 'task_id')
->findOneColumn(TaskModel::TABLE . '.project_id') ?: 0;
}
/**
* 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);
}
/**
* Fire file creation event
*
* @access protected
* @param integer $file_id
*/
protected function fireCreationEvent($file_id)
{
$this->queueManager->push($this->taskFileEventJob->withParams($file_id, self::EVENT_CREATE));
}
/**
* Fire file destruction event
*
* @access protected
* @param integer $file_id
*/
protected function fireDestructionEvent($file_id)
{
$this->queueManager->push($this->taskFileEventJob->withParams($file_id, self::EVENT_DESTROY));
}
}
+415
View File
@@ -0,0 +1,415 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Task Finder model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TaskFinderModel extends Base
{
/**
* Get query for project user overview
*
* @access public
* @param array $project_ids
* @param integer $is_active
* @return \PicoDb\Table
*/
public function getProjectUserOverviewQuery(array $project_ids, $is_active)
{
if (empty($project_ids)) {
$project_ids = array(-1);
}
return $this->db
->table(TaskModel::TABLE)
->columns(
TaskModel::TABLE.'.id',
TaskModel::TABLE.'.title',
TaskModel::TABLE.'.date_due',
TaskModel::TABLE.'.date_started',
TaskModel::TABLE.'.project_id',
TaskModel::TABLE.'.color_id',
TaskModel::TABLE.'.priority',
TaskModel::TABLE.'.time_spent',
TaskModel::TABLE.'.time_estimated',
ProjectModel::TABLE.'.name AS project_name',
ColumnModel::TABLE.'.title AS column_name',
UserModel::TABLE.'.username AS assignee_username',
UserModel::TABLE.'.name AS assignee_name'
)
->eq(TaskModel::TABLE.'.is_active', $is_active)
->in(ProjectModel::TABLE.'.id', $project_ids)
->join(ProjectModel::TABLE, 'id', 'project_id')
->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE);
}
/**
* Get query for assigned user tasks
*
* @access public
* @param integer $user_id User id
* @return \PicoDb\Table
*/
public function getUserQuery($user_id)
{
return $this->getExtendedQuery()
->beginOr()
->eq(TaskModel::TABLE.'.owner_id', $user_id)
->inSubquery(TaskModel::TABLE.'.id', $this->db->table(SubtaskModel::TABLE)->columns('task_id')->eq('user_id', $user_id))
->closeOr()
->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_OPEN)
->eq(ProjectModel::TABLE.'.is_active', ProjectModel::ACTIVE)
->eq(ColumnModel::TABLE.'.hide_in_dashboard', 0);
}
/**
* Extended query
*
* @access public
* @return \PicoDb\Table
*/
public function getExtendedQuery()
{
return $this->db
->table(TaskModel::TABLE)
->columns(
'(SELECT COUNT(*) FROM '.CommentModel::TABLE.' WHERE task_id=tasks.id) AS nb_comments',
'(SELECT COUNT(*) FROM '.TaskFileModel::TABLE.' WHERE task_id=tasks.id) AS nb_files',
'(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id) AS nb_subtasks',
'(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks',
'(SELECT COUNT(*) FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id) AS nb_links',
'(SELECT COUNT(*) FROM '.TaskExternalLinkModel::TABLE.' WHERE '.TaskExternalLinkModel::TABLE.'.task_id = tasks.id) AS nb_external_links',
'(SELECT DISTINCT 1 FROM '.TaskLinkModel::TABLE.' tl JOIN '.LinkModel::TABLE.' l ON tl.link_id = l.id WHERE tl.task_id = tasks.id AND l.label = '."'is a milestone of') AS is_milestone",
TaskModel::TABLE.'.id',
TaskModel::TABLE.'.reference',
TaskModel::TABLE.'.title',
TaskModel::TABLE.'.description',
TaskModel::TABLE.'.date_creation',
TaskModel::TABLE.'.date_modification',
TaskModel::TABLE.'.date_completed',
TaskModel::TABLE.'.date_started',
TaskModel::TABLE.'.date_due',
TaskModel::TABLE.'.color_id',
TaskModel::TABLE.'.project_id',
TaskModel::TABLE.'.column_id',
TaskModel::TABLE.'.swimlane_id',
TaskModel::TABLE.'.owner_id',
TaskModel::TABLE.'.creator_id',
TaskModel::TABLE.'.position',
TaskModel::TABLE.'.is_active',
TaskModel::TABLE.'.score',
TaskModel::TABLE.'.category_id',
TaskModel::TABLE.'.priority',
TaskModel::TABLE.'.date_moved',
TaskModel::TABLE.'.recurrence_status',
TaskModel::TABLE.'.recurrence_trigger',
TaskModel::TABLE.'.recurrence_factor',
TaskModel::TABLE.'.recurrence_timeframe',
TaskModel::TABLE.'.recurrence_basedate',
TaskModel::TABLE.'.recurrence_parent',
TaskModel::TABLE.'.recurrence_child',
TaskModel::TABLE.'.time_estimated',
TaskModel::TABLE.'.time_spent',
TaskModel::TABLE.'.reference',
UserModel::TABLE.'.username AS assignee_username',
UserModel::TABLE.'.name AS assignee_name',
UserModel::TABLE.'.email AS assignee_email',
UserModel::TABLE.'.avatar_path AS assignee_avatar_path',
CategoryModel::TABLE.'.name AS category_name',
CategoryModel::TABLE.'.description AS category_description',
CategoryModel::TABLE.'.color_id AS category_color_id',
ColumnModel::TABLE.'.title AS column_name',
ColumnModel::TABLE.'.position AS column_position',
SwimlaneModel::TABLE.'.name AS swimlane_name',
ProjectModel::TABLE.'.name AS project_name'
)
->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE)
->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
->join(CategoryModel::TABLE, 'id', 'category_id', TaskModel::TABLE)
->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
->join(SwimlaneModel::TABLE, 'id', 'swimlane_id', TaskModel::TABLE)
->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE);
}
/**
* Get all tasks for a given project and status
*
* @access public
* @param integer $project_id Project id
* @param integer $status_id Status id
* @return array
*/
public function getAll($project_id, $status_id = TaskModel::STATUS_OPEN)
{
return $this->db
->table(TaskModel::TABLE)
->eq(TaskModel::TABLE.'.project_id', $project_id)
->eq(TaskModel::TABLE.'.is_active', $status_id)
->asc(TaskModel::TABLE.'.id')
->findAll();
}
/**
* 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(TaskModel::STATUS_OPEN))
{
return $this->db
->table(TaskModel::TABLE)
->eq(TaskModel::TABLE.'.project_id', $project_id)
->in(TaskModel::TABLE.'.is_active', $status)
->asc(TaskModel::TABLE.'.id')
->findAllByColumn(TaskModel::TABLE.'.id');
}
/**
* Get overdue tasks query
*
* @access public
* @return \PicoDb\Table
*/
public function getOverdueTasksQuery()
{
return $this->db->table(TaskModel::TABLE)
->columns(
TaskModel::TABLE.'.id',
TaskModel::TABLE.'.title',
TaskModel::TABLE.'.date_due',
TaskModel::TABLE.'.project_id',
TaskModel::TABLE.'.creator_id',
TaskModel::TABLE.'.owner_id',
ProjectModel::TABLE.'.name AS project_name',
UserModel::TABLE.'.username AS assignee_username',
UserModel::TABLE.'.name AS assignee_name'
)
->join(ProjectModel::TABLE, 'id', 'project_id')
->join(UserModel::TABLE, 'id', 'owner_id')
->eq(ProjectModel::TABLE.'.is_active', 1)
->eq(TaskModel::TABLE.'.is_active', 1)
->neq(TaskModel::TABLE.'.date_due', 0)
->lte(TaskModel::TABLE.'.date_due', time());
}
/**
* Get a list of overdue tasks for all projects
*
* @access public
* @return array
*/
public function getOverdueTasks()
{
return $this->getOverdueTasksQuery()->findAll();
}
/**
* Get a list of overdue tasks by project
*
* @access public
* @param integer $project_id
* @return array
*/
public function getOverdueTasksByProject($project_id)
{
return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.project_id', $project_id)->findAll();
}
/**
* Get a list of overdue tasks by user
*
* @access public
* @param integer $user_id
* @return array
*/
public function getOverdueTasksByUser($user_id)
{
return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.owner_id', $user_id)->findAll();
}
/**
* Get project id for a given task
*
* @access public
* @param integer $task_id Task id
* @return integer
*/
public function getProjectId($task_id)
{
return (int) $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('project_id') ?: 0;
}
/**
* Fetch a task by the id
*
* @access public
* @param integer $task_id Task id
* @return array
*/
public function getById($task_id)
{
return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOne();
}
/**
* Fetch a task by the reference (external id)
*
* @access public
* @param integer $project_id Project id
* @param string $reference Task reference
* @return array
*/
public function getByReference($project_id, $reference)
{
return $this->db->table(TaskModel::TABLE)->eq('project_id', $project_id)->eq('reference', $reference)->findOne();
}
/**
* Get task details (fetch more information from other tables)
*
* @access public
* @param integer $task_id Task id
* @return array
*/
public function getDetails($task_id)
{
return $this->db->table(TaskModel::TABLE)
->columns(
TaskModel::TABLE.'.*',
CategoryModel::TABLE.'.name AS category_name',
SwimlaneModel::TABLE.'.name AS swimlane_name',
ProjectModel::TABLE.'.name AS project_name',
ColumnModel::TABLE.'.title AS column_title',
UserModel::TABLE.'.username AS assignee_username',
UserModel::TABLE.'.name AS assignee_name',
'uc.username AS creator_username',
'uc.name AS creator_name',
CategoryModel::TABLE.'.description AS category_description',
ColumnModel::TABLE.'.position AS column_position'
)
->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE)
->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
->join(CategoryModel::TABLE, 'id', 'category_id', TaskModel::TABLE)
->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
->join(SwimlaneModel::TABLE, 'id', 'swimlane_id', TaskModel::TABLE)
->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE)
->eq(TaskModel::TABLE.'.id', $task_id)
->findOne();
}
/**
* Get iCal query
*
* @access public
* @return \PicoDb\Table
*/
public function getICalQuery()
{
return $this->db->table(TaskModel::TABLE)
->left(UserModel::TABLE, 'ua', 'id', TaskModel::TABLE, 'owner_id')
->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
->columns(
TaskModel::TABLE.'.*',
'ua.email AS assignee_email',
'ua.name AS assignee_name',
'ua.username AS assignee_username',
'uc.email AS creator_email',
'uc.name AS creator_name',
'uc.username AS creator_username'
);
}
/**
* Count all tasks for a given project and status
*
* @access public
* @param integer $project_id Project id
* @param array $status List of status id
* @return integer
*/
public function countByProjectId($project_id, array $status = array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED))
{
return $this->db
->table(TaskModel::TABLE)
->eq('project_id', $project_id)
->in('is_active', $status)
->count();
}
/**
* Count the number of tasks for a given column and status
*
* @access public
* @param integer $project_id Project id
* @param integer $column_id Column id
* @param array $status
* @return int
*/
public function countByColumnId($project_id, $column_id, array $status = array(TaskModel::STATUS_OPEN))
{
return $this->db
->table(TaskModel::TABLE)
->eq('project_id', $project_id)
->eq('column_id', $column_id)
->in('is_active', $status)
->count();
}
/**
* Count the number of tasks for a given column and swimlane
*
* @access public
* @param integer $project_id Project id
* @param integer $column_id Column id
* @param integer $swimlane_id Swimlane id
* @return integer
*/
public function countByColumnAndSwimlaneId($project_id, $column_id, $swimlane_id)
{
return $this->db
->table(TaskModel::TABLE)
->eq('project_id', $project_id)
->eq('column_id', $column_id)
->eq('swimlane_id', $swimlane_id)
->eq('is_active', 1)
->count();
}
/**
* Return true if the task exists
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function exists($task_id)
{
return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->exists();
}
/**
* Get project token
*
* @access public
* @param integer $task_id
* @return string
*/
public function getProjectToken($task_id)
{
return $this->db
->table(TaskModel::TABLE)
->eq(TaskModel::TABLE.'.id', $task_id)
->join(ProjectModel::TABLE, 'id', 'project_id')
->findOneColumn(ProjectModel::TABLE.'.token');
}
}
+305
View File
@@ -0,0 +1,305 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* TaskLink model
*
* @package Kanboard\Model
* @author Olivier Maridat
* @author Frederic Guillot
*/
class TaskLinkModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'task_has_links';
/**
* Events
*
* @var string
*/
const EVENT_CREATE_UPDATE = 'task_internal_link.create_update';
const EVENT_DELETE = 'task_internal_link.delete';
/**
* Get projectId from $task_link_id
*
* @access public
* @param integer $task_link_id
* @return integer
*/
public function getProjectId($task_link_id)
{
return $this->db
->table(self::TABLE)
->eq(self::TABLE.'.id', $task_link_id)
->join(TaskModel::TABLE, 'id', 'task_id')
->findOneColumn(TaskModel::TABLE . '.project_id') ?: 0;
}
/**
* Get a task link
*
* @access public
* @param integer $task_link_id Task link id
* @return array
*/
public function getById($task_link_id)
{
return $this->db
->table(self::TABLE)
->columns(
self::TABLE.'.id',
self::TABLE.'.opposite_task_id',
self::TABLE.'.task_id',
self::TABLE.'.link_id',
LinkModel::TABLE.'.label',
LinkModel::TABLE.'.opposite_id AS opposite_link_id'
)
->eq(self::TABLE.'.id', $task_link_id)
->join(LinkModel::TABLE, 'id', 'link_id')
->findOne();
}
/**
* Get the opposite task link (use the unique index task_has_links_unique)
*
* @access public
* @param array $task_link
* @return array
*/
public function getOppositeTaskLink(array $task_link)
{
$opposite_link_id = $this->linkModel->getOppositeLinkId($task_link['link_id']);
return $this->db->table(self::TABLE)
->eq('opposite_task_id', $task_link['task_id'])
->eq('task_id', $task_link['opposite_task_id'])
->eq('link_id', $opposite_link_id)
->findOne();
}
/**
* Get all links attached to a task
*
* @access public
* @param integer $task_id Task id
* @return array
*/
public function getAll($task_id)
{
return $this->db
->table(self::TABLE)
->columns(
self::TABLE.'.id',
self::TABLE.'.opposite_task_id AS task_id',
LinkModel::TABLE.'.label',
TaskModel::TABLE.'.title',
TaskModel::TABLE.'.is_active',
TaskModel::TABLE.'.project_id',
TaskModel::TABLE.'.column_id',
TaskModel::TABLE.'.color_id',
TaskModel::TABLE.'.date_completed',
TaskModel::TABLE.'.date_started',
TaskModel::TABLE.'.date_due',
TaskModel::TABLE.'.time_spent AS task_time_spent',
TaskModel::TABLE.'.time_estimated AS task_time_estimated',
TaskModel::TABLE.'.owner_id AS task_assignee_id',
UserModel::TABLE.'.username AS task_assignee_username',
UserModel::TABLE.'.name AS task_assignee_name',
ColumnModel::TABLE.'.title AS column_title',
ProjectModel::TABLE.'.name AS project_name'
)
->eq(self::TABLE.'.task_id', $task_id)
->join(LinkModel::TABLE, 'id', 'link_id')
->join(TaskModel::TABLE, 'id', 'opposite_task_id')
->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE)
->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE)
->asc(LinkModel::TABLE.'.id')
->desc(ColumnModel::TABLE.'.position')
->desc(TaskModel::TABLE.'.is_active')
->asc(TaskModel::TABLE.'.position')
->asc(TaskModel::TABLE.'.id')
->findAll();
}
/**
* Get all links attached to a task grouped by label
*
* @access public
* @param integer $task_id Task id
* @return array
*/
public function getAllGroupedByLabel($task_id)
{
$links = $this->getAll($task_id);
$result = array();
foreach ($links as $link) {
if (! isset($result[$link['label']])) {
$result[$link['label']] = array();
}
$result[$link['label']][] = $link;
}
return $result;
}
/**
* Create a new link
*
* @access public
* @param integer $task_id Task id
* @param integer $opposite_task_id Opposite task id
* @param integer $link_id Link id
* @return integer|boolean
*/
public function create($task_id, $opposite_task_id, $link_id)
{
$this->db->startTransaction();
$opposite_link_id = $this->linkModel->getOppositeLinkId($link_id);
$task_link_id1 = $this->createTaskLink($task_id, $opposite_task_id, $link_id);
$task_link_id2 = $this->createTaskLink($opposite_task_id, $task_id, $opposite_link_id);
if ($task_link_id1 === false || $task_link_id2 === false) {
$this->db->cancelTransaction();
return false;
}
$this->db->closeTransaction();
$this->fireEvents(array($task_link_id1, $task_link_id2), self::EVENT_CREATE_UPDATE);
return $task_link_id1;
}
/**
* Update a task link
*
* @access public
* @param integer $task_link_id Task link id
* @param integer $task_id Task id
* @param integer $opposite_task_id Opposite task id
* @param integer $link_id Link id
* @return boolean
*/
public function update($task_link_id, $task_id, $opposite_task_id, $link_id)
{
$this->db->startTransaction();
$task_link = $this->getById($task_link_id);
$opposite_task_link = $this->getOppositeTaskLink($task_link);
$opposite_link_id = $this->linkModel->getOppositeLinkId($link_id);
$result1 = $this->updateTaskLink($task_link_id, $task_id, $opposite_task_id, $link_id);
$result2 = $this->updateTaskLink($opposite_task_link['id'], $opposite_task_id, $task_id, $opposite_link_id);
if ($result1 === false || $result2 === false) {
$this->db->cancelTransaction();
return false;
}
$this->db->closeTransaction();
$this->fireEvents(array($task_link_id, $opposite_task_link['id']), self::EVENT_CREATE_UPDATE);
return true;
}
/**
* Remove a link between two tasks
*
* @access public
* @param integer $task_link_id
* @return boolean
*/
public function remove($task_link_id)
{
$this->taskLinkEventJob->execute($task_link_id, self::EVENT_DELETE);
$this->db->startTransaction();
$link = $this->getById($task_link_id);
$link_id = $this->linkModel->getOppositeLinkId($link['link_id']);
$result1 = $this->db
->table(self::TABLE)
->eq('id', $task_link_id)
->remove();
$result2 = $this->db
->table(self::TABLE)
->eq('opposite_task_id', $link['task_id'])
->eq('task_id', $link['opposite_task_id'])
->eq('link_id', $link_id)
->remove();
if ($result1 === false || $result2 === false) {
$this->db->cancelTransaction();
return false;
}
$this->db->closeTransaction();
return true;
}
/**
* Publish events
*
* @access protected
* @param integer[] $task_link_ids
* @param string $eventName
*/
protected function fireEvents(array $task_link_ids, $eventName)
{
foreach ($task_link_ids as $task_link_id) {
$this->queueManager->push($this->taskLinkEventJob->withParams($task_link_id, $eventName));
}
}
/**
* Create task link
*
* @access protected
* @param integer $task_id
* @param integer $opposite_task_id
* @param integer $link_id
* @return integer|boolean
*/
protected function createTaskLink($task_id, $opposite_task_id, $link_id)
{
return $this->db->table(self::TABLE)->persist(array(
'task_id' => $task_id,
'opposite_task_id' => $opposite_task_id,
'link_id' => $link_id,
));
}
/**
* Update task link
*
* @access protected
* @param integer $task_link_id
* @param integer $task_id
* @param integer $opposite_task_id
* @param integer $link_id
* @return boolean
*/
protected function updateTaskLink($task_link_id, $task_id, $opposite_task_id, $link_id)
{
return $this->db->table(self::TABLE)->eq('id', $task_link_id)->update(array(
'task_id' => $task_id,
'opposite_task_id' => $opposite_task_id,
'link_id' => $link_id,
));
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace Kanboard\Model;
/**
* Task Metadata
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TaskMetadataModel extends MetadataModel
{
/**
* Get the table
*
* @abstract
* @access protected
* @return string
*/
protected function getTable()
{
return 'task_has_metadata';
}
/**
* Define the entity key
*
* @access protected
* @return string
*/
protected function getEntityKey()
{
return 'task_id';
}
}
+156
View File
@@ -0,0 +1,156 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Task Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TaskModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'tasks';
/**
* Task status
*
* @var integer
*/
const STATUS_OPEN = 1;
const STATUS_CLOSED = 0;
/**
* Events
*
* @var string
*/
const EVENT_MOVE_PROJECT = 'task.move.project';
const EVENT_MOVE_COLUMN = 'task.move.column';
const EVENT_MOVE_POSITION = 'task.move.position';
const EVENT_MOVE_SWIMLANE = 'task.move.swimlane';
const EVENT_UPDATE = 'task.update';
const EVENT_CREATE = 'task.create';
const EVENT_CLOSE = 'task.close';
const EVENT_OPEN = 'task.open';
const EVENT_CREATE_UPDATE = 'task.create_update';
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
*
* @var integer
*/
const RECURRING_STATUS_NONE = 0;
const RECURRING_STATUS_PENDING = 1;
const RECURRING_STATUS_PROCESSED = 2;
/**
* Recurrence: trigger
*
* @var integer
*/
const RECURRING_TRIGGER_FIRST_COLUMN = 0;
const RECURRING_TRIGGER_LAST_COLUMN = 1;
const RECURRING_TRIGGER_CLOSE = 2;
/**
* Recurrence: timeframe
*
* @var integer
*/
const RECURRING_TIMEFRAME_DAYS = 0;
const RECURRING_TIMEFRAME_MONTHS = 1;
const RECURRING_TIMEFRAME_YEARS = 2;
/**
* Recurrence: base date used to calculate new due date
*
* @var integer
*/
const RECURRING_BASEDATE_DUEDATE = 0;
const RECURRING_BASEDATE_TRIGGERDATE = 1;
/**
* Remove a task
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function remove($task_id)
{
if (!$this->taskFinderModel->exists($task_id)) {
return false;
}
$this->taskFileModel->removeAll($task_id);
return $this->db->table(self::TABLE)->eq('id', $task_id)->remove();
}
/**
* Get a the task id from a text
*
* Example: "Fix bug #1234" will return 1234
*
* @access public
* @param string $message Text
* @return integer
*/
public function getTaskIdFromText($message)
{
if (preg_match('!#(\d+)!i', $message, $matches) && isset($matches[1])) {
return $matches[1];
}
return 0;
}
/**
* Get task progress based on the column position
*
* @access public
* @param array $task
* @param array $columns
* @return integer
*/
public function getProgress(array $task, array $columns)
{
if ($task['is_active'] == self::STATUS_CLOSED) {
return 100;
}
$position = 0;
foreach ($columns as $column_id => $column_title) {
if ($column_id == $task['column_id']) {
break;
}
$position++;
}
return round(($position * 100) / count($columns), 1);
}
public function getOpenTaskCountBySwimlaneAndColumn($project_id)
{
return $this->db->table(self::TABLE)
->columns('swimlane_id', 'column_id', 'COUNT(*) AS nb_open_tasks')
->eq('project_id', $project_id)
->eq('is_active', 1)
->groupBy('swimlane_id', 'column_id')
->findAll();
}
}
+134
View File
@@ -0,0 +1,134 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Task Modification
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TaskModificationModel extends Base
{
/**
* Update a task
*
* @access public
* @param array $values
* @param boolean $fire_events
* @return boolean
*/
public function update(array $values, $fire_events = true)
{
$task = $this->taskFinderModel->getById($values['id']);
if (isset($values['tags_only_add_new']) && $values['tags_only_add_new'] == 1) {
$this->updateTags($values, $task, false);
} else {
$this->updateTags($values, $task);
}
$this->prepare($values);
$result = $this->db->table(TaskModel::TABLE)->eq('id', $task['id'])->update($values);
if ($fire_events && $result) {
$this->fireEvents($task, $values);
}
return $result;
}
/**
* Fire events
*
* @access protected
* @param array $task
* @param array $changes
*/
protected function fireEvents(array $task, array $changes)
{
$events = array();
if ($this->isAssigneeChanged($task, $changes)) {
$events[] = TaskModel::EVENT_ASSIGNEE_CHANGE;
} elseif ($this->isModified($task, $changes)) {
$events[] = TaskModel::EVENT_CREATE_UPDATE;
$events[] = TaskModel::EVENT_UPDATE;
}
if (! empty($events)) {
$this->queueManager->push(
$this->taskEventJob
->withParams($task['id'], $events, $changes, array(), $task)
);
}
}
/**
* Return true if the task have been modified
*
* @access protected
* @param array $task
* @param array $changes
* @return bool
*/
protected function isModified(array $task, array $changes)
{
$diff = array_diff_assoc($changes, $task);
unset($diff['date_modification']);
return count($diff) > 0;
}
/**
* Return true if the field is the only modified value
*
* @access protected
* @param array $task
* @param array $changes
* @return bool
*/
protected function isAssigneeChanged(array $task, array $changes)
{
$diff = array_diff_assoc($changes, $task);
unset($diff['date_modification']);
return isset($changes['owner_id']) && $task['owner_id'] != $changes['owner_id'] && count($diff) === 1;
}
/**
* Prepare data before task modification
*
* @access protected
* @param array $values
*/
protected function prepare(array &$values)
{
$values = $this->dateParser->convert($values, array('date_due'), true);
$values = $this->dateParser->convert($values, array('date_started'), true);
$this->helper->model->removeFields($values, array('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();
$this->hook->reference('model:task:modification:prepare', $values);
}
/**
* Update tags
*
* @access protected
* @param array $values
* @param array $original_task
*/
protected function updateTags(array &$values, array $original_task, $remove_other_tags = true)
{
if (isset($values['tags'])) {
$this->taskTagModel->save($original_task['project_id'], $values['id'], $values['tags'], $remove_other_tags);
unset($values['tags']);
unset($values['tags_only_add_new']);
}
}
}
+315
View File
@@ -0,0 +1,315 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Task Position
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TaskPositionModel extends Base
{
public function moveBottom($project_id, $task_id, $swimlane_id, $column_id)
{
$this->db->startTransaction();
$task = $this->taskFinderModel->getById($task_id);
$result = $this->db->table(TaskModel::TABLE)
->eq('project_id', $project_id)
->eq('swimlane_id', $swimlane_id)
->eq('column_id', $column_id)
->columns('MAX(position) AS pos')
->findOne();
$position = 1;
if (! empty($result)) {
$position = $result['pos'] + 1;
}
$result = $this->db->table(TaskModel::TABLE)
->eq('id', $task_id)
->eq('project_id', $project_id)
->update([
'swimlane_id' => $swimlane_id,
'column_id' => $column_id,
'position' => $position,
'date_moved' => time(),
'date_modification' => time(),
]);
$this->db->closeTransaction();
if ($result) {
$this->fireEvents($task, $column_id, $position, $swimlane_id);
}
return $result;
}
/**
* Move a task to another column or to another position
*
* @access public
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param integer $column_id Column id
* @param integer $position Position (must be >= 1)
* @param integer $swimlane_id Swimlane id
* @param boolean $fire_events Fire events
* @param bool $onlyOpen Do not move closed tasks
* @return bool
*/
public function movePosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0, $fire_events = true, $onlyOpen = true)
{
if ($position < 1) {
return false;
}
$task = $this->taskFinderModel->getById($task_id);
if ($swimlane_id == 0) {
$swimlane_id = $task['swimlane_id'];
}
if ($onlyOpen && $task['is_active'] == TaskModel::STATUS_CLOSED) {
return true;
}
$result = false;
if ($task['swimlane_id'] != $swimlane_id) {
$result = $this->saveSwimlaneChange($project_id, $task_id, $position, $task['column_id'], $column_id, $task['swimlane_id'], $swimlane_id);
} elseif ($task['column_id'] != $column_id) {
$result = $this->saveColumnChange($project_id, $task_id, $position, $swimlane_id, $task['column_id'], $column_id);
} elseif ($task['position'] != $position) {
$result = $this->savePositionChange($project_id, $task_id, $position, $column_id, $swimlane_id);
}
if ($result && $fire_events) {
$this->fireEvents($task, $column_id, $position, $swimlane_id);
}
return $result;
}
/**
* Move a task to another swimlane
*
* @access private
* @param integer $project_id
* @param integer $task_id
* @param integer $position
* @param integer $original_column_id
* @param integer $new_column_id
* @param integer $original_swimlane_id
* @param integer $new_swimlane_id
* @return boolean
*/
private function saveSwimlaneChange($project_id, $task_id, $position, $original_column_id, $new_column_id, $original_swimlane_id, $new_swimlane_id)
{
$this->db->startTransaction();
$r1 = $this->saveTaskPositions($project_id, $task_id, 0, $original_column_id, $original_swimlane_id);
$r2 = $this->saveTaskPositions($project_id, $task_id, $position, $new_column_id, $new_swimlane_id);
$r3 = $this->saveTaskTimestamps($task_id);
$this->db->closeTransaction();
return $r1 && $r2 && $r3;
}
/**
* Move a task to another column
*
* @access private
* @param integer $project_id
* @param integer $task_id
* @param integer $position
* @param integer $swimlane_id
* @param integer $original_column_id
* @param integer $new_column_id
* @return boolean
*/
private function saveColumnChange($project_id, $task_id, $position, $swimlane_id, $original_column_id, $new_column_id)
{
$this->db->startTransaction();
$r1 = $this->saveTaskPositions($project_id, $task_id, 0, $original_column_id, $swimlane_id);
$r2 = $this->saveTaskPositions($project_id, $task_id, $position, $new_column_id, $swimlane_id);
$r3 = $this->saveTaskTimestamps($task_id);
$this->db->closeTransaction();
return $r1 && $r2 && $r3;
}
/**
* Move a task to another position in the same column
*
* @access private
* @param integer $project_id
* @param integer $task_id
* @param integer $position
* @param integer $column_id
* @param integer $swimlane_id
* @return boolean
*/
private function savePositionChange($project_id, $task_id, $position, $column_id, $swimlane_id)
{
$this->db->startTransaction();
$result = $this->saveTaskPositions($project_id, $task_id, $position, $column_id, $swimlane_id);
$this->db->closeTransaction();
return $result;
}
/**
* Save all task positions for one column
*
* @access private
* @param integer $project_id
* @param integer $task_id
* @param integer $position
* @param integer $column_id
* @param integer $swimlane_id
* @return boolean
*/
private function saveTaskPositions($project_id, $task_id, $position, $column_id, $swimlane_id)
{
$tasks_ids = $this->db->table(TaskModel::TABLE)
->eq('is_active', 1)
->eq('swimlane_id', $swimlane_id)
->eq('project_id', $project_id)
->eq('column_id', $column_id)
->neq('id', $task_id)
->asc('position')
->asc('id')
->findAllByColumn('id');
$offset = 1;
foreach ($tasks_ids as $current_task_id) {
// Insert the new task
if ($position == $offset) {
if (! $this->saveTaskPosition($task_id, $offset, $column_id, $swimlane_id)) {
return false;
}
$offset++;
}
// Rewrite other tasks position
if (! $this->saveTaskPosition($current_task_id, $offset, $column_id, $swimlane_id)) {
return false;
}
$offset++;
}
// Insert the new task at the bottom and normalize bad position
if ($position >= $offset && ! $this->saveTaskPosition($task_id, $offset, $column_id, $swimlane_id)) {
return false;
}
return true;
}
/**
* Update task timestamps
*
* @access private
* @param integer $task_id
* @return bool
*/
private function saveTaskTimestamps($task_id)
{
$now = time();
return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update(array(
'date_moved' => $now,
'date_modification' => $now,
));
}
/**
* Save new task position
*
* @access private
* @param integer $task_id
* @param integer $position
* @param integer $column_id
* @param integer $swimlane_id
* @return boolean
*/
private function saveTaskPosition($task_id, $position, $column_id, $swimlane_id)
{
$result = $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update(array(
'position' => $position,
'column_id' => $column_id,
'swimlane_id' => $swimlane_id,
));
if (! $result) {
$this->db->cancelTransaction();
return false;
}
return true;
}
/**
* Fire events
*
* @access private
* @param array $task
* @param integer $new_column_id
* @param integer $new_position
* @param integer $new_swimlane_id
*/
private function fireEvents(array $task, $new_column_id, $new_position, $new_swimlane_id)
{
$changes = array(
'project_id' => $task['project_id'],
'position' => $new_position,
'column_id' => $new_column_id,
'swimlane_id' => $new_swimlane_id,
'src_column_id' => $task['column_id'],
'dst_column_id' => $new_column_id,
'date_moved' => $task['date_moved'],
'recurrence_status' => $task['recurrence_status'],
'recurrence_trigger' => $task['recurrence_trigger'],
);
if ($task['swimlane_id'] != $new_swimlane_id) {
$this->taskEventJob->execute(
$task['id'],
array(TaskModel::EVENT_MOVE_SWIMLANE),
$changes,
$changes
);
if ($task['column_id'] != $new_column_id) {
$this->taskEventJob->execute(
$task['id'],
array(TaskModel::EVENT_MOVE_COLUMN),
$changes,
$changes
);
}
} elseif ($task['column_id'] != $new_column_id) {
$this->taskEventJob->execute(
$task['id'],
array(TaskModel::EVENT_MOVE_COLUMN),
$changes,
$changes
);
} elseif ($task['position'] != $new_position) {
$this->taskEventJob->execute(
$task['id'],
array(TaskModel::EVENT_MOVE_POSITION),
$changes,
$changes
);
}
}
}
+82
View File
@@ -0,0 +1,82 @@
<?php
namespace Kanboard\Model;
/**
* Task Project Duplication
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TaskProjectDuplicationModel extends TaskDuplicationModel
{
/**
* Duplicate a task to another project
*
* @access public
* @param integer $task_id
* @param integer $project_id
* @param integer $swimlane_id
* @param integer $column_id
* @param integer $category_id
* @param integer $owner_id
* @return boolean|integer
*/
public function duplicateToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
{
$values = $this->prepare($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id);
$this->checkDestinationProjectValues($values);
$new_task_id = $this->save($task_id, $values);
if ($new_task_id !== false) {
$this->tagDuplicationModel->duplicateTaskTagsToAnotherProject($task_id, $new_task_id, $project_id);
$this->taskLinkModel->create($new_task_id, $task_id, 4);
$attachments = $this->taskFileModel->getAll($task_id);
$externalLinks = $this->taskExternalLinkModel->getAll($task_id);
foreach ($attachments as $attachment) {
$this->taskFileModel->create($new_task_id, $attachment['name'], $attachment['path'], $attachment['size']);
}
foreach ($externalLinks as $externalLink) {
$this->taskExternalLinkModel->create([
'task_id' => $new_task_id,
'creator_id' => $externalLink['creator_id'],
'dependency' => $externalLink['dependency'],
'title' => $externalLink['title'],
'link_type' => $externalLink['link_type'],
'url' => $externalLink['url'],
]);
}
}
$hook_values = ['source_task_id' => $task_id, 'destination_task_id' => $new_task_id];
$this->hook->reference('model:task:project_duplication:aftersave', $hook_values);
return $new_task_id;
}
/**
* Prepare values before duplication
*
* @access protected
* @param integer $task_id
* @param integer $project_id
* @param integer $swimlane_id
* @param integer $column_id
* @param integer $category_id
* @param integer $owner_id
* @return array
*/
protected function prepare($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id)
{
$values = $this->copyFields($task_id);
$values['project_id'] = $project_id;
$values['column_id'] = $column_id !== null ? $column_id : $values['column_id'];
$values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $values['swimlane_id'];
$values['category_id'] = $category_id !== null ? $category_id : $values['category_id'];
$values['owner_id'] = $owner_id !== null ? $owner_id : $values['owner_id'];
return $values;
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace Kanboard\Model;
/**
* Task Project Move
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TaskProjectMoveModel extends TaskDuplicationModel
{
/**
* Move a task to another project
*
* @access public
* @param integer $task_id
* @param integer $project_id
* @param integer $swimlane_id
* @param integer $column_id
* @param integer $category_id
* @param integer $owner_id
* @return boolean
*/
public function moveToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
{
$task = $this->taskFinderModel->getById($task_id);
$values = $this->prepare($project_id, $swimlane_id, $column_id, $category_id, $owner_id, $task);
$this->checkDestinationProjectValues($values);
$this->tagDuplicationModel->syncTaskTagsToAnotherProject($task_id, $project_id);
if ($this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update($values)) {
$this->queueManager->push($this->taskEventJob->withParams($task_id, array(TaskModel::EVENT_MOVE_PROJECT), $values));
}
return true;
}
/**
* Prepare new task values
*
* @access protected
* @param integer $project_id
* @param integer $swimlane_id
* @param integer $column_id
* @param integer $category_id
* @param integer $owner_id
* @param array $task
* @return array
*/
protected function prepare($project_id, $swimlane_id, $column_id, $category_id, $owner_id, array $task)
{
$values = array();
$values['is_active'] = 1;
$values['project_id'] = $project_id;
$values['column_id'] = $column_id !== null ? $column_id : $task['column_id'];
$values['position'] = $this->taskFinderModel->countByColumnId($project_id, $values['column_id']) + 1;
$values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $task['swimlane_id'];
$values['category_id'] = $category_id !== null ? $category_id : $task['category_id'];
$values['owner_id'] = $owner_id !== null ? $owner_id : $task['owner_id'];
$values['priority'] = $task['priority'];
return $values;
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php
namespace Kanboard\Model;
use DateInterval;
use DateTime;
/**
* Task Recurrence
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TaskRecurrenceModel extends TaskDuplicationModel
{
/**
* Return the list user selectable recurrence status
*
* @access public
* @return array
*/
public function getRecurrenceStatusList()
{
return array(
TaskModel::RECURRING_STATUS_NONE => t('No'),
TaskModel::RECURRING_STATUS_PENDING => t('Yes'),
);
}
/**
* Return the list recurrence triggers
*
* @access public
* @return array
*/
public function getRecurrenceTriggerList()
{
return array(
TaskModel::RECURRING_TRIGGER_FIRST_COLUMN => t('When task is moved from first column'),
TaskModel::RECURRING_TRIGGER_LAST_COLUMN => t('When task is moved to last column'),
TaskModel::RECURRING_TRIGGER_CLOSE => t('When task is closed'),
);
}
/**
* Return the list options to calculate recurrence due date
*
* @access public
* @return array
*/
public function getRecurrenceBasedateList()
{
return array(
TaskModel::RECURRING_BASEDATE_DUEDATE => t('Existing due date'),
TaskModel::RECURRING_BASEDATE_TRIGGERDATE => t('Action date'),
);
}
/**
* Return the list recurrence timeframes
*
* @access public
* @return array
*/
public function getRecurrenceTimeframeList()
{
return array(
TaskModel::RECURRING_TIMEFRAME_DAYS => t('Day(s)'),
TaskModel::RECURRING_TIMEFRAME_MONTHS => t('Month(s)'),
TaskModel::RECURRING_TIMEFRAME_YEARS => t('Year(s)'),
);
}
/**
* Duplicate recurring task
*
* @access public
* @param integer $task_id Task id
* @return boolean|integer Recurrence task id
*/
public function duplicateRecurringTask($task_id)
{
$values = $this->copyFields($task_id);
if ($values['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING) {
$values['recurrence_parent'] = $task_id;
$values['column_id'] = $this->columnModel->getFirstColumnId($values['project_id']);
$this->calculateRecurringTaskDueDate($values);
$recurring_task_id = $this->save($task_id, $values);
if ($recurring_task_id !== false) {
$this->tagDuplicationModel->duplicateTaskTags($task_id, $recurring_task_id);
$externalLinks = $this->taskExternalLinkModel->getAll($task_id);
foreach ($externalLinks as $externalLink) {
$this->taskExternalLinkModel->create([
'task_id' => $recurring_task_id,
'creator_id' => $externalLink['creator_id'],
'dependency' => $externalLink['dependency'],
'title' => $externalLink['title'],
'link_type' => $externalLink['link_type'],
'url' => $externalLink['url'],
]);
}
$parent_update = $this->db
->table(TaskModel::TABLE)
->eq('id', $task_id)
->update(array(
'recurrence_status' => TaskModel::RECURRING_STATUS_PROCESSED,
'recurrence_child' => $recurring_task_id,
));
if ($parent_update) {
return $recurring_task_id;
}
}
}
return false;
}
/**
* Calculate new due date for new recurrence task
*
* @access public
* @param array $values Task fields
*/
public function calculateRecurringTaskDueDate(array &$values)
{
if (! empty($values['date_due']) && $values['recurrence_factor'] != 0) {
if ($values['recurrence_basedate'] == TaskModel::RECURRING_BASEDATE_TRIGGERDATE) {
$values['date_due'] = time();
}
$factor = abs($values['recurrence_factor']);
$subtract = $values['recurrence_factor'] < 0;
switch ($values['recurrence_timeframe']) {
case TaskModel::RECURRING_TIMEFRAME_MONTHS:
$interval = 'P' . $factor . 'M';
break;
case TaskModel::RECURRING_TIMEFRAME_YEARS:
$interval = 'P' . $factor . 'Y';
break;
default:
$interval = 'P' . $factor . 'D';
}
$date_due = new DateTime();
$date_due->setTimestamp($values['date_due']);
$subtract ? $date_due->sub(new DateInterval($interval)) : $date_due->add(new DateInterval($interval));
$values['date_due'] = $date_due->getTimestamp();
}
}
}
+107
View File
@@ -0,0 +1,107 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
class TaskReorderModel extends Base
{
public function reorderByTaskId($projectID, $swimlaneID, $columnID, $direction)
{
$this->db->startTransaction();
$taskIDs = $this->db->table(TaskModel::TABLE)
->eq('project_id', $projectID)
->eq('swimlane_id', $swimlaneID)
->eq('column_id', $columnID)
->orderBy('id', $direction)
->findAllByColumn('id');
$this->reorderTasks($taskIDs);
$this->db->closeTransaction();
}
public function reorderByPriority($projectID, $swimlaneID, $columnID, $direction)
{
$this->db->startTransaction();
$taskIDs = $this->db->table(TaskModel::TABLE)
->eq('project_id', $projectID)
->eq('swimlane_id', $swimlaneID)
->eq('column_id', $columnID)
->orderBy('priority', $direction)
->asc('id')
->findAllByColumn('id');
$this->reorderTasks($taskIDs);
$this->db->closeTransaction();
}
public function reorderByAssigneeAndPriority($projectID, $swimlaneID, $columnID, $direction)
{
$this->db->startTransaction();
$taskIDs = $this->db->table(TaskModel::TABLE)
->eq('tasks.project_id', $projectID)
->eq('tasks.swimlane_id', $swimlaneID)
->eq('tasks.column_id', $columnID)
->asc('u.name')
->asc('u.username')
->orderBy('tasks.priority', $direction)
->left(UserModel::TABLE, 'u', 'id', TaskModel::TABLE, 'owner_id')
->findAllByColumn('tasks.id');
$this->reorderTasks($taskIDs);
$this->db->closeTransaction();
}
public function reorderByAssignee($projectID, $swimlaneID, $columnID, $direction)
{
$this->db->startTransaction();
$taskIDs = $this->db->table(TaskModel::TABLE)
->eq('tasks.project_id', $projectID)
->eq('tasks.swimlane_id', $swimlaneID)
->eq('tasks.column_id', $columnID)
->orderBy('u.name', $direction)
->orderBy('u.username', $direction)
->orderBy('u.id', $direction)
->left(UserModel::TABLE, 'u', 'id', TaskModel::TABLE, 'owner_id')
->findAllByColumn('tasks.id');
$this->reorderTasks($taskIDs);
$this->db->closeTransaction();
}
public function reorderByDueDate($projectID, $swimlaneID, $columnID, $direction)
{
$this->db->startTransaction();
$taskIDs = $this->db->table(TaskModel::TABLE)
->eq('project_id', $projectID)
->eq('swimlane_id', $swimlaneID)
->eq('column_id', $columnID)
->orderBy('date_due', $direction)
->asc('id')
->findAllByColumn('id');
$this->reorderTasks($taskIDs);
$this->db->closeTransaction();
}
protected function reorderTasks(array $taskIDs)
{
$i = 1;
foreach ($taskIDs as $taskID) {
$this->db->table(TaskModel::TABLE)
->eq('id', $taskID)
->update(['position' => $i]);
$i++;
}
}
}
+144
View File
@@ -0,0 +1,144 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Task Status
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TaskStatusModel extends Base
{
/**
* Return true if the task is closed
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function isClosed($task_id)
{
return $this->checkStatus($task_id, TaskModel::STATUS_CLOSED);
}
/**
* Return true if the task is open
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function isOpen($task_id)
{
return $this->checkStatus($task_id, TaskModel::STATUS_OPEN);
}
/**
* Mark a task closed
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function close($task_id)
{
$this->subtaskStatusModel->closeAll($task_id);
return $this->changeStatus($task_id, TaskModel::STATUS_CLOSED, time(), TaskModel::EVENT_CLOSE);
}
/**
* Mark a task open
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function open($task_id)
{
return $this->changeStatus($task_id, TaskModel::STATUS_OPEN, 0, TaskModel::EVENT_OPEN);
}
/**
* Close multiple tasks
*
* @access public
* @param array $task_ids
*/
public function closeMultipleTasks(array $task_ids)
{
foreach ($task_ids as $task_id) {
$this->close($task_id);
}
}
/**
* Close all tasks within a column/swimlane
*
* @access public
* @param integer $swimlane_id
* @param integer $column_id
*/
public function closeTasksBySwimlaneAndColumn($swimlane_id, $column_id)
{
$task_ids = $this->db
->table(TaskModel::TABLE)
->eq('swimlane_id', $swimlane_id)
->eq('column_id', $column_id)
->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_OPEN)
->findAllByColumn('id');
$this->closeMultipleTasks($task_ids);
}
/**
* Common method to change the status of task
*
* @access private
* @param integer $task_id Task id
* @param integer $status Task status
* @param integer $date_completed Timestamp
* @param string $event_name Event name
* @return boolean
*/
private function changeStatus($task_id, $status, $date_completed, $event_name)
{
if (! $this->taskFinderModel->exists($task_id)) {
return false;
}
$result = $this->db
->table(TaskModel::TABLE)
->eq('id', $task_id)
->update(array(
'is_active' => $status,
'date_completed' => $date_completed,
'date_modification' => time(),
));
if ($result) {
$this->queueManager->push($this->taskEventJob->withParams($task_id, array($event_name)));
}
return $result;
}
/**
* Check the status of a task
*
* @access private
* @param integer $task_id Task id
* @param integer $status Task status
* @return boolean
*/
private function checkStatus($task_id, $status)
{
return $this->db
->table(TaskModel::TABLE)
->eq('id', $task_id)
->eq('is_active', $status)
->exists();
}
}
+189
View File
@@ -0,0 +1,189 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Class TaskTagModel
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TaskTagModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'task_has_tags';
/**
* Get all tags not available in a project
*
* @access public
* @param integer $task_id
* @param integer $project_id
* @return array
*/
public function getTagsByTaskNotAvailableInProject($task_id, $project_id)
{
return $this->db->table(TagModel::TABLE)
->eq(self::TABLE.'.task_id', $task_id)
->notIn(TagModel::TABLE.'.project_id', array(0, $project_id))
->join(self::TABLE, 'tag_id', 'id')
->findAll();
}
/**
* Get all tags associated to a task
*
* @access public
* @param integer $task_id
* @return array
*/
public function getTagsByTask($task_id)
{
return $this->db->table(TagModel::TABLE)
->columns(TagModel::TABLE.'.id', TagModel::TABLE.'.name', TagModel::TABLE.'.color_id')
->eq(self::TABLE.'.task_id', $task_id)
->join(self::TABLE, 'tag_id', 'id')
->findAll();
}
/**
* Get all tags associated to a list of tasks
*
* @access public
* @param integer[] $task_ids
* @return array
*/
public function getTagsByTaskIds($task_ids)
{
if (empty($task_ids)) {
return array();
}
$tags = $this->db->table(TagModel::TABLE)
->columns(TagModel::TABLE.'.id', TagModel::TABLE.'.name', TagModel::TABLE.'.color_id', self::TABLE.'.task_id')
->in(self::TABLE.'.task_id', $task_ids)
->join(self::TABLE, 'tag_id', 'id')
->asc(TagModel::TABLE.'.name')
->findAll();
return array_column_index($tags, 'task_id');
}
/**
* Get dictionary of tags
*
* @access public
* @param integer $task_id
* @return array
*/
public function getList($task_id)
{
$tags = $this->getTagsByTask($task_id);
return array_column($tags, 'name', 'id');
}
/**
* Add or update a list of tags to a task
*
* @access public
* @param integer $project_id
* @param integer $task_id
* @param string[] $tags
* @return boolean
*/
public function save($project_id, $task_id, array $tags, $remove_other_tags = true)
{
$task_tags = $this->getList($task_id);
$tags = array_filter($tags);
if ($remove_other_tags) {
return $this->associateTags($project_id, $task_id, $task_tags, $tags) &&
$this->dissociateTags($task_id, $task_tags, $tags);
} else {
return $this->associateTags($project_id, $task_id, $task_tags, $tags);
}
}
/**
* Associate a tag to a task
*
* @access public
* @param integer $task_id
* @param integer $tag_id
* @return boolean
*/
public function associateTag($task_id, $tag_id)
{
return $this->db->table(self::TABLE)->insert(array(
'task_id' => $task_id,
'tag_id' => $tag_id,
));
}
/**
* Dissociate a tag from a task
*
* @access public
* @param integer $task_id
* @param integer $tag_id
* @return boolean
*/
public function dissociateTag($task_id, $tag_id)
{
return $this->db->table(self::TABLE)
->eq('task_id', $task_id)
->eq('tag_id', $tag_id)
->remove();
}
/**
* Associate missing tags
*
* @access protected
* @param integer $project_id
* @param integer $task_id
* @param array $task_tags
* @param string[] $tags
* @return bool
*/
protected function associateTags($project_id, $task_id, $task_tags, $tags)
{
foreach ($tags as $tag) {
$tag_id = $this->tagModel->findOrCreateTag($project_id, $tag);
if (! isset($task_tags[$tag_id]) && ! $this->associateTag($task_id, $tag_id)) {
return false;
}
}
return true;
}
/**
* Dissociate removed tags
*
* @access protected
* @param integer $task_id
* @param array $task_tags
* @param string[] $tags
* @return bool
*/
protected function dissociateTags($task_id, $task_tags, $tags)
{
foreach ($task_tags as $tag_id => $tag) {
if (! in_array($tag, $tags)) {
if (! $this->dissociateTag($task_id, $tag_id)) {
return false;
}
}
}
return true;
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Class Theme
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class ThemeModel extends Base
{
/**
* Get available theme
*
* @access public
* @return array
*/
public function getThemes()
{
return [
'light' => t('Light theme'),
'dark' => t('Dark theme'),
'auto' => t('Automatic theme - Sync with system'),
];
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Class Timezone
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TimezoneModel extends Base
{
/**
* Get available timezones
*
* @access public
* @param boolean $prepend Prepend a default value
* @return array
*/
public function getTimezones($prepend = false)
{
$timezones = timezone_identifiers_list();
$listing = array_combine(array_values($timezones), $timezones);
if ($prepend) {
return array('' => t('Application default')) + $listing;
}
return $listing;
}
/**
* Get current timezone
*
* @access public
* @return string
*/
public function getCurrentTimezone()
{
return $this->userSession->getTimezone() ?: $this->configModel->get('application_timezone', 'UTC');
}
/**
* Set timezone
*
* @access public
*/
public function setCurrentTimezone()
{
date_default_timezone_set($this->getCurrentTimezone());
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* Transition
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class TransitionModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'transitions';
/**
* Save transition event
*
* @access public
* @param integer $user_id
* @param array $task_event
* @return bool
*/
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_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']
));
}
/**
* Get time spent by task for each column
*
* @access public
* @param integer $task_id
* @return array
*/
public function getTimeSpentByTask($task_id)
{
return $this->db
->hashtable(self::TABLE)
->groupBy('src_column_id')
->eq('task_id', $task_id)
->getAll('src_column_id', 'SUM(time_spent) AS time_spent');
}
/**
* Get all transitions by task
*
* @access public
* @param integer $task_id
* @return array
*/
public function getAllByTask($task_id)
{
return $this->db->table(self::TABLE)
->columns(
'src.title as src_column',
'dst.title as dst_column',
UserModel::TABLE.'.name',
UserModel::TABLE.'.username',
self::TABLE.'.user_id',
self::TABLE.'.date',
self::TABLE.'.time_spent'
)
->eq('task_id', $task_id)
->desc('date')
->join(UserModel::TABLE, 'id', 'user_id')
->join(ColumnModel::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src')
->join(ColumnModel::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst')
->findAll();
}
/**
* Get all transitions by project
*
* @access public
* @param integer $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 getAllByProjectAndDate($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(self::TABLE)
->columns(
TaskModel::TABLE.'.id',
TaskModel::TABLE.'.title',
'src.title as src_column',
'dst.title as dst_column',
UserModel::TABLE.'.name',
UserModel::TABLE.'.username',
self::TABLE.'.user_id',
self::TABLE.'.date',
self::TABLE.'.time_spent'
)
->gte('date', $from)
->lte('date', $to)
->eq(self::TABLE.'.project_id', $project_id)
->desc('date')
->desc(self::TABLE.'.id')
->join(TaskModel::TABLE, 'id', 'task_id')
->join(UserModel::TABLE, 'id', 'user_id')
->join(ColumnModel::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src')
->join(ColumnModel::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst')
->findAll();
}
}
+105
View File
@@ -0,0 +1,105 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* User Locking Model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class UserLockingModel extends Base
{
/**
* Get the number of failed login for the user
*
* @access public
* @param string $username
* @return integer
*/
public function getFailedLogin($username)
{
return (int) $this->db->table(UserModel::TABLE)
->eq('username', $username)
->findOneColumn('nb_failed_login');
}
/**
* Reset to 0 the counter of failed login
*
* @access public
* @param string $username
* @return boolean
*/
public function resetFailedLogin($username)
{
return $this->db->table(UserModel::TABLE)
->eq('username', $username)
->update(array(
'nb_failed_login' => 0,
'lock_expiration_date' => 0,
));
}
/**
* Increment failed login counter
*
* @access public
* @param string $username
* @return boolean
*/
public function incrementFailedLogin($username)
{
return $this->db->table(UserModel::TABLE)
->eq('username', $username)
->increment('nb_failed_login', 1);
}
/**
* Check if the account is locked
*
* @access public
* @param string $username
* @return boolean
*/
public function isLocked($username)
{
return $this->db->table(UserModel::TABLE)
->eq('username', $username)
->neq('lock_expiration_date', 0)
->gte('lock_expiration_date', time())
->exists();
}
/**
* Lock the account for the specified duration
*
* @access public
* @param string $username Username
* @param integer $duration Duration in minutes
* @return boolean
*/
public function lock($username, $duration = 15)
{
return $this->db->table(UserModel::TABLE)
->eq('username', $username)
->update(array(
'lock_expiration_date' => time() + $duration * 60
));
}
/**
* Return true if the captcha must be shown
*
* @access public
* @param string $username
* @param integer $tries
* @return boolean
*/
public function hasCaptcha($username, $tries = BRUTEFORCE_CAPTCHA)
{
return $this->getFailedLogin($username) >= $tries;
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Kanboard\Model;
/**
* User Metadata
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class UserMetadataModel extends MetadataModel
{
const KEY_COMMENT_SORTING_DIRECTION = 'comment.sorting.direction';
const KEY_BOARD_COLLAPSED = 'board.collapsed.';
/**
* Get the table
*
* @abstract
* @access protected
* @return string
*/
protected function getTable()
{
return 'user_has_metadata';
}
/**
* Define the entity key
*
* @access protected
* @return string
*/
protected function getEntityKey()
{
return 'user_id';
}
}
+448
View File
@@ -0,0 +1,448 @@
<?php
namespace Kanboard\Model;
use PicoDb\Database;
use Kanboard\Core\Base;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
use InvalidArgumentException;
/**
* User model
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class UserModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'users';
/**
* Id used for everybody (filtering)
*
* @var integer
*/
const EVERYBODY_ID = -1;
public function isValidSession($userID, $sessionRole)
{
return $this->db->table(self::TABLE)
->eq('id', $userID)
->eq('is_active', 1)
->eq('role', $sessionRole)
->exists();
}
public function has2FA($username)
{
return $this->db->table(self::TABLE)
->eq('username', $username)
->eq('is_active', 1)
->eq('twofactor_activated', 1)
->exists();
}
/**
* Return true if the user exists
*
* @access public
* @param integer $user_id User id
* @return boolean
*/
public function exists($user_id)
{
return $this->db->table(self::TABLE)->eq('id', $user_id)->exists();
}
/**
* 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
* @return \PicoDb\Table
*/
public function getQuery()
{
return $this->db->table(self::TABLE);
}
/**
* Return true is the given user id is administrator
*
* @access public
* @param integer $user_id User id
* @return boolean
*/
public function isAdmin($user_id)
{
return $this->userSession->isAdmin() || // Avoid SQL query if connected
$this->db
->table(UserModel::TABLE)
->eq('id', $user_id)
->eq('role', Role::APP_ADMIN)
->exists();
}
/**
* Get a specific user by id
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function getById($user_id)
{
return $this->db->table(self::TABLE)->eq('id', $user_id)->findOne();
}
/**
* Get a specific user by the Google id
*
* @access public
* @param string $externalIdColumn
* @param string $externalId
* @return array|boolean
*/
public function getByExternalId($externalIdColumn, $externalId)
{
if (empty($externalId)) {
return false;
}
if (! $this->db->isValidIdentifier($externalIdColumn)) {
throw new InvalidArgumentException('Invalid external id column name');
}
return $this->db->table(self::TABLE)->eq($externalIdColumn, $externalId)->findOne();
}
/**
* Get a specific user by the username
*
* @access public
* @param string $username Username
* @return array
*/
public function getByUsername($username)
{
return $this->db->table(self::TABLE)->eq('username', $username)->findOne();
}
/**
* Get user_id by username
*
* @access public
* @param string $username Username
* @return integer
*/
public function getIdByUsername($username)
{
return $this->db->table(self::TABLE)->eq('username', $username)->findOneColumn('id');
}
/**
* Get a specific user by the email address
*
* @access public
* @param string $email Email
* @return array|boolean
*/
public function getByEmail($email)
{
if (empty($email)) {
return false;
}
return $this->db->table(self::TABLE)->eq('email', $email)->findOne();
}
/**
* Fetch user by using the token
*
* @access public
* @param string $token Token
* @return array|boolean
*/
public function getByToken($token)
{
if (empty($token)) {
return false;
}
return $this->db
->table(self::TABLE)
->eq('token', $token)
->eq('is_active', 1)
->findOne();
}
/**
* Get all users
*
* @access public
* @return array
*/
public function getAll()
{
return $this->getQuery()->asc('username')->findAll();
}
/**
* Get the number of users
*
* @access public
* @return integer
*/
public function count()
{
return $this->db->table(self::TABLE)->count();
}
/**
* List all users (key-value pairs with id/username)
*
* @access public
* @param boolean $prepend Prepend "All users"
* @return array
*/
public function getActiveUsersList($prepend = false)
{
$users = $this->db->table(self::TABLE)->eq('is_active', 1)->columns('id', 'username', 'name')->findAll();
$listing = $this->prepareList($users);
if ($prepend) {
return array(UserModel::EVERYBODY_ID => t('Everybody')) + $listing;
}
return $listing;
}
/**
* Common method to prepare a user list
*
* @access public
* @param array $users Users list (from database)
* @return array Formated list
*/
public function prepareList(array $users)
{
$result = array();
foreach ($users as $user) {
$result[$user['id']] = $this->helper->user->getFullname($user);
}
asort($result);
return $result;
}
/**
* Prepare values before an update or a create
*
* @access public
* @param array $values Form values
*/
public function prepare(array &$values)
{
if (isset($values['password'])) {
if (! empty($values['password'])) {
$values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT);
} else {
unset($values['password']);
}
}
if (isset($values['username'])) {
$values['username'] = trim($values['username']);
}
$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'));
}
/**
* Add a new user in the database
*
* @access public
* @param array $values Form values
* @return boolean|integer
*/
public function create(array $values)
{
$this->prepare($values);
return $this->db->table(self::TABLE)->persist($values);
}
/**
* Modify a new user
*
* @access public
* @param array $values Form values
* @return boolean
*/
public function update(array $values)
{
$this->prepare($values);
$updates = $values;
unset($updates['id']);
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($updates);
$this->userSession->refresh($values['id']);
return $result;
}
/**
* Disable a specific user
*
* @access public
* @param integer $user_id
* @return boolean
*/
public function disable($user_id)
{
$this->db->startTransaction();
$result1 = $this->db->table(self::TABLE)->eq('id', $user_id)->update(array(
'is_active' => 0,
'token' => '',
));
$result2 = $this->db->table(ProjectModel::TABLE)->eq('is_private', 1)->eq('owner_id', $user_id)->update(array('is_active' => 0));
$this->db->closeTransaction();
return $result1 && $result2;
}
/**
* 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
* @param integer $user_id User id
* @return boolean
*/
public function remove($user_id)
{
$this->avatarFileModel->remove($user_id);
return $this->db->transaction(function (Database $db) use ($user_id) {
// All assigned tasks are now unassigned (no foreign key)
if (! $db->table(TaskModel::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => 0))) {
return false;
}
// All assigned subtasks are now unassigned (no foreign key)
if (! $db->table(SubtaskModel::TABLE)->eq('user_id', $user_id)->update(array('user_id' => 0))) {
return false;
}
// All comments are not assigned anymore (no foreign key)
if (! $db->table(CommentModel::TABLE)->eq('user_id', $user_id)->update(array('user_id' => 0))) {
return false;
}
// All private projects are removed
$project_ids = $db->table(ProjectModel::TABLE)
->eq('is_private', 1)
->eq(ProjectUserRoleModel::TABLE.'.user_id', $user_id)
->join(ProjectUserRoleModel::TABLE, 'project_id', 'id')
->findAllByColumn(ProjectModel::TABLE.'.id');
if (! empty($project_ids)) {
$db->table(ProjectModel::TABLE)->in('id', $project_ids)->remove();
}
// Finally remove the user
if (! $db->table(UserModel::TABLE)->eq('id', $user_id)->remove()) {
return false;
}
});
}
/**
* Enable public access for a user
*
* @access public
* @param integer $user_id User id
* @return bool
*/
public function enablePublicAccess($user_id)
{
return $this->db
->table(self::TABLE)
->eq('id', $user_id)
->save(array('token' => Token::getToken()));
}
/**
* Disable public access for a user
*
* @access public
* @param integer $user_id User id
* @return bool
*/
public function disablePublicAccess($user_id)
{
return $this->db
->table(self::TABLE)
->eq('id', $user_id)
->save(array('token' => ''));
}
/**
* Get or create user id by using an external id (Google, GitHub, etc.)
*
* @param string $username Username
* @param string $name Full name
* @param string $externalIdColumn Column name for the external id (e.g. google_id, github_id, etc.)
* @param string $externalId External id (e.g. Google id, GitHub id, etc.)
* @return integer User id
*/
public function getOrCreateExternalUserId($username, $name, $externalIdColumn, $externalId)
{
if (! $this->db->isValidIdentifier($externalIdColumn)) {
throw new InvalidArgumentException('Invalid external id column name');
}
$userId = $this->db->table(self::TABLE)->eq($externalIdColumn, $externalId)->findOneColumn('id');
if (empty($userId)) {
$userId = $this->create(array(
'username' => $username,
'name' => $name,
'is_ldap_user' => 1,
$externalIdColumn => $externalId,
));
}
return $userId;
}
}
+206
View File
@@ -0,0 +1,206 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* User Notification Filter
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class UserNotificationFilterModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const PROJECT_TABLE = 'user_has_notifications';
/**
* User filters
*
* @var integer
*/
const FILTER_NONE = 1;
const FILTER_ASSIGNEE = 2;
const FILTER_CREATOR = 3;
const FILTER_BOTH = 4;
/**
* Get the list of filters
*
* @access public
* @return array
*/
public function getFilters()
{
return array(
self::FILTER_NONE => t('All tasks'),
self::FILTER_ASSIGNEE => t('Only for tasks assigned to me'),
self::FILTER_CREATOR => t('Only for tasks created by me'),
self::FILTER_BOTH => t('Only for tasks created by me and tasks assigned to me'),
);
}
/**
* Get user selected filter
*
* @access public
* @param integer $user_id
* @return integer
*/
public function getSelectedFilter($user_id)
{
return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->findOneColumn('notifications_filter');
}
/**
* Save selected filter for a user
*
* @access public
* @param integer $user_id
* @param string $filter
* @return boolean
*/
public function saveFilter($user_id, $filter)
{
return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->update(array(
'notifications_filter' => $filter,
));
}
/**
* Get user selected projects
*
* @access public
* @param integer $user_id
* @return array
*/
public function getSelectedProjects($user_id)
{
return $this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->findAllByColumn('project_id');
}
/**
* Save selected projects for a user
*
* @access public
* @param integer $user_id
* @param integer[] $project_ids
* @return boolean
*/
public function saveSelectedProjects($user_id, array $project_ids)
{
$results = array();
$this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->remove();
foreach ($project_ids as $project_id) {
$results[] = $this->db->table(self::PROJECT_TABLE)->insert(array(
'user_id' => $user_id,
'project_id' => $project_id,
));
}
return !in_array(false, $results, true);
}
/**
* Return true if the user should receive notification
*
* @access public
* @param array $user
* @param array $event_data
* @return boolean
*/
public function shouldReceiveNotification(array $user, array $event_data)
{
$filters = array(
'filterNone',
'filterAssignee',
'filterCreator',
'filterBoth',
);
foreach ($filters as $filter) {
if ($this->$filter($user, $event_data)) {
return $this->filterProject($user, $event_data);
}
}
return false;
}
/**
* Return true if the user will receive all notifications
*
* @access public
* @param array $user
* @return boolean
*/
public function filterNone(array $user)
{
return $user['notifications_filter'] == self::FILTER_NONE;
}
/**
* Return true if the user is the assignee and selected the filter "assignee"
*
* @access public
* @param array $user
* @param array $event_data
* @return boolean
*/
public function filterAssignee(array $user, array $event_data)
{
return $user['notifications_filter'] == self::FILTER_ASSIGNEE && $event_data['task']['owner_id'] == $user['id'];
}
/**
* Return true if the user is the creator and enabled the filter "creator"
*
* @access public
* @param array $user
* @param array $event_data
* @return boolean
*/
public function filterCreator(array $user, array $event_data)
{
return $user['notifications_filter'] == self::FILTER_CREATOR && $event_data['task']['creator_id'] == $user['id'];
}
/**
* Return true if the user is the assignee or the creator and selected the filter "both"
*
* @access public
* @param array $user
* @param array $event_data
* @return boolean
*/
public function filterBoth(array $user, array $event_data)
{
return $user['notifications_filter'] == self::FILTER_BOTH &&
($event_data['task']['creator_id'] == $user['id'] || $event_data['task']['owner_id'] == $user['id']);
}
/**
* Return true if the user want to receive notification for the selected project
*
* @access public
* @param array $user
* @param array $event_data
* @return boolean
*/
public function filterProject(array $user, array $event_data)
{
$projects = $this->getSelectedProjects($user['id']);
if (! empty($projects)) {
return in_array($event_data['task']['project_id'], $projects);
}
return true;
}
}
+183
View File
@@ -0,0 +1,183 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
use Kanboard\Core\Translator;
/**
* User Notification
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class UserNotificationModel extends Base
{
/**
* Send notifications to people
*
* @access public
* @param string $event_name
* @param array $event_data
*/
public function sendNotifications($event_name, array $event_data)
{
$users = $this->getUsersWithNotificationEnabled($event_data['task']['project_id'], $this->userSession->getId());
foreach ($users as $user) {
if ($this->userNotificationFilterModel->shouldReceiveNotification($user, $event_data)) {
$this->sendUserNotification($user, $event_name, $event_data);
}
}
}
/**
* Send notification to someone
*
* @access public
* @param array $user User
* @param string $event_name
* @param array $event_data
*/
public function sendUserNotification(array $user, $event_name, array $event_data)
{
$loadedLocales = Translator::$locales;
Translator::unload();
// Use the user language otherwise use the application language (do not use the session language)
if (! empty($user['language'])) {
Translator::load($user['language']);
} else {
Translator::load($this->configModel->get('application_language', 'en_US'));
}
foreach ($this->userNotificationTypeModel->getSelectedTypes($user['id']) as $type) {
$this->userNotificationTypeModel->getType($type)->notifyUser($user, $event_name, $event_data);
}
// Restore locales
Translator::$locales = $loadedLocales;
}
/**
* Get a list of people with notifications enabled
*
* @access public
* @param integer $project_id Project id
* @param integer $exclude_user_id User id to exclude
* @return array
*/
public function getUsersWithNotificationEnabled($project_id, $exclude_user_id = 0)
{
$users = array();
$members = $this->getProjectUserMembersWithNotificationEnabled($project_id, $exclude_user_id);
$groups = $this->getProjectGroupMembersWithNotificationEnabled($project_id, $exclude_user_id);
foreach (array_merge($members, $groups) as $user) {
if (! isset($users[$user['id']])) {
$users[$user['id']] = $user;
}
}
return array_values($users);
}
/**
* Enable notification for someone
*
* @access public
* @param integer $user_id
* @return boolean
*/
public function enableNotification($user_id)
{
return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->update(array('notifications_enabled' => 1));
}
/**
* Disable notification for someone
*
* @access public
* @param integer $user_id
* @return boolean
*/
public function disableNotification($user_id)
{
return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->update(array('notifications_enabled' => 0));
}
/**
* Save settings for the given user
*
* @access public
* @param integer $user_id User id
* @param array $values Form values
*/
public function saveSettings($user_id, array $values)
{
$types = empty($values['notification_types']) ? array() : array_keys($values['notification_types']);
if (! empty($types)) {
$this->enableNotification($user_id);
} else {
$this->disableNotification($user_id);
}
$filter = empty($values['notifications_filter']) ? UserNotificationFilterModel::FILTER_BOTH : $values['notifications_filter'];
$project_ids = empty($values['notification_projects']) ? array() : array_keys($values['notification_projects']);
$this->userNotificationFilterModel->saveFilter($user_id, $filter);
$this->userNotificationFilterModel->saveSelectedProjects($user_id, $project_ids);
$this->userNotificationTypeModel->saveSelectedTypes($user_id, $types);
}
/**
* Read user settings to display the form
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function readSettings($user_id)
{
$values = $this->db->table(UserModel::TABLE)->eq('id', $user_id)->columns('notifications_enabled', 'notifications_filter')->findOne();
$values['notification_types'] = $this->userNotificationTypeModel->getSelectedTypes($user_id);
$values['notification_projects'] = $this->userNotificationFilterModel->getSelectedProjects($user_id);
return $values;
}
/**
* Get a list of group members with notification enabled
*
* @access private
* @param integer $project_id Project id
* @param integer $exclude_user_id User id to exclude
* @return array
*/
private function getProjectUserMembersWithNotificationEnabled($project_id, $exclude_user_id)
{
return $this->db
->table(ProjectUserRoleModel::TABLE)
->columns(UserModel::TABLE.'.id', UserModel::TABLE.'.username', UserModel::TABLE.'.name', UserModel::TABLE.'.email', UserModel::TABLE.'.language', UserModel::TABLE.'.notifications_filter')
->join(UserModel::TABLE, 'id', 'user_id')
->eq(ProjectUserRoleModel::TABLE.'.project_id', $project_id)
->eq(UserModel::TABLE.'.notifications_enabled', '1')
->eq(UserModel::TABLE.'.is_active', 1)
->neq(UserModel::TABLE.'.id', $exclude_user_id)
->findAll();
}
private function getProjectGroupMembersWithNotificationEnabled($project_id, $exclude_user_id)
{
return $this->db
->table(ProjectGroupRoleModel::TABLE)
->columns(UserModel::TABLE.'.id', UserModel::TABLE.'.username', UserModel::TABLE.'.name', UserModel::TABLE.'.email', UserModel::TABLE.'.language', UserModel::TABLE.'.notifications_filter')
->join(GroupMemberModel::TABLE, 'group_id', 'group_id', ProjectGroupRoleModel::TABLE)
->join(UserModel::TABLE, 'id', 'user_id', GroupMemberModel::TABLE)
->eq(ProjectGroupRoleModel::TABLE.'.project_id', $project_id)
->eq(UserModel::TABLE.'.notifications_enabled', '1')
->neq(UserModel::TABLE.'.id', $exclude_user_id)
->eq(UserModel::TABLE.'.is_active', 1)
->findAll();
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace Kanboard\Model;
/**
* User Notification Type
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class UserNotificationTypeModel extends NotificationTypeModel
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'user_has_notification_types';
/**
* Get selected notification types for a given user
*
* @access public
* @param integer $user_id
* @return array
*/
public function getSelectedTypes($user_id)
{
$types = $this->db->table(self::TABLE)->eq('user_id', $user_id)->asc('notification_type')->findAllByColumn('notification_type');
return $this->filterTypes($types);
}
/**
* Save notification types for a given user
*
* @access public
* @param integer $user_id
* @param string[] $types
* @return boolean
*/
public function saveSelectedTypes($user_id, array $types)
{
$results = array();
$this->db->table(self::TABLE)->eq('user_id', $user_id)->remove();
foreach ($types as $type) {
$results[] = $this->db->table(self::TABLE)->insert(array('user_id' => $user_id, 'notification_type' => $type));
}
return ! in_array(false, $results, true);
}
}
+117
View File
@@ -0,0 +1,117 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
/**
* User Unread Notification
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class UserUnreadNotificationModel extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'user_has_unread_notifications';
/**
* Add unread notification to someone
*
* @access public
* @param integer $user_id
* @param string $event_name
* @param array $event_data
*/
public function create($user_id, $event_name, array $event_data)
{
$this->db->table(self::TABLE)->insert(array(
'user_id' => $user_id,
'date_creation' => time(),
'event_name' => $event_name,
'event_data' => json_encode($event_data),
));
}
/**
* Get one notification
*
* @param integer $notification_id
* @return array|null
*/
public function getById($notification_id)
{
$notification = $this->db->table(self::TABLE)->eq('id', $notification_id)->findOne();
if (! empty($notification)) {
$this->unserialize($notification);
}
return $notification;
}
/**
* Get all notifications for a user
*
* @access public
* @param integer $user_id
* @return array
*/
public function getAll($user_id)
{
$events = $this->db->table(self::TABLE)->eq('user_id', $user_id)->asc('date_creation')->findAll();
foreach ($events as &$event) {
$this->unserialize($event);
}
return $events;
}
/**
* Mark a notification as read
*
* @access public
* @param integer $user_id
* @param integer $notification_id
* @return boolean
*/
public function markAsRead($user_id, $notification_id)
{
return $this->db->table(self::TABLE)->eq('id', $notification_id)->eq('user_id', $user_id)->remove();
}
/**
* Mark all notifications as read for a user
*
* @access public
* @param integer $user_id
* @return boolean
*/
public function markAllAsRead($user_id)
{
return $this->db->table(self::TABLE)->eq('user_id', $user_id)->remove();
}
/**
* Return true if the user as unread notifications
*
* @access public
* @param integer $user_id
* @return boolean
*/
public function hasNotifications($user_id)
{
return $this->db->table(self::TABLE)->eq('user_id', $user_id)->exists();
}
private function unserialize(&$event)
{
$event['event_data'] = json_decode($event['event_data'], true);
$event['title'] = $this->notificationModel->getTitleWithoutAuthor($event['event_name'], $event['event_data']);
}
}