diff options
Diffstat (limited to 'app/Model')
-rw-r--r-- | app/Model/Acl.php | 1 | ||||
-rw-r--r-- | app/Model/Budget.php | 24 | ||||
-rw-r--r-- | app/Model/Currency.php | 104 | ||||
-rw-r--r-- | app/Model/ProjectActivity.php | 8 | ||||
-rw-r--r-- | app/Model/Subtask.php | 1 | ||||
-rw-r--r-- | app/Model/SubtaskForecast.php | 118 | ||||
-rw-r--r-- | app/Model/TaskPosition.php | 3 | ||||
-rw-r--r-- | app/Model/Transition.php | 170 | ||||
-rw-r--r-- | app/Model/Webhook.php | 39 |
9 files changed, 419 insertions, 49 deletions
diff --git a/app/Model/Acl.php b/app/Model/Acl.php index b52a7864..403c45d0 100644 --- a/app/Model/Acl.php +++ b/app/Model/Acl.php @@ -72,6 +72,7 @@ class Acl extends Base 'link' => '*', 'project' => array('remove'), 'hourlyrate' => '*', + 'currency' => '*', ); /** diff --git a/app/Model/Budget.php b/app/Model/Budget.php index 84cadf6e..d74dd870 100644 --- a/app/Model/Budget.php +++ b/app/Model/Budget.php @@ -111,15 +111,19 @@ class Budget extends Base $date = $today->format('Y-m-d'); $today_in = isset($in[$date]) ? (int) $in[$date] : 0; $today_out = isset($out[$date]) ? (int) $out[$date] : 0; - $left += $today_in; - $left -= $today_out; - - $serie[] = array( - 'date' => $date, - 'in' => $today_in, - 'out' => -$today_out, - 'left' => $left, - ); + + if ($today_in > 0 || $today_out > 0) { + + $left += $today_in; + $left -= $today_out; + + $serie[] = array( + 'date' => $date, + 'in' => $today_in, + 'out' => -$today_out, + 'left' => $left, + ); + } } return $serie; @@ -143,7 +147,7 @@ class Budget extends Base foreach ($rates as $rate) { if ($rate['user_id'] == $record['user_id'] && date('Y-m-d', $rate['date_effective']) <= date('Y-m-d', $record['start'])) { - $hourly_price = $rate['rate']; + $hourly_price = $this->currency->getPrice($rate['currency'], $rate['rate']); break; } } diff --git a/app/Model/Currency.php b/app/Model/Currency.php new file mode 100644 index 00000000..bc423337 --- /dev/null +++ b/app/Model/Currency.php @@ -0,0 +1,104 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Currency + * + * @package model + * @author Frederic Guillot + */ +class Currency extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'currencies'; + + /** + * 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 + * @return array + */ + public function getPrice($currency, $price) + { + static $rates = null; + $reference = $this->config->get('application_currency', 'USD'); + + if ($reference !== $currency) { + $rates = $rates === null ? $this->db->hashtable(self::TABLE)->getAll('currency', 'rate') : array(); + $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)->count() === 1) { + return $this->update($currency, $rate); + } + + return $this->persist(self::TABLE, compact('currency', '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)); + } + + /** + * Validate + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validate(array $values) + { + $v = new Validator($values, array( + new Validators\Required('currency', t('Field required')), + new Validators\Required('rate', t('Field required')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php index 652cc842..c5fbbd38 100644 --- a/app/Model/ProjectActivity.php +++ b/app/Model/ProjectActivity.php @@ -162,7 +162,13 @@ class ProjectActivity extends Base { switch ($event['event_name']) { case Task::EVENT_ASSIGNEE_CHANGE: - return t('%s change the assignee of the task #%d to %s', $event['author'], $event['task']['id'], $event['task']['assignee_name'] ?: $event['task']['assignee_username']); + $assignee = $event['task']['assignee_name'] ?: $event['task']['assignee_username']; + + if (! empty($assignee)) { + return t('%s change the assignee of the task #%d to %s', $event['author'], $event['task']['id'], $assignee); + } + + return t('%s remove the assignee of the task %s', $event['author'], e('#%d', $event['task']['id'])); case Task::EVENT_UPDATE: return t('%s updated the task #%d', $event['author'], $event['task']['id']); case Task::EVENT_CREATE: diff --git a/app/Model/Subtask.php b/app/Model/Subtask.php index e33373e0..492f3a77 100644 --- a/app/Model/Subtask.php +++ b/app/Model/Subtask.php @@ -98,6 +98,7 @@ class Subtask extends Base Subtask::TABLE.'.*', Task::TABLE.'.project_id', Task::TABLE.'.color_id', + Task::TABLE.'.title AS task_name', Project::TABLE.'.name AS project_name' ) ->eq('user_id', $user_id) diff --git a/app/Model/SubtaskForecast.php b/app/Model/SubtaskForecast.php new file mode 100644 index 00000000..cb86f6d7 --- /dev/null +++ b/app/Model/SubtaskForecast.php @@ -0,0 +1,118 @@ +<?php + +namespace Model; + +use DateTime; +use DateInterval; + +/** + * Subtask Forecast + * + * @package model + * @author Frederic Guillot + */ +class SubtaskForecast extends Base +{ + /** + * Get not completed subtasks with an estimate sorted by postition + * + * @access public + * @param integer $user_id + * @return array + */ + public function getSubtasks($user_id) + { + return $this->db + ->table(Subtask::TABLE) + ->columns(Subtask::TABLE.'.id', Task::TABLE.'.project_id', Subtask::TABLE.'.task_id', Subtask::TABLE.'.title', Subtask::TABLE.'.time_estimated') + ->join(Task::TABLE, 'id', 'task_id') + ->asc(Task::TABLE.'.position') + ->asc(Subtask::TABLE.'.position') + ->gt(Subtask::TABLE.'.time_estimated', 0) + ->eq(Subtask::TABLE.'.status', Subtask::STATUS_TODO) + ->eq(Subtask::TABLE.'.user_id', $user_id) + ->findAll(); + } + + /** + * Get the start date for the forecast + * + * @access public + * @param integer $user_id + * @return array + */ + public function getStartDate($user_id) + { + $subtask = $this->db->table(Subtask::TABLE) + ->columns(Subtask::TABLE.'.time_estimated', SubtaskTimeTracking::TABLE.'.start') + ->eq(SubtaskTimeTracking::TABLE.'.user_id', $user_id) + ->eq(SubtaskTimeTracking::TABLE.'.end', 0) + ->status('status', Subtask::STATUS_INPROGRESS) + ->join(SubtaskTimeTracking::TABLE, 'subtask_id', 'id') + ->findOne(); + + if ($subtask && $subtask['time_estimated'] && $subtask['start']) { + return date('Y-m-d H:i', $subtask['start'] + $subtask['time_estimated'] * 3600); + } + + return date('Y-m-d H:i'); + } + + /** + * Get all calendar events according to the user timetable and the subtasks estimates + * + * @access public + * @param integer $user_id + * @param string $end End date of the calendar + * @return array + */ + public function getCalendarEvents($user_id, $end) + { + $events = array(); + $start_date = new DateTime($this->getStartDate($user_id)); + $timetable = $this->timetable->calculate($user_id, $start_date, new DateTime($end)); + $subtasks = $this->getSubtasks($user_id); + $total = count($subtasks); + $offset = 0; + + foreach ($timetable as $slot) { + + $interval = $this->dateParser->getHours($slot[0], $slot[1]); + $start = $slot[0]->getTimestamp(); + + if ($slot[0] < $start_date) { + continue; + } + + while ($offset < $total) { + + $event = array( + 'id' => $subtasks[$offset]['id'].'-'.$subtasks[$offset]['task_id'].'-'.$offset, + 'subtask_id' => $subtasks[$offset]['id'], + 'title' => t('#%d', $subtasks[$offset]['task_id']).' '.$subtasks[$offset]['title'], + 'url' => $this->helper->url('task', 'show', array('task_id' => $subtasks[$offset]['task_id'], 'project_id' => $subtasks[$offset]['project_id'])), + 'editable' => false, + 'start' => date('Y-m-d\TH:i:s', $start), + ); + + if ($subtasks[$offset]['time_estimated'] <= $interval) { + + $start += $subtasks[$offset]['time_estimated'] * 3600; + $interval -= $subtasks[$offset]['time_estimated']; + $offset++; + + $event['end'] = date('Y-m-d\TH:i:s', $start); + $events[] = $event; + } + else { + $subtasks[$offset]['time_estimated'] -= $interval; + $event['end'] = $slot[1]->format('Y-m-d\TH:i:s'); + $events[] = $event; + break; + } + } + } + + return $events; + } +} diff --git a/app/Model/TaskPosition.php b/app/Model/TaskPosition.php index 6dd10b02..ab5fe43b 100644 --- a/app/Model/TaskPosition.php +++ b/app/Model/TaskPosition.php @@ -143,6 +143,9 @@ class TaskPosition extends Base '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'], ); if ($task['swimlane_id'] != $new_swimlane_id) { diff --git a/app/Model/Transition.php b/app/Model/Transition.php new file mode 100644 index 00000000..cb759e4a --- /dev/null +++ b/app/Model/Transition.php @@ -0,0 +1,170 @@ +<?php + +namespace Model; + +/** + * Transition model + * + * @package model + * @author Frederic Guillot + */ +class Transition extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'transitions'; + + /** + * Save transition event + * + * @access public + * @param integer $user_id + * @param array $task + * @return boolean + */ + public function save($user_id, array $task) + { + return $this->db->table(self::TABLE)->insert(array( + 'user_id' => $user_id, + 'project_id' => $task['project_id'], + 'task_id' => $task['task_id'], + 'src_column_id' => $task['src_column_id'], + 'dst_column_id' => $task['dst_column_id'], + 'date' => time(), + 'time_spent' => time() - $task['date_moved'] + )); + } + + /** + * 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', + User::TABLE.'.name', + User::TABLE.'.username', + self::TABLE.'.user_id', + self::TABLE.'.date', + self::TABLE.'.time_spent' + ) + ->eq('task_id', $task_id) + ->desc('date') + ->join(User::TABLE, 'id', 'user_id') + ->join(Board::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') + ->join(Board::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') + ->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( + Task::TABLE.'.id', + Task::TABLE.'.title', + 'src.title as src_column', + 'dst.title as dst_column', + User::TABLE.'.name', + User::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') + ->join(Task::TABLE, 'id', 'task_id') + ->join(User::TABLE, 'id', 'user_id') + ->join(Board::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') + ->join(Board::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') + ->findAll(); + } + + /** + * Get project export + * + * @access public + * @param integer $project_id Project id + * @param mixed $from Start date (timestamp or user formatted date) + * @param mixed $to End date (timestamp or user formatted date) + * @return array + */ + public function export($project_id, $from, $to) + { + $results = array($this->getColumns()); + $transitions = $this->getAllByProjectAndDate($project_id, $from, $to); + + foreach ($transitions as $transition) { + $results[] = $this->format($transition); + } + + return $results; + } + + /** + * Get column titles + * + * @access public + * @return string[] + */ + public function getColumns() + { + return array( + e('Id'), + e('Task Title'), + e('Source column'), + e('Destination column'), + e('Executer'), + e('Date'), + e('Time spent'), + ); + } + + /** + * Format the output of a transition array + * + * @access public + * @param array $transition + * @return array + */ + public function format(array $transition) + { + $values = array(); + $values[] = $transition['id']; + $values[] = $transition['title']; + $values[] = $transition['src_column']; + $values[] = $transition['dst_column']; + $values[] = $transition['name'] ?: $transition['username']; + $values[] = date('Y-m-d H:i', $transition['date']); + $values[] = round($transition['time_spent'] / 3600, 2); + + return $values; + } +} diff --git a/app/Model/Webhook.php b/app/Model/Webhook.php index 7edffa6e..b3603818 100644 --- a/app/Model/Webhook.php +++ b/app/Model/Webhook.php @@ -11,27 +11,6 @@ namespace Model; class Webhook extends Base { /** - * HTTP connection timeout in seconds - * - * @var integer - */ - const HTTP_TIMEOUT = 1; - - /** - * Number of maximum redirections for the HTTP client - * - * @var integer - */ - const HTTP_MAX_REDIRECTS = 3; - - /** - * HTTP client user agent - * - * @var string - */ - const HTTP_USER_AGENT = 'Kanboard Webhook'; - - /** * Call the external URL * * @access public @@ -42,22 +21,6 @@ class Webhook extends Base { $token = $this->config->get('webhook_token'); - $headers = array( - 'Connection: close', - 'User-Agent: '.self::HTTP_USER_AGENT, - ); - - $context = stream_context_create(array( - 'http' => array( - 'method' => 'POST', - 'protocol_version' => 1.1, - 'timeout' => self::HTTP_TIMEOUT, - 'max_redirects' => self::HTTP_MAX_REDIRECTS, - 'header' => implode("\r\n", $headers), - 'content' => json_encode($task) - ) - )); - if (strpos($url, '?') !== false) { $url .= '&token='.$token; } @@ -65,6 +28,6 @@ class Webhook extends Base $url .= '?token='.$token; } - @file_get_contents($url, false, $context); + return $this->httpClient->post($url, $task); } } |