summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorFrederic Guillot <fred@kanboard.net>2015-06-28 18:52:01 -0400
committerFrederic Guillot <fred@kanboard.net>2015-06-28 18:52:01 -0400
commite22985df50d3a0a2ac883c43749542e41e425927 (patch)
tree784830150173ad36ad40037b9c938f09f6c77025 /app
parent0fa64fc9bd947e2f82f60d63d57479fa4189ef68 (diff)
Start to implement advanced search query language
Diffstat (limited to 'app')
-rw-r--r--app/Controller/Projectinfo.php8
-rw-r--r--app/Core/Lexer.php142
-rw-r--r--app/Model/Color.php23
-rw-r--r--app/Model/TaskFilter.php140
-rw-r--r--app/ServiceProvider/ClassProvider.php1
5 files changed, 311 insertions, 3 deletions
diff --git a/app/Controller/Projectinfo.php b/app/Controller/Projectinfo.php
index a9498f43..c30c1652 100644
--- a/app/Controller/Projectinfo.php
+++ b/app/Controller/Projectinfo.php
@@ -46,9 +46,11 @@ class Projectinfo extends Base
if ($search !== '') {
- $paginator
- ->setQuery($this->taskFinder->getSearchQuery($project['id'], $search))
- ->calculate();
+ // $paginator
+ // ->setQuery($this->taskFinder->getSearchQuery($project['id'], $search))
+ // ->calculate();
+
+ $paginator->setQuery($this->taskFilter->search($search)->filterByProject($project['id'])->getQuery())->calculate();
$nb_tasks = $paginator->getTotal();
}
diff --git a/app/Core/Lexer.php b/app/Core/Lexer.php
new file mode 100644
index 00000000..a81965c5
--- /dev/null
+++ b/app/Core/Lexer.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace Core;
+
+/**
+ * Lexer
+ *
+ * @package core
+ * @author Frederic Guillot
+ */
+class Lexer
+{
+ /**
+ * Current position
+ *
+ * @access private
+ * @var integer
+ */
+ private $offset = 0;
+
+ /**
+ * Token map
+ *
+ * @access private
+ * @var array
+ */
+ private $tokenMap = array(
+ "/^(assignee:)/" => 'T_ASSIGNEE',
+ "/^(color:)/" => 'T_COLOR',
+ "/^(due:)/" => 'T_DUE',
+ "/^(title:)/" => 'T_TITLE',
+ "/^(\s+)/" => 'T_WHITESPACE',
+ '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE',
+ '/^(yesterday|tomorrow|today)/' => 'T_DATE',
+ '/^("(.*?)")/' => 'T_STRING',
+ "/^(\w+)/" => 'T_STRING',
+ );
+
+ /**
+ * Tokenize input string
+ *
+ * @access public
+ * @param string $input
+ * @return array
+ */
+ public function tokenize($input)
+ {
+ $tokens = array();
+ $this->offset = 0;
+
+ while (isset($input[$this->offset])) {
+
+ $result = $this->match(substr($input, $this->offset));
+
+ if ($result === false) {
+ return array();
+ }
+
+ $tokens[] = $result;
+ }
+
+ return $tokens;
+ }
+
+ /**
+ * Find a token that match and move the offset
+ *
+ * @access public
+ * @param string $string
+ * @return array|boolean
+ */
+ public function match($string)
+ {
+ foreach ($this->tokenMap as $pattern => $name) {
+ if (preg_match($pattern, $string, $matches)) {
+
+ $this->offset += strlen($matches[1]);
+
+ return array(
+ 'match' => trim($matches[1], '"'),
+ 'token' => $name,
+ );
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Change the output of tokenizer to be easily parsed by the database filter
+ *
+ * Example: ['T_ASSIGNEE' => ['user1', 'user2'], 'T_TITLE' => 'task title']
+ *
+ * @access public
+ * @param array $tokens
+ * @return array
+ */
+ public function map(array $tokens)
+ {
+ $map = array(
+ 'T_TITLE' => '',
+ );
+
+ while (false !== ($token = current($tokens))) {
+
+ switch ($token['token']) {
+ case 'T_ASSIGNEE':
+ case 'T_COLOR':
+ $next = next($tokens);
+
+ if ($next !== false && $next['token'] === 'T_STRING') {
+ $map[$token['token']][] = $next['match'];
+ }
+
+ break;
+
+ case 'T_DUE':
+ $next = next($tokens);
+
+ if ($next !== false && $next['token'] === 'T_DATE') {
+ $map[$token['token']] = $next['match'];
+ }
+
+ break;
+
+ default:
+ $map['T_TITLE'] .= $token['match'];
+ break;
+ }
+
+ next($tokens);
+ }
+
+ $map['T_TITLE'] = trim($map['T_TITLE']);
+
+ if (empty($map['T_TITLE'])) {
+ unset($map['T_TITLE']);
+ }
+
+ return $map;
+ }
+}
diff --git a/app/Model/Color.php b/app/Model/Color.php
index 9a21f691..a35aff8f 100644
--- a/app/Model/Color.php
+++ b/app/Model/Color.php
@@ -100,6 +100,29 @@ class Color extends Base
);
/**
+ * 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;
+ }
+ else if ($color === strtolower($params['name'])) {
+ return $color_id;
+ }
+ }
+
+ return '';
+ }
+
+ /**
* Get available colors
*
* @access public
diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php
index bd03a8bc..4f306b14 100644
--- a/app/Model/TaskFilter.php
+++ b/app/Model/TaskFilter.php
@@ -24,6 +24,42 @@ class TaskFilter extends Base
public $query;
/**
+ * Apply filters according to the search input
+ *
+ * @access public
+ * @param string $input
+ * @return TaskFilter
+ */
+ public function search($input)
+ {
+ $tree = $this->lexer->map($this->lexer->tokenize($input));
+ $this->query = $this->taskFinder->getExtendedQuery();
+
+ if (empty($tree)) {
+ $this->query->addCondition('1 = 0');
+ }
+
+ foreach ($tree as $filter => $value) {
+ switch ($filter) {
+ case 'T_ASSIGNEE':
+ $this->filterByAssignee($value);
+ break;
+ case 'T_COLOR':
+ $this->filterByColors($value);
+ break;
+ case 'T_DUE':
+ $this->filterByDueDate($value);
+ break;
+ case 'T_TITLE':
+ $this->filterByTitle($value);
+ break;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
* Create a new query
*
* @access public
@@ -164,6 +200,35 @@ class TaskFilter extends Base
}
/**
+ * Filter by assignee names
+ *
+ * @access public
+ * @param array $values List of assignees
+ * @return TaskFilter
+ */
+ public function filterByAssignee(array $values)
+ {
+ $this->query->beginOr();
+
+ foreach ($values as $assignee) {
+
+ switch ($assignee) {
+ case 'me':
+ $this->query->eq('owner_id', $this->userSession->getId());
+ break;
+ case 'nobody':
+ $this->query->eq('owner_id', 0);
+ break;
+ default:
+ $this->query->ilike(User::TABLE.'.username', '%'.$assignee.'%');
+ $this->query->ilike(User::TABLE.'.name', '%'.$assignee.'%');
+ }
+ }
+
+ $this->query->closeOr();
+ }
+
+ /**
* Filter by color
*
* @access public
@@ -180,6 +245,26 @@ class TaskFilter extends Base
}
/**
+ * Filter by colors
+ *
+ * @access public
+ * @param array $colors
+ * @return TaskFilter
+ */
+ public function filterByColors(array $colors)
+ {
+ $this->query->beginOr();
+
+ foreach ($colors as $color) {
+ $this->filterByColor($this->color->find($color));
+ }
+
+ $this->query->closeOr();
+
+ return $this;
+ }
+
+ /**
* Filter by column
*
* @access public
@@ -228,6 +313,18 @@ class TaskFilter extends Base
}
/**
+ * Filter by due date
+ *
+ * @access public
+ * @param string $date ISO8601 date format
+ * @return TaskFilter
+ */
+ public function filterByDueDate($date)
+ {
+ return $this->filterWithOperator('date_due', $date, true);
+ }
+
+ /**
* Filter by due date (range)
*
* @access public
@@ -295,6 +392,17 @@ class TaskFilter extends Base
}
/**
+ * Get the PicoDb query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
* Format the results to the ajax autocompletion
*
* @access public
@@ -465,4 +573,36 @@ class TaskFilter extends Base
return $vEvent;
}
+
+ /**
+ * Filter with an operator
+ *
+ * @access public
+ * @param string $field
+ * @param string $value
+ * @param boolean $is_date
+ * @return TaskFilter
+ */
+ private function filterWithOperator($field, $value, $is_date)
+ {
+ $operators = array(
+ '<=' => 'lte',
+ '>=' => 'gte',
+ '<' => 'lt',
+ '>' => 'gt',
+ );
+
+ foreach ($operators as $operator => $method) {
+
+ if (strpos($value, $operator) === 0) {
+ $value = substr($value, strlen($operator));
+ $this->query->$method($field, $is_date ? $this->dateParser->getTimestampFromIsoFormat($value) : $value);
+ return $this;
+ }
+ }
+
+ $this->query->eq($field, $is_date ? $this->dateParser->getTimestampFromIsoFormat($value) : $value);
+
+ return $this;
+ }
}
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index 4ecd357b..1fa0d0ef 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -67,6 +67,7 @@ class ClassProvider implements ServiceProviderInterface
'EmailClient',
'Helper',
'HttpClient',
+ 'Lexer',
'MemoryCache',
'Request',
'Session',