summaryrefslogtreecommitdiff
path: root/app/Model
diff options
context:
space:
mode:
Diffstat (limited to 'app/Model')
-rw-r--r--app/Model/Acl.php1
-rw-r--r--app/Model/Budget.php24
-rw-r--r--app/Model/Currency.php104
-rw-r--r--app/Model/ProjectActivity.php8
-rw-r--r--app/Model/Subtask.php1
-rw-r--r--app/Model/SubtaskForecast.php118
-rw-r--r--app/Model/TaskPosition.php3
-rw-r--r--app/Model/Transition.php170
-rw-r--r--app/Model/Webhook.php39
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);
}
}