summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/Core/Lexer.php4
-rw-r--r--app/Model/TaskFilter.php50
-rw-r--r--app/Template/search/index.php17
-rw-r--r--docs/search.markdown17
-rw-r--r--tests/units/LexerTest.php50
-rw-r--r--tests/units/TaskFilterTest.php70
6 files changed, 205 insertions, 3 deletions
diff --git a/app/Core/Lexer.php b/app/Core/Lexer.php
index ad0631d5..d277f998 100644
--- a/app/Core/Lexer.php
+++ b/app/Core/Lexer.php
@@ -31,6 +31,8 @@ class Lexer
"/^(status:)/" => 'T_STATUS',
"/^(description:)/" => 'T_DESCRIPTION',
"/^(category:)/" => 'T_CATEGORY',
+ "/^(column:)/" => 'T_COLUMN',
+ "/^(project:)/" => 'T_PROJECT',
"/^(\s+)/" => 'T_WHITESPACE',
'/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE',
'/^(yesterday|tomorrow|today)/' => 'T_DATE',
@@ -109,6 +111,8 @@ class Lexer
case 'T_ASSIGNEE':
case 'T_COLOR':
case 'T_CATEGORY':
+ case 'T_COLUMN':
+ case 'T_PROJECT':
$next = next($tokens);
if ($next !== false && $next['token'] === 'T_STRING') {
diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php
index 4a086078..31080cb5 100644
--- a/app/Model/TaskFilter.php
+++ b/app/Model/TaskFilter.php
@@ -62,6 +62,12 @@ class TaskFilter extends Base
case 'T_CATEGORY':
$this->filterByCategoryName($value);
break;
+ case 'T_PROJECT':
+ $this->filterByProjectName($value);
+ break;
+ case 'T_COLUMN':
+ $this->filterByColumnName($value);
+ break;
}
}
@@ -190,6 +196,29 @@ class TaskFilter extends Base
}
/**
+ * Filter by project name
+ *
+ * @access public
+ * @param array $values List of project name
+ * @return TaskFilter
+ */
+ public function filterByProjectName(array $values)
+ {
+ $this->query->beginOr();
+
+ foreach ($values as $project) {
+ if (ctype_digit($project)) {
+ $this->query->eq(Task::TABLE.'.project_id', $project);
+ }
+ else {
+ $this->query->ilike(Project::TABLE.'.name', $project);
+ }
+ }
+
+ $this->query->closeOr();
+ }
+
+ /**
* Filter by category id
*
* @access public
@@ -326,6 +355,24 @@ class TaskFilter extends Base
}
/**
+ * Filter by column name
+ *
+ * @access public
+ * @param array $values List of column name
+ * @return TaskFilter
+ */
+ public function filterByColumnName(array $values)
+ {
+ $this->query->beginOr();
+
+ foreach ($values as $project) {
+ $this->query->ilike(Board::TABLE.'.title', $project);
+ }
+
+ $this->query->closeOr();
+ }
+
+ /**
* Filter by swimlane
*
* @access public
@@ -382,7 +429,6 @@ class TaskFilter extends Base
*/
public function filterByDueDate($date)
{
- $this->query->neq(Task::TABLE.'.date_due', '');
$this->query->neq(Task::TABLE.'.date_due', 0);
$this->query->notNull(Task::TABLE.'.date_due');
return $this->filterWithOperator(Task::TABLE.'.date_due', $date, true);
@@ -452,7 +498,7 @@ class TaskFilter extends Base
*/
public function findAll()
{
- return $this->query->findAll();
+ return $this->query->asc(Task::TABLE.'.id')->findAll();
}
/**
diff --git a/app/Template/search/index.php b/app/Template/search/index.php
index 058f428d..47a926f4 100644
--- a/app/Template/search/index.php
+++ b/app/Template/search/index.php
@@ -15,7 +15,22 @@
<input type="submit" value="<?= t('Search') ?>" class="btn btn-blue"/>
</form>
- <?php if (! empty($values['search']) && $paginator->isEmpty()): ?>
+ <?php if (empty($values['search'])): ?>
+ <div class="listing">
+ <h3><?= t('Advanced search') ?></h3>
+ <p><?= t('Example of query: ') ?><strong>project:"My project" assignee:me due:tomorrow</strong></p>
+ <ul>
+ <li><?= t('Search by project: ') ?><strong>project:"My project"</strong></li>
+ <li><?= t('Search by column: ') ?><strong>column:"Work in progress"</strong></li>
+ <li><?= t('Search by assignee: ') ?><strong>assignee:nobody</strong></li>
+ <li><?= t('Search by color: ') ?><strong>color:Blue</strong></li>
+ <li><?= t('Search by category: ') ?><strong>category:"Feature Request"</strong></li>
+ <li><?= t('Search by description: ') ?><strong>description:"Something to find"</strong></li>
+ <li><?= t('Search by due date: ') ?><strong>due:2015-07-01</strong></li>
+ </ul>
+ <p><a href="http://kanboard.net/documentation/search" target="_blank"><?= t('More examples in the documentation') ?></a></p>
+ </div>
+ <?php elseif (! empty($values['search']) && $paginator->isEmpty()): ?>
<p class="alert"><?= t('Nothing found.') ?></p>
<?php elseif (! $paginator->isEmpty()): ?>
<?= $this->render('search/results', array(
diff --git a/docs/search.markdown b/docs/search.markdown
index 6e40874a..1d254b29 100644
--- a/docs/search.markdown
+++ b/docs/search.markdown
@@ -127,3 +127,20 @@ Attribute: **category**
- Find tasks with a specific category: `category:"Feature Request"`
- Find all tasks that have those categories: `category:"Bug" category:"Improvements"`
- Find tasks with no category assigned: `category:none`
+
+Search by project
+-----------------
+
+Attribute: **project**
+
+- Find tasks by project name: `project:"My project name"`
+- Find tasks by project id: `project:23`
+- Find tasks for several projects: `project:"My project A" project:"My project B"`
+
+Search by column
+----------------
+
+Attribute: **column**
+
+- Find tasks by column name: `column:"Work in progress"`
+- Find tasks for several columns: `column:"Backlog" column:ready`
diff --git a/tests/units/LexerTest.php b/tests/units/LexerTest.php
index 4d0d67ad..38974357 100644
--- a/tests/units/LexerTest.php
+++ b/tests/units/LexerTest.php
@@ -91,6 +91,56 @@ class LexerTest extends Base
);
}
+ public function testColumnQuery()
+ {
+ $lexer = new Lexer;
+
+ $this->assertEquals(
+ array(array('match' => 'column:', 'token' => 'T_COLUMN'), array('match' => 'Feature Request', 'token' => 'T_STRING')),
+ $lexer->tokenize('column:"Feature Request"')
+ );
+
+ $this->assertEquals(
+ array('T_COLUMN' => array('Feature Request')),
+ $lexer->map($lexer->tokenize('column:"Feature Request"'))
+ );
+
+ $this->assertEquals(
+ array('T_COLUMN' => array('Feature Request', 'Bug')),
+ $lexer->map($lexer->tokenize('column:"Feature Request" column:Bug'))
+ );
+
+ $this->assertEquals(
+ array(),
+ $lexer->map($lexer->tokenize('column: '))
+ );
+ }
+
+ public function testProjectQuery()
+ {
+ $lexer = new Lexer;
+
+ $this->assertEquals(
+ array(array('match' => 'project:', 'token' => 'T_PROJECT'), array('match' => 'My project', 'token' => 'T_STRING')),
+ $lexer->tokenize('project:"My project"')
+ );
+
+ $this->assertEquals(
+ array('T_PROJECT' => array('My project')),
+ $lexer->map($lexer->tokenize('project:"My project"'))
+ );
+
+ $this->assertEquals(
+ array('T_PROJECT' => array('My project', 'plop')),
+ $lexer->map($lexer->tokenize('project:"My project" project:plop'))
+ );
+
+ $this->assertEquals(
+ array(),
+ $lexer->map($lexer->tokenize('project: '))
+ );
+ }
+
public function testStatusQuery()
{
$lexer = new Lexer;
diff --git a/tests/units/TaskFilterTest.php b/tests/units/TaskFilterTest.php
index 33e1792e..c68e8880 100644
--- a/tests/units/TaskFilterTest.php
+++ b/tests/units/TaskFilterTest.php
@@ -124,6 +124,76 @@ class TaskFilterTest extends Base
$this->assertEmpty($tasks);
}
+ public function testSearchWithProject()
+ {
+ $p = new Project($this->container);
+ $tc = new TaskCreation($this->container);
+ $tf = new TaskFilter($this->container);
+
+ $this->assertEquals(1, $p->create(array('name' => 'My project A')));
+ $this->assertEquals(2, $p->create(array('name' => 'My project B')));
+ $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
+ $this->assertNotFalse($tc->create(array('project_id' => 2, 'title' => 'task2')));
+
+ $tf->search('project:"My project A"');
+ $tasks = $tf->findAll();
+ $this->assertNotEmpty($tasks);
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('task1', $tasks[0]['title']);
+ $this->assertEquals('My project A', $tasks[0]['project_name']);
+
+ $tf->search('project:2');
+ $tasks = $tf->findAll();
+ $this->assertNotEmpty($tasks);
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('task2', $tasks[0]['title']);
+ $this->assertEquals('My project B', $tasks[0]['project_name']);
+
+ $tf->search('project:"My project A" project:"my project b"');
+ $tasks = $tf->findAll();
+ $this->assertNotEmpty($tasks);
+ $this->assertCount(2, $tasks);
+ $this->assertEquals('task1', $tasks[0]['title']);
+ $this->assertEquals('My project A', $tasks[0]['project_name']);
+ $this->assertEquals('task2', $tasks[1]['title']);
+ $this->assertEquals('My project B', $tasks[1]['project_name']);
+
+ $tf->search('project:"not found"');
+ $tasks = $tf->findAll();
+ $this->assertEmpty($tasks);
+ }
+
+ public function testSearchWithColumn()
+ {
+ $p = new Project($this->container);
+ $tc = new TaskCreation($this->container);
+ $tf = new TaskFilter($this->container);
+
+ $this->assertEquals(1, $p->create(array('name' => 'My project A')));
+ $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
+ $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'column_id' => 3)));
+
+ $tf->search('column:Backlog');
+ $tasks = $tf->findAll();
+ $this->assertNotEmpty($tasks);
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('task1', $tasks[0]['title']);
+ $this->assertEquals('Backlog', $tasks[0]['column_name']);
+
+ $tf->search('column:backlog column:"Work in progress"');
+ $tasks = $tf->findAll();
+ $this->assertNotEmpty($tasks);
+ $this->assertCount(2, $tasks);
+ $this->assertEquals('task1', $tasks[0]['title']);
+ $this->assertEquals('Backlog', $tasks[0]['column_name']);
+ $this->assertEquals('task2', $tasks[1]['title']);
+ $this->assertEquals('Work in progress', $tasks[1]['column_name']);
+
+ $tf->search('column:"not found"');
+ $tasks = $tf->findAll();
+ $this->assertEmpty($tasks);
+ }
+
public function testSearchWithDueDate()
{
$dp = new DateParser($this->container);