summaryrefslogtreecommitdiff
path: root/app/Model
diff options
context:
space:
mode:
Diffstat (limited to 'app/Model')
-rw-r--r--app/Model/Acl.php4
-rw-r--r--app/Model/Authentication.php7
-rw-r--r--app/Model/Base.php2
-rw-r--r--app/Model/Link.php234
-rw-r--r--app/Model/ProjectActivity.php8
-rw-r--r--app/Model/ProjectPermission.php19
-rw-r--r--app/Model/SubtaskTimeTracking.php133
-rw-r--r--app/Model/TaskFilter.php32
-rw-r--r--app/Model/TaskFinder.php1
-rw-r--r--app/Model/TaskLink.php142
-rw-r--r--app/Model/User.php12
11 files changed, 583 insertions, 11 deletions
diff --git a/app/Model/Acl.php b/app/Model/Acl.php
index b6c0e6b8..4d4a22a7 100644
--- a/app/Model/Acl.php
+++ b/app/Model/Acl.php
@@ -38,7 +38,8 @@ class Acl extends Base
'project' => array('show', 'tasks', 'search', 'activity'),
'subtask' => '*',
'task' => '*',
- 'calendar' => array('show', 'events', 'save'),
+ 'tasklink' => '*',
+ 'calendar' => array('show', 'project', 'save'),
);
/**
@@ -67,6 +68,7 @@ class Acl extends Base
'app' => array('dashboard'),
'user' => array('index', 'create', 'save', 'remove'),
'config' => '*',
+ 'link' => '*',
'project' => array('remove'),
);
diff --git a/app/Model/Authentication.php b/app/Model/Authentication.php
index 92898cd5..86c1c43f 100644
--- a/app/Model/Authentication.php
+++ b/app/Model/Authentication.php
@@ -42,6 +42,13 @@ class Authentication extends Base
// If the user is already logged it's ok
if ($this->userSession->isLogged()) {
+ // Check if the user session match an existing user
+ if (! $this->user->exists($this->userSession->getId())) {
+ $this->backend('rememberMe')->destroy($this->userSession->getId());
+ $this->session->close();
+ return false;
+ }
+
// We update each time the RememberMe cookie tokens
if ($this->backend('rememberMe')->hasCookie()) {
$this->backend('rememberMe')->refresh();
diff --git a/app/Model/Base.php b/app/Model/Base.php
index 319e53fc..f836231c 100644
--- a/app/Model/Base.php
+++ b/app/Model/Base.php
@@ -25,6 +25,7 @@ use Pimple\Container;
* @property \Model\File $file
* @property \Model\Helper $helper
* @property \Model\LastLogin $lastLogin
+ * @property \Model\Link $link
* @property \Model\Notification $notification
* @property \Model\Project $project
* @property \Model\ProjectDuplication $projectDuplication
@@ -38,6 +39,7 @@ use Pimple\Container;
* @property \Model\TaskExport $taskExport
* @property \Model\TaskFinder $taskFinder
* @property \Model\TaskHistory $taskHistory
+ * @property \Model\TaskLink $taskLink
* @property \Model\TaskPosition $taskPosition
* @property \Model\TaskValidator $taskValidator
* @property \Model\TimeTracking $timeTracking
diff --git a/app/Model/Link.php b/app/Model/Link.php
new file mode 100644
index 00000000..87ba49c4
--- /dev/null
+++ b/app/Model/Link.php
@@ -0,0 +1,234 @@
+<?php
+
+namespace Model;
+
+use PDO;
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Link model
+ *
+ * @package model
+ * @author Olivier Maridat
+ * @author Frederic Guillot
+ */
+class Link 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)
+ {
+ $link = $this->getById($link_id);
+ return $link['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 booelan $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
+ */
+ public function create($label, $opposite_label = '')
+ {
+ $this->db->startTransaction();
+
+ if (! $this->db->table(self::TABLE)->insert(array('label' => $label))) {
+ $this->db->cancelTransaction();
+ return false;
+ }
+
+ if ($opposite_label !== '') {
+ $this->createOpposite($opposite_label);
+ }
+
+ $this->db->closeTransaction();
+
+ return true;
+ }
+
+ /**
+ * Create the opposite label (executed inside create() method)
+ *
+ * @access private
+ * @param string $label
+ */
+ private function createOpposite($label)
+ {
+ $label_id = $this->db->getConnection()->getLastId();
+
+ $this->db
+ ->table(self::TABLE)
+ ->insert(array(
+ 'label' => $label,
+ 'opposite_id' => $label_id,
+ ));
+
+ $this->db
+ ->table(self::TABLE)
+ ->eq('id', $label_id)
+ ->update(array(
+ 'opposite_id' => $this->db->getConnection()->getLastId()
+ ));
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * Validate creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('label', t('Field required')),
+ new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE),
+ new Validators\NotEquals('label', 'opposite_label', t('The labels must be different')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('id', t('Field required')),
+ new Validators\Required('opposite_id', t('Field required')),
+ new Validators\Required('label', t('Field required')),
+ new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php
index 7762d0fd..652cc842 100644
--- a/app/Model/ProjectActivity.php
+++ b/app/Model/ProjectActivity.php
@@ -62,7 +62,7 @@ class ProjectActivity extends Base
*/
public function getProject($project_id, $limit = 50, $start = null, $end = null)
{
- return $this->getProjects(array($project_id), $limit, $start = null, $end = null);
+ return $this->getProjects(array($project_id), $limit, $start, $end);
}
/**
@@ -91,15 +91,15 @@ class ProjectActivity extends Base
->join(User::TABLE, 'id', 'creator_id')
->desc(self::TABLE.'.id')
->limit($limit);
-
+
if(!is_null($start)){
$query->gte('date_creation', $start);
}
-
+
if(!is_null($end)){
$query->lte('date_creation', $end);
}
-
+
$events = $query->findAll();
foreach ($events as &$event) {
diff --git a/app/Model/ProjectPermission.php b/app/Model/ProjectPermission.php
index fb6316b6..12bd9309 100644
--- a/app/Model/ProjectPermission.php
+++ b/app/Model/ProjectPermission.php
@@ -326,7 +326,7 @@ class ProjectPermission extends Base
*
* @access public
* @param integer $user_id User id
- * @return []integer
+ * @return array
*/
public function getMemberProjectIds($user_id)
{
@@ -338,6 +338,23 @@ class ProjectPermission extends Base
}
/**
+ * Return a list of active project ids where the user is member
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return array
+ */
+ public function getActiveMemberProjectIds($user_id)
+ {
+ return $this->db
+ ->table(Project::TABLE)
+ ->eq('user_id', $user_id)
+ ->eq(Project::TABLE.'.is_active', Project::ACTIVE)
+ ->join(self::TABLE, 'project_id', 'id')
+ ->findAllByColumn('projects.id');
+ }
+
+ /**
* Return a list of active projects where the user is member
*
* @access public
diff --git a/app/Model/SubtaskTimeTracking.php b/app/Model/SubtaskTimeTracking.php
index 8093cf80..1e7e252e 100644
--- a/app/Model/SubtaskTimeTracking.php
+++ b/app/Model/SubtaskTimeTracking.php
@@ -21,7 +21,7 @@ class SubtaskTimeTracking extends Base
* Get query for user timesheet (pagination)
*
* @access public
- * @param integer $user_id User id
+ * @param integer $user_id User id
* @return \PicoDb\Table
*/
public function getUserQuery($user_id)
@@ -36,7 +36,8 @@ class SubtaskTimeTracking extends Base
Subtask::TABLE.'.task_id',
Subtask::TABLE.'.title AS subtask_title',
Task::TABLE.'.title AS task_title',
- Task::TABLE.'.project_id'
+ Task::TABLE.'.project_id',
+ Task::TABLE.'.color_id'
)
->join(Subtask::TABLE, 'id', 'subtask_id')
->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE)
@@ -44,10 +45,10 @@ class SubtaskTimeTracking extends Base
}
/**
- * Get query for task (pagination)
+ * Get query for task timesheet (pagination)
*
* @access public
- * @param integer $task_id Task id
+ * @param integer $task_id Task id
* @return \PicoDb\Table
*/
public function getTaskQuery($task_id)
@@ -73,6 +74,36 @@ class SubtaskTimeTracking extends Base
}
/**
+ * Get query for project timesheet (pagination)
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return \PicoDb\Table
+ */
+ public function getProjectQuery($project_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->columns(
+ self::TABLE.'.id',
+ self::TABLE.'.subtask_id',
+ self::TABLE.'.end',
+ self::TABLE.'.start',
+ self::TABLE.'.user_id',
+ Subtask::TABLE.'.task_id',
+ Subtask::TABLE.'.title AS subtask_title',
+ Task::TABLE.'.project_id',
+ Task::TABLE.'.color_id',
+ User::TABLE.'.username',
+ User::TABLE.'.name AS user_fullname'
+ )
+ ->join(Subtask::TABLE, 'id', 'subtask_id')
+ ->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE)
+ ->join(User::TABLE, 'id', 'user_id', self::TABLE)
+ ->eq(Task::TABLE.'.project_id', $project_id);
+ }
+
+ /**
* Get all recorded time slots for a given user
*
* @access public
@@ -88,6 +119,98 @@ class SubtaskTimeTracking extends Base
}
/**
+ * Get user calendar events
+ *
+ * @access public
+ * @param integer $user_id
+ * @param integer $start
+ * @param integer $end
+ * @return array
+ */
+ public function getUserCalendarEvents($user_id, $start, $end)
+ {
+ $result = $this->getUserQuery($user_id)
+ ->addCondition($this->getCalendarCondition($start, $end))
+ ->findAll();
+
+ return $this->toCalendarEvents($result);
+ }
+
+ /**
+ * Get project calendar events
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $start
+ * @param integer $end
+ * @return array
+ */
+ public function getProjectCalendarEvents($project_id, $start, $end)
+ {
+ $result = $this->getProjectQuery($project_id)
+ ->addCondition($this->getCalendarCondition($start, $end))
+ ->findAll();
+
+ return $this->toCalendarEvents($result);
+ }
+
+ /**
+ * Get time slots that should be displayed in the calendar time range
+ *
+ * @access private
+ * @param string $start ISO8601 start date
+ * @param string $end ISO8601 end date
+ * @return string
+ */
+ private function getCalendarCondition($start, $end)
+ {
+ $start_time = $this->dateParser->getTimestampFromIsoFormat($start);
+ $end_time = $this->dateParser->getTimestampFromIsoFormat($end);
+ $start_column = $this->db->escapeIdentifier('start');
+ $end_column = $this->db->escapeIdentifier('end');
+
+ $conditions = array(
+ "($start_column >= '$start_time' AND $start_column <= '$end_time')",
+ "($start_column <= '$start_time' AND $end_column >= '$start_time')",
+ "($start_column <= '$start_time' AND $end_column = '0')",
+ );
+
+ return '('.implode(' OR ', $conditions).')';
+ }
+
+ /**
+ * Convert a record set to calendar events
+ *
+ * @access private
+ * @param array $rows
+ * @return array
+ */
+ private function toCalendarEvents(array $rows)
+ {
+ $events = array();
+
+ foreach ($rows as $row) {
+
+ $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : '';
+
+ $events[] = array(
+ 'id' => $row['id'],
+ 'subtask_id' => $row['subtask_id'],
+ 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user,
+ 'start' => date('Y-m-d\TH:i:s', $row['start']),
+ 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()),
+ 'backgroundColor' => $this->color->getBackgroundColor($row['color_id']),
+ 'borderColor' => $this->color->getBorderColor($row['color_id']),
+ 'textColor' => 'black',
+ 'url' => $this->helper->url('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])),
+ 'editable' => false,
+ );
+ }
+
+ return $events;
+ }
+
+ /**
* Log start time
*
* @access public
@@ -133,7 +256,7 @@ class SubtaskTimeTracking extends Base
*/
public function updateTaskTimeTracking($task_id)
{
- $result = $this->calculateSubtaskTime($task_id);
+ $result = $this->calculateSubtaskTime($task_id);
if (empty($result['total_spent']) && empty($result['total_estimated'])) {
return true;
diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php
index b5a90154..31de2795 100644
--- a/app/Model/TaskFilter.php
+++ b/app/Model/TaskFilter.php
@@ -18,6 +18,24 @@ class TaskFilter extends Base
return $this;
}
+ public function excludeTasks(array $task_ids)
+ {
+ $this->query->notin('id', $task_ids);
+ return $this;
+ }
+
+ public function filterByTitle($title)
+ {
+ $this->query->ilike('title', '%'.$title.'%');
+ return $this;
+ }
+
+ public function filterByProjects(array $project_ids)
+ {
+ $this->query->in('project_id', $project_ids);
+ return $this;
+ }
+
public function filterByProject($project_id)
{
if ($project_id > 0) {
@@ -94,6 +112,20 @@ class TaskFilter extends Base
return $this->query->findAll();
}
+ public function toAutoCompletion()
+ {
+ return $this->query->columns('id', 'title')->filter(function(array $results) {
+
+ foreach ($results as &$result) {
+ $result['value'] = $result['title'];
+ $result['label'] = '#'.$result['id'].' - '.$result['title'];
+ }
+
+ return $results;
+
+ })->findAll();
+ }
+
public function toCalendarEvents()
{
$events = array();
diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php
index 27fa8150..98ece4e1 100644
--- a/app/Model/TaskFinder.php
+++ b/app/Model/TaskFinder.php
@@ -84,6 +84,7 @@ class TaskFinder extends Base
'(SELECT count(*) FROM task_has_files WHERE task_id=tasks.id) AS nb_files',
'(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id) AS nb_subtasks',
'(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id AND status=2) AS nb_completed_subtasks',
+ '(SELECT count(*) FROM ' . TaskLink::TABLE . ' WHERE ' . TaskLink::TABLE . '.task_id = tasks.id) AS nb_links',
'tasks.id',
'tasks.reference',
'tasks.title',
diff --git a/app/Model/TaskLink.php b/app/Model/TaskLink.php
new file mode 100644
index 00000000..c712e5a8
--- /dev/null
+++ b/app/Model/TaskLink.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace Model;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * TaskLink model
+ *
+ * @package model
+ * @author Olivier Maridat
+ * @author Frederic Guillot
+ */
+class TaskLink extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'task_has_links';
+
+ /**
+ * 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)->eq('id', $task_link_id)->findOne();
+ }
+
+ /**
+ * Get all links attached to a task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getLinks($task_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->columns(
+ self::TABLE.'.id',
+ self::TABLE.'.opposite_task_id AS task_id',
+ Link::TABLE.'.label',
+ Task::TABLE.'.title',
+ Task::TABLE.'.is_active',
+ Task::TABLE.'.project_id'
+ )
+ ->eq(self::TABLE.'.task_id', $task_id)
+ ->join(Link::TABLE, 'id', 'link_id')
+ ->join(Task::TABLE, 'id', 'opposite_task_id')
+ ->findAll();
+ }
+
+ /**
+ * 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 boolean
+ */
+ public function create($task_id, $opposite_task_id, $link_id)
+ {
+ $this->db->startTransaction();
+
+ // Create the original link
+ $this->db->table(self::TABLE)->insert(array(
+ 'task_id' => $task_id,
+ 'opposite_task_id' => $opposite_task_id,
+ 'link_id' => $link_id,
+ ));
+
+ $link_id = $this->link->getOppositeLinkId($link_id);
+
+ // Create the opposite link
+ $this->db->table(self::TABLE)->insert(array(
+ 'task_id' => $opposite_task_id,
+ 'opposite_task_id' => $task_id,
+ 'link_id' => $link_id,
+ ));
+
+ $this->db->closeTransaction();
+
+ return true;
+ }
+
+ /**
+ * Remove a link between two tasks
+ *
+ * @access public
+ * @param integer $task_link_id
+ * @return boolean
+ */
+ public function remove($task_link_id)
+ {
+ $this->db->startTransaction();
+
+ $link = $this->getById($task_link_id);
+ $link_id = $this->link->getOppositeLinkId($link['link_id']);
+
+ $this->db->table(self::TABLE)->eq('id', $task_link_id)->remove();
+
+ $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();
+
+ $this->db->closeTransaction();
+
+ return true;
+ }
+
+ /**
+ * Validate creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('task_id', t('Field required')),
+ new Validators\Required('link_id', t('Field required')),
+ new Validators\Required('title', t('Field required')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Model/User.php b/app/Model/User.php
index 01be8597..7586f3c4 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -29,6 +29,18 @@ class User extends Base
const EVERYBODY_ID = -1;
/**
+ * 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)->count() === 1;
+ }
+
+ /**
* Get query to fetch all users
*
* @access public