summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog1
-rw-r--r--app/Controller/Search.php18
-rw-r--r--app/Filter/BaseDateFilter.php103
-rw-r--r--app/Filter/BaseFilter.php44
-rw-r--r--app/Filter/ProjectActivityCreationDateFilter.php38
-rw-r--r--app/Filter/ProjectActivityCreatorFilter.php65
-rw-r--r--app/Filter/ProjectActivityProjectIdsFilter.php2
-rw-r--r--app/Filter/ProjectActivityProjectNameFilter.php38
-rw-r--r--app/Filter/ProjectActivityTaskStatusFilter.php43
-rw-r--r--app/Filter/ProjectActivityTaskTitleFilter.php15
-rw-r--r--app/Filter/TaskCompletionDateFilter.php2
-rw-r--r--app/Filter/TaskCreationDateFilter.php2
-rw-r--r--app/Filter/TaskDueDateFilter.php2
-rw-r--r--app/Filter/TaskModificationDateFilter.php2
-rw-r--r--app/Filter/TaskProjectsFilter.php7
-rw-r--r--app/Filter/TaskStartDateFilter.php2
-rw-r--r--app/Helper/ProjectActivityHelper.php27
-rw-r--r--app/Locale/bs_BA/translations.php10
-rw-r--r--app/Locale/cs_CZ/translations.php10
-rw-r--r--app/Locale/da_DK/translations.php10
-rw-r--r--app/Locale/de_DE/translations.php10
-rw-r--r--app/Locale/el_GR/translations.php10
-rw-r--r--app/Locale/es_ES/translations.php10
-rw-r--r--app/Locale/fi_FI/translations.php10
-rw-r--r--app/Locale/fr_FR/translations.php10
-rw-r--r--app/Locale/hu_HU/translations.php10
-rw-r--r--app/Locale/id_ID/translations.php10
-rw-r--r--app/Locale/it_IT/translations.php10
-rw-r--r--app/Locale/ja_JP/translations.php10
-rw-r--r--app/Locale/ko_KR/translations.php10
-rw-r--r--app/Locale/my_MY/translations.php10
-rw-r--r--app/Locale/nb_NO/translations.php10
-rw-r--r--app/Locale/nl_NL/translations.php10
-rw-r--r--app/Locale/pl_PL/translations.php10
-rw-r--r--app/Locale/pt_BR/translations.php10
-rw-r--r--app/Locale/pt_PT/translations.php10
-rw-r--r--app/Locale/ru_RU/translations.php12
-rw-r--r--app/Locale/sr_Latn_RS/translations.php10
-rw-r--r--app/Locale/sv_SE/translations.php10
-rw-r--r--app/Locale/th_TH/translations.php10
-rw-r--r--app/Locale/tr_TR/translations.php10
-rw-r--r--app/Locale/zh_CN/translations.php10
-rw-r--r--app/Model/ProjectActivity.php1
-rw-r--r--app/ServiceProvider/FilterProvider.php30
-rw-r--r--app/ServiceProvider/RouteProvider.php3
-rw-r--r--app/Template/activity/filter_dropdown.php14
-rw-r--r--app/Template/search/activity.php39
-rw-r--r--app/Template/search/index.php4
-rw-r--r--doc/search.markdown83
-rw-r--r--tests/units/Filter/ProjectActivityCreationDateFilterTest.php117
-rw-r--r--tests/units/Filter/ProjectActivityCreatorFilterTest.php91
-rw-r--r--tests/units/Filter/ProjectActivityProjectNameFilterTest.php35
-rw-r--r--tests/units/Filter/ProjectActivityTaskStatusFilterTest.php49
-rw-r--r--tests/units/Filter/ProjectActivityTaskTitleFilterTest.php47
54 files changed, 1066 insertions, 110 deletions
diff --git a/ChangeLog b/ChangeLog
index 1bbe8062..f4952b53 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -3,6 +3,7 @@ Version 1.0.28 (unreleased)
New features:
+* Search in activity stream
* Search in comments
* Search by task creator
diff --git a/app/Controller/Search.php b/app/Controller/Search.php
index 840a90c8..a42e9d3d 100644
--- a/app/Controller/Search.php
+++ b/app/Controller/Search.php
@@ -46,4 +46,22 @@ class Search extends Base
'title' => t('Search tasks').($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '')
)));
}
+
+ public function activity()
+ {
+ $search = urldecode($this->request->getStringParam('search'));
+ $events = $this->helper->projectActivity->searchEvents($search);
+ $nb_events = count($events);
+
+ $this->response->html($this->helper->layout->app('search/activity', array(
+ 'values' => array(
+ 'search' => $search,
+ 'controller' => 'search',
+ 'action' => 'activity',
+ ),
+ 'title' => t('Search in activity stream').($nb_events > 0 ? ' ('.$nb_events.')' : ''),
+ 'nb_events' => $nb_events,
+ 'events' => $events,
+ )));
+ }
}
diff --git a/app/Filter/BaseDateFilter.php b/app/Filter/BaseDateFilter.php
new file mode 100644
index 00000000..56fb2d78
--- /dev/null
+++ b/app/Filter/BaseDateFilter.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\DateParser;
+
+/**
+ * Base date filter class
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+abstract class BaseDateFilter extends BaseFilter
+{
+ /**
+ * DateParser object
+ *
+ * @access protected
+ * @var DateParser
+ */
+ protected $dateParser;
+
+ /**
+ * Set DateParser object
+ *
+ * @access public
+ * @param DateParser $dateParser
+ * @return $this
+ */
+ public function setDateParser(DateParser $dateParser)
+ {
+ $this->dateParser = $dateParser;
+ return $this;
+ }
+
+ /**
+ * Parse operator in the input string
+ *
+ * @access protected
+ * @return string
+ */
+ protected function parseOperator()
+ {
+ $operators = array(
+ '<=' => 'lte',
+ '>=' => 'gte',
+ '<' => 'lt',
+ '>' => 'gt',
+ );
+
+ foreach ($operators as $operator => $method) {
+ if (strpos($this->value, $operator) === 0) {
+ $this->value = substr($this->value, strlen($operator));
+ return $method;
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * Apply a date filter
+ *
+ * @access protected
+ * @param string $field
+ */
+ protected function applyDateFilter($field)
+ {
+ $method = $this->parseOperator();
+ $timestamp = $this->dateParser->getTimestampFromIsoFormat($this->value);
+
+ if ($method !== '') {
+ $this->query->$method($field, $this->getTimestampFromOperator($method, $timestamp));
+ } else {
+ $this->query->gte($field, $timestamp);
+ $this->query->lte($field, $timestamp + 86399);
+ }
+ }
+
+ /**
+ * Get timestamp from the operator
+ *
+ * @access public
+ * @param string $method
+ * @param integer $timestamp
+ * @return integer
+ */
+ protected function getTimestampFromOperator($method, $timestamp)
+ {
+ switch ($method) {
+ case 'lte':
+ return $timestamp + 86399;
+ case 'lt':
+ return $timestamp;
+ case 'gte':
+ return $timestamp;
+ case 'gt':
+ return $timestamp + 86400;
+ }
+
+ return $timestamp;
+ }
+}
diff --git a/app/Filter/BaseFilter.php b/app/Filter/BaseFilter.php
index a7e6a61a..79a664be 100644
--- a/app/Filter/BaseFilter.php
+++ b/app/Filter/BaseFilter.php
@@ -72,48 +72,4 @@ abstract class BaseFilter
$this->value = $value;
return $this;
}
-
- /**
- * Parse operator in the input string
- *
- * @access protected
- * @return string
- */
- protected function parseOperator()
- {
- $operators = array(
- '<=' => 'lte',
- '>=' => 'gte',
- '<' => 'lt',
- '>' => 'gt',
- );
-
- foreach ($operators as $operator => $method) {
- if (strpos($this->value, $operator) === 0) {
- $this->value = substr($this->value, strlen($operator));
- return $method;
- }
- }
-
- return '';
- }
-
- /**
- * Apply a date filter
- *
- * @access protected
- * @param string $field
- */
- protected function applyDateFilter($field)
- {
- $timestamp = strtotime($this->value);
- $method = $this->parseOperator();
-
- if ($method !== '') {
- $this->query->$method($field, $timestamp);
- } else {
- $this->query->gte($field, $timestamp);
- $this->query->lte($field, $timestamp + 86399);
- }
- }
}
diff --git a/app/Filter/ProjectActivityCreationDateFilter.php b/app/Filter/ProjectActivityCreationDateFilter.php
new file mode 100644
index 00000000..d0b7f754
--- /dev/null
+++ b/app/Filter/ProjectActivityCreationDateFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectActivity;
+
+/**
+ * Filter activity events by creation date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityCreationDateFilter extends BaseDateFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('created');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->applyDateFilter(ProjectActivity::TABLE.'.date_creation');
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityCreatorFilter.php b/app/Filter/ProjectActivityCreatorFilter.php
new file mode 100644
index 00000000..c95569d6
--- /dev/null
+++ b/app/Filter/ProjectActivityCreatorFilter.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectActivity;
+
+/**
+ * Filter activity events by creator
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityCreatorFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Current user id
+ *
+ * @access private
+ * @var int
+ */
+ private $currentUserId = 0;
+
+ /**
+ * Set current user id
+ *
+ * @access public
+ * @param integer $userId
+ * @return TaskAssigneeFilter
+ */
+ public function setCurrentUserId($userId)
+ {
+ $this->currentUserId = $userId;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('creator');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ if ($this->value === 'me') {
+ $this->query->eq(ProjectActivity::TABLE . '.creator_id', $this->currentUserId);
+ } else {
+ $this->query->beginOr();
+ $this->query->ilike('uc.username', '%'.$this->value.'%');
+ $this->query->ilike('uc.name', '%'.$this->value.'%');
+ $this->query->closeOr();
+ }
+ }
+}
diff --git a/app/Filter/ProjectActivityProjectIdsFilter.php b/app/Filter/ProjectActivityProjectIdsFilter.php
index 4d7c9028..47cf0c25 100644
--- a/app/Filter/ProjectActivityProjectIdsFilter.php
+++ b/app/Filter/ProjectActivityProjectIdsFilter.php
@@ -21,7 +21,7 @@ class ProjectActivityProjectIdsFilter extends BaseFilter implements FilterInterf
*/
public function getAttributes()
{
- return array('project_ids');
+ return array('projects');
}
/**
diff --git a/app/Filter/ProjectActivityProjectNameFilter.php b/app/Filter/ProjectActivityProjectNameFilter.php
new file mode 100644
index 00000000..0cf73657
--- /dev/null
+++ b/app/Filter/ProjectActivityProjectNameFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+
+/**
+ * Filter activity events by project name
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityProjectNameFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('project');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->ilike(Project::TABLE.'.name', '%'.$this->value.'%');
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityTaskStatusFilter.php b/app/Filter/ProjectActivityTaskStatusFilter.php
new file mode 100644
index 00000000..69e2c52d
--- /dev/null
+++ b/app/Filter/ProjectActivityTaskStatusFilter.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter activity events by task status
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityTaskStatusFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('status');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if ($this->value === 'open') {
+ $this->query->eq(Task::TABLE.'.is_active', Task::STATUS_OPEN);
+ } elseif ($this->value === 'closed') {
+ $this->query->eq(Task::TABLE.'.is_active', Task::STATUS_CLOSED);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityTaskTitleFilter.php b/app/Filter/ProjectActivityTaskTitleFilter.php
index ed3f36d6..bf2afa30 100644
--- a/app/Filter/ProjectActivityTaskTitleFilter.php
+++ b/app/Filter/ProjectActivityTaskTitleFilter.php
@@ -3,7 +3,6 @@
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
-use Kanboard\Model\Task;
/**
* Filter activity events by task title
@@ -11,7 +10,7 @@ use Kanboard\Model\Task;
* @package filter
* @author Frederic Guillot
*/
-class ProjectActivityTaskTitleFilter extends BaseFilter implements FilterInterface
+class ProjectActivityTaskTitleFilter extends TaskTitleFilter implements FilterInterface
{
/**
* Get search attribute
@@ -23,16 +22,4 @@ class ProjectActivityTaskTitleFilter extends BaseFilter implements FilterInterfa
{
return array('title');
}
-
- /**
- * Apply filter
- *
- * @access public
- * @return FilterInterface
- */
- public function apply()
- {
- $this->query->ilike(Task::TABLE.'.title', '%'.$this->value.'%');
- return $this;
- }
}
diff --git a/app/Filter/TaskCompletionDateFilter.php b/app/Filter/TaskCompletionDateFilter.php
index 5166bebf..f206a3e2 100644
--- a/app/Filter/TaskCompletionDateFilter.php
+++ b/app/Filter/TaskCompletionDateFilter.php
@@ -11,7 +11,7 @@ use Kanboard\Model\Task;
* @package filter
* @author Frederic Guillot
*/
-class TaskCompletionDateFilter extends BaseFilter implements FilterInterface
+class TaskCompletionDateFilter extends BaseDateFilter implements FilterInterface
{
/**
* Get search attribute
diff --git a/app/Filter/TaskCreationDateFilter.php b/app/Filter/TaskCreationDateFilter.php
index 26318b3e..bb6efad6 100644
--- a/app/Filter/TaskCreationDateFilter.php
+++ b/app/Filter/TaskCreationDateFilter.php
@@ -11,7 +11,7 @@ use Kanboard\Model\Task;
* @package filter
* @author Frederic Guillot
*/
-class TaskCreationDateFilter extends BaseFilter implements FilterInterface
+class TaskCreationDateFilter extends BaseDateFilter implements FilterInterface
{
/**
* Get search attribute
diff --git a/app/Filter/TaskDueDateFilter.php b/app/Filter/TaskDueDateFilter.php
index 6ba55eb9..e36efdd0 100644
--- a/app/Filter/TaskDueDateFilter.php
+++ b/app/Filter/TaskDueDateFilter.php
@@ -11,7 +11,7 @@ use Kanboard\Model\Task;
* @package filter
* @author Frederic Guillot
*/
-class TaskDueDateFilter extends BaseFilter implements FilterInterface
+class TaskDueDateFilter extends BaseDateFilter implements FilterInterface
{
/**
* Get search attribute
diff --git a/app/Filter/TaskModificationDateFilter.php b/app/Filter/TaskModificationDateFilter.php
index d8838bce..5036e9c1 100644
--- a/app/Filter/TaskModificationDateFilter.php
+++ b/app/Filter/TaskModificationDateFilter.php
@@ -11,7 +11,7 @@ use Kanboard\Model\Task;
* @package filter
* @author Frederic Guillot
*/
-class TaskModificationDateFilter extends BaseFilter implements FilterInterface
+class TaskModificationDateFilter extends BaseDateFilter implements FilterInterface
{
/**
* Get search attribute
diff --git a/app/Filter/TaskProjectsFilter.php b/app/Filter/TaskProjectsFilter.php
index e0fc09cf..47636b1d 100644
--- a/app/Filter/TaskProjectsFilter.php
+++ b/app/Filter/TaskProjectsFilter.php
@@ -32,7 +32,12 @@ class TaskProjectsFilter extends BaseFilter implements FilterInterface
*/
public function apply()
{
- $this->query->in(Task::TABLE.'.project_id', $this->value);
+ if (empty($this->value)) {
+ $this->query->eq(Task::TABLE.'.project_id', 0);
+ } else {
+ $this->query->in(Task::TABLE.'.project_id', $this->value);
+ }
+
return $this;
}
}
diff --git a/app/Filter/TaskStartDateFilter.php b/app/Filter/TaskStartDateFilter.php
index d45bc0d4..dd30762b 100644
--- a/app/Filter/TaskStartDateFilter.php
+++ b/app/Filter/TaskStartDateFilter.php
@@ -11,7 +11,7 @@ use Kanboard\Model\Task;
* @package filter
* @author Frederic Guillot
*/
-class TaskStartDateFilter extends BaseFilter implements FilterInterface
+class TaskStartDateFilter extends BaseDateFilter implements FilterInterface
{
/**
* Get search attribute
diff --git a/app/Helper/ProjectActivityHelper.php b/app/Helper/ProjectActivityHelper.php
index 738fec66..0638a978 100644
--- a/app/Helper/ProjectActivityHelper.php
+++ b/app/Helper/ProjectActivityHelper.php
@@ -18,6 +18,33 @@ use Kanboard\Model\ProjectActivity;
class ProjectActivityHelper extends Base
{
/**
+ * Search events
+ *
+ * @access public
+ * @param string $search
+ * @return array
+ */
+ public function searchEvents($search)
+ {
+ $projects = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
+ $events = array();
+
+ if ($search !== '') {
+ $queryBuilder = $this->projectActivityLexer->build($search);
+ $queryBuilder
+ ->withFilter(new ProjectActivityProjectIdsFilter(array_keys($projects)))
+ ->getQuery()
+ ->desc(ProjectActivity::TABLE.'.id')
+ ->limit(500)
+ ;
+
+ $events = $queryBuilder->format(new ProjectActivityEventFormatter($this->container));
+ }
+
+ return $events;
+ }
+
+ /**
* Get project activity events
*
* @access public
diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php
index 7ca864f4..e384f923 100644
--- a/app/Locale/bs_BA/translations.php
+++ b/app/Locale/bs_BA/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php
index b2921de9..3c8de1ad 100644
--- a/app/Locale/cs_CZ/translations.php
+++ b/app/Locale/cs_CZ/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php
index c4743922..747fa2d1 100644
--- a/app/Locale/da_DK/translations.php
+++ b/app/Locale/da_DK/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php
index 999bf048..fa447e62 100644
--- a/app/Locale/de_DE/translations.php
+++ b/app/Locale/de_DE/translations.php
@@ -1153,4 +1153,14 @@ return array(
'Upload my avatar image' => 'Mein Avatar Bild hochladen',
'Remove my image' => 'Mein Bild entfernen',
'The OAuth2 state parameter is invalid' => 'Der OAuth2 Statusparameter ist ungültig',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php
index 9a31e485..84cf8462 100644
--- a/app/Locale/el_GR/translations.php
+++ b/app/Locale/el_GR/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php
index c3623369..e52c959b 100644
--- a/app/Locale/es_ES/translations.php
+++ b/app/Locale/es_ES/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php
index 8e5dd81f..f47852b0 100644
--- a/app/Locale/fi_FI/translations.php
+++ b/app/Locale/fi_FI/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php
index cedd6039..0c2e4955 100644
--- a/app/Locale/fr_FR/translations.php
+++ b/app/Locale/fr_FR/translations.php
@@ -1153,4 +1153,14 @@ return array(
'Upload my avatar image' => 'Uploader mon image d\'avatar',
'Remove my image' => 'Supprimer mon image',
'The OAuth2 state parameter is invalid' => 'Le paramètre "state" de OAuth2 est invalide',
+ 'User not found.' => 'Utilisateur introuvable.',
+ 'Search in activity stream' => 'Chercher dans le flux d\'activité',
+ 'My activities' => 'Mes activités',
+ 'Activity until yesterday' => 'Activités jusqu\'à hier',
+ 'Activity until today' => 'Activités jusqu\'à aujourd\'hui',
+ 'Search by creator: ' => 'Rechercher par créateur : ',
+ 'Search by creation date: ' => 'Rechercher par date de création : ',
+ 'Search by task status: ' => 'Rechercher par le statut des tâches : ',
+ 'Search by task title: ' => 'Rechercher par le titre des tâches : ',
+ 'Activity stream search' => 'Recherche dans le flux d\'activité',
);
diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php
index f642a6c1..9a2d666a 100644
--- a/app/Locale/hu_HU/translations.php
+++ b/app/Locale/hu_HU/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php
index 3f105054..9cbca60e 100644
--- a/app/Locale/id_ID/translations.php
+++ b/app/Locale/id_ID/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php
index 93ceb03f..d0209b3a 100644
--- a/app/Locale/it_IT/translations.php
+++ b/app/Locale/it_IT/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php
index b48eabd8..69ab5f17 100644
--- a/app/Locale/ja_JP/translations.php
+++ b/app/Locale/ja_JP/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/ko_KR/translations.php b/app/Locale/ko_KR/translations.php
index 8379761f..f4320c55 100644
--- a/app/Locale/ko_KR/translations.php
+++ b/app/Locale/ko_KR/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php
index 36b3db0b..f6f15937 100644
--- a/app/Locale/my_MY/translations.php
+++ b/app/Locale/my_MY/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php
index 465efb53..f3d3047a 100644
--- a/app/Locale/nb_NO/translations.php
+++ b/app/Locale/nb_NO/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php
index 3c3fa1ee..f08f5eff 100644
--- a/app/Locale/nl_NL/translations.php
+++ b/app/Locale/nl_NL/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php
index d06e347f..8222f9e1 100644
--- a/app/Locale/pl_PL/translations.php
+++ b/app/Locale/pl_PL/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php
index cdb06dea..60242d95 100644
--- a/app/Locale/pt_BR/translations.php
+++ b/app/Locale/pt_BR/translations.php
@@ -1153,4 +1153,14 @@ return array(
'Upload my avatar image' => 'Enviar a minha imagem de avatar',
'Remove my image' => 'Remover a minha imagem',
'The OAuth2 state parameter is invalid' => 'O parâmetro "state" de OAuth2 não é válido',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php
index e38344f8..956d1259 100644
--- a/app/Locale/pt_PT/translations.php
+++ b/app/Locale/pt_PT/translations.php
@@ -1153,4 +1153,14 @@ return array(
'Upload my avatar image' => 'Enviar a minha imagem de avatar',
'Remove my image' => 'Remover a minha imagem',
'The OAuth2 state parameter is invalid' => 'O parametro de estado do OAuth2 é inválido',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php
index b3503e52..1e548e0d 100644
--- a/app/Locale/ru_RU/translations.php
+++ b/app/Locale/ru_RU/translations.php
@@ -1152,5 +1152,15 @@ return array(
'Avatar' => 'Аватар',
'Upload my avatar image' => 'Загрузить моё изображение для аватара',
'Remove my image' => 'Удалить моё изображение',
- 'The OAuth2 state parameter is invalid' => 'Параметр состояние OAuth2 неправильный'
+ 'The OAuth2 state parameter is invalid' => 'Параметр состояние OAuth2 неправильный',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php
index c7070a8d..b69e6cf4 100644
--- a/app/Locale/sr_Latn_RS/translations.php
+++ b/app/Locale/sr_Latn_RS/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php
index e4728d2d..634b87d0 100644
--- a/app/Locale/sv_SE/translations.php
+++ b/app/Locale/sv_SE/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php
index 1e2fb98a..1e913f28 100644
--- a/app/Locale/th_TH/translations.php
+++ b/app/Locale/th_TH/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php
index 6e8fae2f..95bcc8a8 100644
--- a/app/Locale/tr_TR/translations.php
+++ b/app/Locale/tr_TR/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php
index decd49d8..7b0c3139 100644
--- a/app/Locale/zh_CN/translations.php
+++ b/app/Locale/zh_CN/translations.php
@@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
);
diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php
index 31cee113..d993015b 100644
--- a/app/Model/ProjectActivity.php
+++ b/app/Model/ProjectActivity.php
@@ -71,6 +71,7 @@ class ProjectActivity extends Base
'uc.avatar_path'
)
->join(Task::TABLE, 'id', 'task_id')
+ ->join(Project::TABLE, 'id', 'project_id')
->left(User::TABLE, 'uc', 'id', ProjectActivity::TABLE, 'creator_id');
}
diff --git a/app/ServiceProvider/FilterProvider.php b/app/ServiceProvider/FilterProvider.php
index 4b4dbd2d..f3918d77 100644
--- a/app/ServiceProvider/FilterProvider.php
+++ b/app/ServiceProvider/FilterProvider.php
@@ -4,6 +4,10 @@ namespace Kanboard\ServiceProvider;
use Kanboard\Core\Filter\LexerBuilder;
use Kanboard\Core\Filter\QueryBuilder;
+use Kanboard\Filter\ProjectActivityCreationDateFilter;
+use Kanboard\Filter\ProjectActivityCreatorFilter;
+use Kanboard\Filter\ProjectActivityProjectNameFilter;
+use Kanboard\Filter\ProjectActivityTaskStatusFilter;
use Kanboard\Filter\ProjectActivityTaskTitleFilter;
use Kanboard\Filter\TaskAssigneeFilter;
use Kanboard\Filter\TaskCategoryFilter;
@@ -86,8 +90,18 @@ class FilterProvider implements ServiceProviderInterface
$container['projectActivityLexer'] = $container->factory(function ($c) {
$builder = new LexerBuilder();
- $builder->withQuery($c['projectActivity']->getQuery());
- $builder->withFilter(new ProjectActivityTaskTitleFilter());
+ $builder
+ ->withQuery($c['projectActivity']->getQuery())
+ ->withFilter(new ProjectActivityTaskTitleFilter(), true)
+ ->withFilter(new ProjectActivityTaskStatusFilter())
+ ->withFilter(new ProjectActivityProjectNameFilter())
+ ->withFilter(ProjectActivityCreationDateFilter::getInstance()
+ ->setDateParser($c['dateParser'])
+ )
+ ->withFilter(ProjectActivityCreatorFilter::getInstance()
+ ->setCurrentUserId($c['userSession']->getId())
+ )
+ ;
return $builder;
});
@@ -124,17 +138,23 @@ class FilterProvider implements ServiceProviderInterface
)
->withFilter(new TaskColumnFilter())
->withFilter(new TaskCommentFilter())
- ->withFilter(new TaskCreationDateFilter())
+ ->withFilter(TaskCreationDateFilter::getInstance()
+ ->setDateParser($c['dateParser'])
+ )
->withFilter(TaskCreatorFilter::getInstance()
->setCurrentUserId($c['userSession']->getId())
)
->withFilter(new TaskDescriptionFilter())
- ->withFilter(new TaskDueDateFilter())
+ ->withFilter(TaskDueDateFilter::getInstance()
+ ->setDateParser($c['dateParser'])
+ )
->withFilter(new TaskIdFilter())
->withFilter(TaskLinkFilter::getInstance()
->setDatabase($c['db'])
)
- ->withFilter(new TaskModificationDateFilter())
+ ->withFilter(TaskModificationDateFilter::getInstance()
+ ->setDateParser($c['dateParser'])
+ )
->withFilter(new TaskProjectFilter())
->withFilter(new TaskReferenceFilter())
->withFilter(new TaskStatusFilter())
diff --git a/app/ServiceProvider/RouteProvider.php b/app/ServiceProvider/RouteProvider.php
index 0e7548d4..30d23a51 100644
--- a/app/ServiceProvider/RouteProvider.php
+++ b/app/ServiceProvider/RouteProvider.php
@@ -42,7 +42,7 @@ class RouteProvider implements ServiceProviderInterface
// Search routes
$container['route']->addRoute('search', 'search', 'index');
- $container['route']->addRoute('search/:search', 'search', 'index');
+ $container['route']->addRoute('search/activity', 'search', 'activity');
// ProjectCreation routes
$container['route']->addRoute('project/create', 'ProjectCreation', 'create');
@@ -62,6 +62,7 @@ class RouteProvider implements ServiceProviderInterface
$container['route']->addRoute('project/:project_id/enable', 'project', 'enable');
$container['route']->addRoute('project/:project_id/permissions', 'ProjectPermission', 'index');
$container['route']->addRoute('project/:project_id/import', 'taskImport', 'step1');
+ $container['route']->addRoute('project/:project_id/activity', 'activity', 'project');
// Project Overview
$container['route']->addRoute('project/:project_id/overview', 'ProjectOverview', 'show');
diff --git a/app/Template/activity/filter_dropdown.php b/app/Template/activity/filter_dropdown.php
new file mode 100644
index 00000000..8d7a7de3
--- /dev/null
+++ b/app/Template/activity/filter_dropdown.php
@@ -0,0 +1,14 @@
+<div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon" title="<?= t('Default filters') ?>"><i class="fa fa-filter fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li><a href="#" class="filter-helper filter-reset" data-filter="" title="<?= t('Keyboard shortcut: "%s"', 'r') ?>"><?= t('Reset filters') ?></a></li>
+ <li><a href="#" class="filter-helper" data-filter="creator:me"><?= t('My activities') ?></a></li>
+ <li><a href="#" class="filter-helper" data-filter="created:<=<?= date('Y-m-d', strtotime('yesterday')) ?>"><?= t('Activity until yesterday') ?></a></li>
+ <li><a href="#" class="filter-helper" data-filter="created:<=<?= date('Y-m-d')?>"><?= t('Activity until today') ?></a></li>
+ <li><a href="#" class="filter-helper" data-filter="status:closed"><?= t('Closed tasks') ?></a></li>
+ <li><a href="#" class="filter-helper" data-filter="status:open"><?= t('Open tasks') ?></a></li>
+ <li>
+ <?= $this->url->doc(t('View advanced search syntax'), 'search') ?>
+ </li>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/search/activity.php b/app/Template/search/activity.php
new file mode 100644
index 00000000..60362215
--- /dev/null
+++ b/app/Template/search/activity.php
@@ -0,0 +1,39 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li>
+ <i class="fa fa-search fa-fw"></i>
+ <?= $this->url->link(t('Search tasks'), 'search', 'index') ?>
+ </li>
+ </ul>
+ </div>
+
+ <div class="filter-box">
+ <form method="get" action="<?= $this->url->dir() ?>" class="search">
+ <?= $this->form->hidden('controller', $values) ?>
+ <?= $this->form->hidden('action', $values) ?>
+ <?= $this->form->text('search', $values, array(), array(empty($values['search']) ? 'autofocus' : '', 'placeholder="'.t('Search').'"'), 'form-input-large') ?>
+ <?= $this->render('activity/filter_dropdown') ?>
+ </form>
+ </div>
+
+ <?php if (empty($values['search'])): ?>
+ <div class="listing">
+ <h3><?= t('Advanced search') ?></h3>
+ <p><?= t('Example of query: ') ?><strong>project:"My project" creator:me</strong></p>
+ <ul>
+ <li><?= t('Search by project: ') ?><strong>project:"My project"</strong></li>
+ <li><?= t('Search by creator: ') ?><strong>creator:admin</strong></li>
+ <li><?= t('Search by creation date: ') ?><strong>created:today</strong></li>
+ <li><?= t('Search by task status: ') ?><strong>status:open</strong></li>
+ <li><?= t('Search by task title: ') ?><strong>title:"My task"</strong></li>
+ </ul>
+ <p><i class="fa fa-external-link fa-fw"></i><?= $this->url->doc(t('View advanced search syntax'), 'search') ?></p>
+ </div>
+ <?php elseif (! empty($values['search']) && $nb_events === 0): ?>
+ <p class="alert"><?= t('Nothing found.') ?></p>
+ <?php else: ?>
+ <?= $this->render('event/events', array('events' => $events)) ?>
+ <?php endif ?>
+
+</section> \ No newline at end of file
diff --git a/app/Template/search/index.php b/app/Template/search/index.php
index 9231a6f3..d5d07ed6 100644
--- a/app/Template/search/index.php
+++ b/app/Template/search/index.php
@@ -2,8 +2,8 @@
<div class="page-header">
<ul>
<li>
- <i class="fa fa-folder fa-fw"></i>
- <?= $this->url->link(t('All projects'), 'project', 'index') ?>
+ <i class="fa fa-search fa-fw"></i>
+ <?= $this->url->link(t('Activity stream search'), 'search', 'activity') ?>
</li>
</ul>
</div>
diff --git a/doc/search.markdown b/doc/search.markdown
index f6d343e9..37bb8625 100644
--- a/doc/search.markdown
+++ b/doc/search.markdown
@@ -1,7 +1,8 @@
Advanced Search Syntax
======================
-Kanboard uses a simple query language for advanced search.
+Kanboard uses a simple query language for advanced search.
+You can search in tasks, comments, subtasks, links but also in the activity stream.
Example of query
----------------
@@ -12,23 +13,23 @@ This example will return all tasks assigned to me with a due date for tomorrow a
assigne:me due:tomorrow my title
```
-Search by task id or title
---------------------------
+Global search
+-------------
+
+### Search by task id or title
- Search by task id: `#123`
- Search by task id and task title: `123`
- Search by task title: anything that doesn't match any search attributes
-Search by status
-----------------
+### Search by status
Attribute: **status**
- Query to find open tasks: `status:open`
- Query to find closed tasks: `status:closed`
-Search by assignee
-------------------
+### Search by assignee
Attribute: **assignee**
@@ -38,8 +39,7 @@ Attribute: **assignee**
- Query for unassigned tasks: `assignee:nobody`
- Query for my assigned tasks: `assignee:me`
-Search by task creator
-----------------------
+### Search by task creator
Attribute: **creator**
@@ -47,23 +47,20 @@ Attribute: **creator**
- Tasks created by John Doe: `creator:"John Doe"`
- Tasks created by the user id #1: `creator:1`
-Search by subtask assignee
---------------------------
+### Search by subtask assignee
Attribute: **subtask:assignee**
- Example: `subtask:assignee:"John Doe"`
-Search by color
----------------
+### Search by color
Attribute: **color**
- Query to search by color id: `color:blue`
- Query to search by color name: `color:"Deep Orange"`
-Search by the due date
-----------------------
+### Search by the due date
Attribute: **due**
@@ -83,8 +80,7 @@ Operators supported with a date:
- Greater than or equal: **due:>=2015-06-29**
- Lower than or equal: **due:<=2015-06-29**
-Search by modification date
----------------------------
+### Search by modification date
Attribute: **modified** or **updated**
@@ -94,29 +90,25 @@ There is also a filter by recently modified tasks: `modified:recently`.
This query will use the same value as the board highlight period configured in settings.
-Search by creation date
------------------------
+### Search by creation date
Attribute: **created**
Works in the same way as the modification date queries.
-Search by description
----------------------
+### Search by description
Attribute: **description** or **desc**
Example: `description:"text search"`
-Search by external reference
-----------------------------
+### Search by external reference
The task reference is an external id of your task, by example a ticket number from another software.
- Find tasks with a reference: `ref:1234` or `reference:TICKET-1234`
-Search by category
-------------------
+### Search by category
Attribute: **category**
@@ -124,8 +116,7 @@ Attribute: **category**
- Find all tasks that have those categories: `category:"Bug" category:"Improvements"`
- Find tasks with no category assigned: `category:none`
-Search by project
------------------
+### Search by project
Attribute: **project**
@@ -133,16 +124,14 @@ Attribute: **project**
- Find tasks by project id: `project:23`
- Find tasks for several projects: `project:"My project A" project:"My project B"`
-Search by columns
------------------
+### Search by columns
Attribute: **column**
- Find tasks by column name: `column:"Work in progress"`
- Find tasks for several columns: `column:"Backlog" column:ready`
-Search by swim-lane
--------------------
+### Search by swim-lane
Attribute: **swimlane**
@@ -150,17 +139,41 @@ Attribute: **swimlane**
- Find tasks in the default swim-lane: `swimlane:default`
- Find tasks into several swim-lanes: `swimlane:"Version 1.2" swimlane:"Version 1.3"`
-Search by task link
-------------------
+### Search by task link
Attribute: **link**
- Find tasks by link name: `link:"is a milestone of"`
- Find tasks into several links: `link:"is a milestone of" link:"relates to"`
-Search by comment
------------------
+### Search by comment
Attribute: **comment**
- Find comments that contains this title: `comment:"My comment message"`
+
+Activity stream search
+----------------------
+
+### Search events by task title
+
+Attribute: **title** or none (default)
+
+- Example: `title:"My task"`
+- Search by task id: `#123`
+
+### Search events by task status
+
+Attribute: **status**
+
+### Search by event creator
+
+Attribute: **creator**
+
+### Search by event creation date
+
+Attribute: **created**
+
+### Search events by project
+
+Attribute: **project**
diff --git a/tests/units/Filter/ProjectActivityCreationDateFilterTest.php b/tests/units/Filter/ProjectActivityCreationDateFilterTest.php
new file mode 100644
index 00000000..d679f285
--- /dev/null
+++ b/tests/units/Filter/ProjectActivityCreationDateFilterTest.php
@@ -0,0 +1,117 @@
+<?php
+
+use Kanboard\Filter\ProjectActivityCreationDateFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectActivity;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\Task;
+
+require_once __DIR__.'/../Base.php';
+
+class ProjectActivityCreationDateFilterTest extends Base
+{
+ public function testWithToday()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreationDateFilter('today');
+ $filter->setDateParser($this->container['dateParser']);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ }
+
+ public function testWithYesterday()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreationDateFilter('yesterday');
+ $filter->setDateParser($this->container['dateParser']);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(0, $events);
+ }
+
+ public function testWithIsoDate()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreationDateFilter(date('Y-m-d'));
+ $filter->setDateParser($this->container['dateParser']);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ }
+
+ public function testWithOperatorAndIsoDate()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreationDateFilter('>='.date('Y-m-d'));
+ $filter->setDateParser($this->container['dateParser']);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreationDateFilter('<'.date('Y-m-d'));
+ $filter->setDateParser($this->container['dateParser']);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(0, $events);
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreationDateFilter('>'.date('Y-m-d'));
+ $filter->setDateParser($this->container['dateParser']);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(0, $events);
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreationDateFilter('>='.date('Y-m-d'));
+ $filter->setDateParser($this->container['dateParser']);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ }
+}
diff --git a/tests/units/Filter/ProjectActivityCreatorFilterTest.php b/tests/units/Filter/ProjectActivityCreatorFilterTest.php
new file mode 100644
index 00000000..99c70322
--- /dev/null
+++ b/tests/units/Filter/ProjectActivityCreatorFilterTest.php
@@ -0,0 +1,91 @@
+<?php
+
+use Kanboard\Filter\ProjectActivityCreatorFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectActivity;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\Task;
+
+require_once __DIR__.'/../Base.php';
+
+class ProjectActivityCreatorFilterTest extends Base
+{
+ public function testWithUsername()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreatorFilter('admin');
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ }
+
+ public function testWithAnotherUsername()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreatorFilter('John Doe');
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(0, $events);
+ }
+
+ public function testWithCurrentUser()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreatorFilter('me');
+ $filter->setCurrentUserId(1);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ }
+
+ public function testWithAnotherCurrentUser()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityCreatorFilter('me');
+ $filter->setCurrentUserId(2);
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(0, $events);
+ }
+}
diff --git a/tests/units/Filter/ProjectActivityProjectNameFilterTest.php b/tests/units/Filter/ProjectActivityProjectNameFilterTest.php
new file mode 100644
index 00000000..de9d7d59
--- /dev/null
+++ b/tests/units/Filter/ProjectActivityProjectNameFilterTest.php
@@ -0,0 +1,35 @@
+<?php
+
+use Kanboard\Filter\ProjectActivityProjectNameFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectActivity;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\Task;
+
+require_once __DIR__.'/../Base.php';
+
+class ProjectActivityProjectNameFilterTest extends Base
+{
+ public function testFilterByProjectName()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+ $query = $projectActivityModel->getQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'P2')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 2)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+ $this->assertNotFalse($projectActivityModel->createEvent(2, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
+
+ $filter = new ProjectActivityProjectNameFilter('P1');
+ $filter->withQuery($query)->apply();
+ $this->assertCount(1, $query->findAll());
+ }
+}
diff --git a/tests/units/Filter/ProjectActivityTaskStatusFilterTest.php b/tests/units/Filter/ProjectActivityTaskStatusFilterTest.php
new file mode 100644
index 00000000..b8df6338
--- /dev/null
+++ b/tests/units/Filter/ProjectActivityTaskStatusFilterTest.php
@@ -0,0 +1,49 @@
+<?php
+
+use Kanboard\Filter\ProjectActivityTaskStatusFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectActivity;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\Task;
+use Kanboard\Model\TaskStatus;
+
+require_once __DIR__.'/../Base.php';
+
+class ProjectActivityTaskStatusFilterTest extends Base
+{
+ public function testFilterByTaskStatus()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $taskStatus = new TaskStatus($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
+
+ $this->assertTrue($taskStatus->close(1));
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityTaskStatusFilter('open');
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ $this->assertEquals(2, $events[0]['task_id']);
+
+ $query = $projectActivityModel->getQuery();
+ $filter = new ProjectActivityTaskStatusFilter('closed');
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ $this->assertEquals(1, $events[0]['task_id']);
+ }
+}
diff --git a/tests/units/Filter/ProjectActivityTaskTitleFilterTest.php b/tests/units/Filter/ProjectActivityTaskTitleFilterTest.php
index 6a7c23af..925a1ab2 100644
--- a/tests/units/Filter/ProjectActivityTaskTitleFilterTest.php
+++ b/tests/units/Filter/ProjectActivityTaskTitleFilterTest.php
@@ -11,7 +11,7 @@ require_once __DIR__.'/../Base.php';
class ProjectActivityTaskTitleFilterTest extends Base
{
- public function testFilterByTaskId()
+ public function testWithFullTitle()
{
$taskFinder = new TaskFinder($this->container);
$taskCreation = new TaskCreation($this->container);
@@ -31,4 +31,49 @@ class ProjectActivityTaskTitleFilterTest extends Base
$filter->withQuery($query)->apply();
$this->assertCount(1, $query->findAll());
}
+
+ public function testWithPartialTitle()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+ $query = $projectActivityModel->getQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test1', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test2', 'project_id' => 1)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
+
+ $filter = new ProjectActivityTaskTitleFilter('test');
+ $filter->withQuery($query)->apply();
+ $this->assertCount(2, $query->findAll());
+ }
+
+ public function testWithId()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $projectActivityModel = new ProjectActivity($this->container);
+ $query = $projectActivityModel->getQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
+
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test1', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test2', 'project_id' => 1)));
+
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
+ $this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
+
+ $filter = new ProjectActivityTaskTitleFilter('#2');
+ $filter->withQuery($query)->apply();
+
+ $events = $query->findAll();
+ $this->assertCount(1, $events);
+ $this->assertEquals(2, $events[0]['task_id']);
+ }
}