看板初始化提交
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user