Implement advanced search queries
Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
@@ -141,5 +141,7 @@ return [
|
||||
['name' => 'comments_api#delete', 'url' => '/api/v{apiVersion}/cards/{cardId}/comments/{commentId}', 'verb' => 'DELETE'],
|
||||
|
||||
['name' => 'overview_api#upcomingCards', 'url' => '/api/v{apiVersion}/overview/upcoming', 'verb' => 'GET'],
|
||||
|
||||
['name' => 'search#search', 'url' => '/api/v{apiVersion}/search', 'verb' => 'GET'],
|
||||
]
|
||||
];
|
||||
|
||||
@@ -47,6 +47,7 @@ use OCA\Deck\Listeners\FullTextSearchEventListener;
|
||||
use OCA\Deck\Middleware\DefaultBoardMiddleware;
|
||||
use OCA\Deck\Middleware\ExceptionMiddleware;
|
||||
use OCA\Deck\Notification\Notifier;
|
||||
use OCA\Deck\Search\CardCommentProvider;
|
||||
use OCA\Deck\Search\DeckProvider;
|
||||
use OCA\Deck\Service\PermissionService;
|
||||
use OCA\Deck\Sharing\DeckShareProvider;
|
||||
@@ -95,9 +96,7 @@ class Application extends App implements IBootstrap {
|
||||
$context->injectFn(Closure::fromCallable([$this, 'registerCollaborationResources']));
|
||||
|
||||
$context->injectFn(function (IManager $shareManager) {
|
||||
if (method_exists($shareManager, 'registerShareProvider')) {
|
||||
$shareManager->registerShareProvider(DeckShareProvider::class);
|
||||
}
|
||||
$shareManager->registerShareProvider(DeckShareProvider::class);
|
||||
});
|
||||
|
||||
$context->injectFn(function (Listener $listener, IEventDispatcher $eventDispatcher) {
|
||||
@@ -122,6 +121,7 @@ class Application extends App implements IBootstrap {
|
||||
});
|
||||
|
||||
$context->registerSearchProvider(DeckProvider::class);
|
||||
$context->registerSearchProvider(CardCommentProvider::class);
|
||||
$context->registerDashboardWidget(DeckWidget::class);
|
||||
|
||||
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
|
||||
|
||||
59
lib/Controller/SearchController.php
Normal file
59
lib/Controller/SearchController.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
/*
|
||||
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace OCA\Deck\Controller;
|
||||
|
||||
use OCA\Deck\Db\Card;
|
||||
use OCA\Deck\Service\SearchService;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCSController;
|
||||
use OCP\IRequest;
|
||||
|
||||
class SearchController extends OCSController {
|
||||
|
||||
/**
|
||||
* @var SearchService
|
||||
*/
|
||||
private $searchService;
|
||||
|
||||
public function __construct(string $appName, IRequest $request, SearchService $searchService) {
|
||||
parent::__construct($appName, $request);
|
||||
$this->searchService = $searchService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function search(string $term, ?int $limit = null, ?int $cursor = null): DataResponse {
|
||||
$cards = $this->searchService->searchCards($term, $limit, $cursor);
|
||||
return new DataResponse(array_map(function (Card $card) {
|
||||
$json = $card->jsonSerialize();
|
||||
$json['relatedStack'] = $card->getRelatedStack();
|
||||
$json['relatedBoard'] = $card->getRelatedBoard();
|
||||
return $json;
|
||||
}, $cards));
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,9 @@ class Card extends RelationalEntity {
|
||||
protected $notified = false;
|
||||
protected $deletedAt = 0;
|
||||
protected $commentsUnread = 0;
|
||||
|
||||
protected $relatedStack = null;
|
||||
protected $relatedBoard = null;
|
||||
|
||||
private $databaseType = 'sqlite';
|
||||
|
||||
@@ -73,6 +76,9 @@ class Card extends RelationalEntity {
|
||||
$this->addRelation('participants');
|
||||
$this->addRelation('commentsUnread');
|
||||
$this->addResolvable('owner');
|
||||
|
||||
$this->addRelation('relatedStack');
|
||||
$this->addRelation('relatedBoard');
|
||||
}
|
||||
|
||||
public function setDatabaseType($type) {
|
||||
@@ -119,6 +125,8 @@ class Card extends RelationalEntity {
|
||||
$json['duedate'] = $this->getDuedate(true);
|
||||
unset($json['notified']);
|
||||
unset($json['descriptionPrev']);
|
||||
unset($json['relatedStack']);
|
||||
unset($json['relatedBoard']);
|
||||
return $json;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
namespace OCA\Deck\Db;
|
||||
|
||||
use Exception;
|
||||
use OCA\Deck\AppInfo\Application;
|
||||
use OCA\Deck\Search\Query\SearchQuery;
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Notification\IManager;
|
||||
|
||||
@@ -37,6 +41,8 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
private $labelMapper;
|
||||
/** @var IUserManager */
|
||||
private $userManager;
|
||||
/** @var IGroupManager */
|
||||
private $groupManager;
|
||||
/** @var IManager */
|
||||
private $notificationManager;
|
||||
private $databaseType;
|
||||
@@ -46,6 +52,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
IDBConnection $db,
|
||||
LabelMapper $labelMapper,
|
||||
IUserManager $userManager,
|
||||
IGroupManager $groupManager,
|
||||
IManager $notificationManager,
|
||||
$databaseType = 'sqlite',
|
||||
$database4ByteSupport = true
|
||||
@@ -53,6 +60,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
parent::__construct($db, 'deck_cards', Card::class);
|
||||
$this->labelMapper = $labelMapper;
|
||||
$this->userManager = $userManager;
|
||||
$this->groupManager = $groupManager;
|
||||
$this->notificationManager = $notificationManager;
|
||||
$this->databaseType = $databaseType;
|
||||
$this->database4ByteSupport = $database4ByteSupport;
|
||||
@@ -117,7 +125,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
->addOrderBy('id');
|
||||
/** @var Card $card */
|
||||
$card = $this->findEntity($qb);
|
||||
$labels = $this->labelMapper->findAssignedLabelsForCard($card->id);
|
||||
$labels = $this->labelMapper->findAssignedLabelsForCard($card->getId());
|
||||
$card->setLabels($labels);
|
||||
$this->mapOwner($card);
|
||||
return $card;
|
||||
@@ -260,25 +268,202 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
public function search($boardIds, $term, $limit = null, $offset = null) {
|
||||
public function search(array $boardIds, SearchQuery $query, int $limit = null, int $offset = null): array {
|
||||
$qb = $this->queryCardsByBoards($boardIds);
|
||||
$qb->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
||||
$qb->andWhere($qb->expr()->eq('s.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
||||
$qb->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->iLike('c.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($term) . '%')),
|
||||
$qb->expr()->iLike('c.description', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($term) . '%'))
|
||||
)
|
||||
);
|
||||
$this->extendQueryByFilter($qb, $query);
|
||||
|
||||
if (count($query->getTextTokens()) > 0) {
|
||||
$tokenMatching = $qb->expr()->andX(
|
||||
...array_map(function (string $token) use ($qb) {
|
||||
return $qb->expr()->orX(
|
||||
$qb->expr()->iLike(
|
||||
'c.title',
|
||||
$qb->createNamedParameter('%' . $this->db->escapeLikeParameter($token) . '%', IQueryBuilder::PARAM_STR),
|
||||
IQueryBuilder::PARAM_STR
|
||||
),
|
||||
$qb->expr()->iLike(
|
||||
'c.description',
|
||||
$qb->createNamedParameter('%' . $this->db->escapeLikeParameter($token) . '%', IQueryBuilder::PARAM_STR),
|
||||
IQueryBuilder::PARAM_STR
|
||||
)
|
||||
);
|
||||
}, $query->getTextTokens())
|
||||
);
|
||||
$qb->andWhere(
|
||||
$tokenMatching
|
||||
);
|
||||
}
|
||||
|
||||
$qb->groupBy('c.id');
|
||||
$qb->orderBy('c.last_modified', 'DESC');
|
||||
if ($limit !== null) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
if ($offset !== null) {
|
||||
$qb->setFirstResult($offset);
|
||||
$qb->andWhere($qb->expr()->lt('c.last_modified', $qb->createNamedParameter($offset, IQueryBuilder::PARAM_INT)));
|
||||
}
|
||||
return $this->findEntities($qb);
|
||||
|
||||
$result = $qb->execute();
|
||||
$entities = [];
|
||||
while ($row = $result->fetch()) {
|
||||
$entities[] = Card::fromRow($row);
|
||||
}
|
||||
$result->closeCursor();
|
||||
return $entities;
|
||||
}
|
||||
|
||||
public function searchComments(array $boardIds, SearchQuery $query, int $limit = null, int $offset = null): array {
|
||||
if (count($query->getTextTokens()) === 0) {
|
||||
return [];
|
||||
}
|
||||
$qb = $this->queryCardsByBoards($boardIds);
|
||||
$this->extendQueryByFilter($qb, $query);
|
||||
|
||||
$qb->innerJoin('c', 'comments', 'comments', $qb->expr()->andX(
|
||||
$qb->expr()->eq('comments.object_id', 'c.id', IQueryBuilder::PARAM_STR),
|
||||
$qb->expr()->eq('comments.object_type', $qb->createNamedParameter(Application::COMMENT_ENTITY_TYPE, IQueryBuilder::PARAM_STR))
|
||||
));
|
||||
$qb->selectAlias('comments.id', 'comment_id');
|
||||
|
||||
$tokenMatching = $qb->expr()->andX(
|
||||
...array_map(function (string $token) use ($qb) {
|
||||
return $qb->expr()->iLike(
|
||||
'comments.message',
|
||||
$qb->createNamedParameter('%' . $this->db->escapeLikeParameter($token) . '%', IQueryBuilder::PARAM_STR),
|
||||
IQueryBuilder::PARAM_STR
|
||||
);
|
||||
}, $query->getTextTokens())
|
||||
);
|
||||
$qb->andWhere(
|
||||
$tokenMatching
|
||||
);
|
||||
|
||||
$qb->groupBy('comments.id');
|
||||
$qb->orderBy('comments.id', 'DESC');
|
||||
if ($limit !== null) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
if ($offset !== null) {
|
||||
$qb->andWhere($qb->expr()->lt('comments.id', $qb->createNamedParameter($offset, IQueryBuilder::PARAM_INT)));
|
||||
}
|
||||
|
||||
$result = $qb->execute();
|
||||
$entities = $result->fetchAll();
|
||||
$result->closeCursor();
|
||||
return $entities;
|
||||
}
|
||||
|
||||
private function extendQueryByFilter(IQueryBuilder $qb, SearchQuery $query) {
|
||||
$qb->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
||||
$qb->andWhere($qb->expr()->eq('s.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
||||
$qb->innerJoin('s', 'deck_boards', 'b', $qb->expr()->eq('b.id', 's.board_id'));
|
||||
$qb->andWhere($qb->expr()->eq('b.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
foreach ($query->getTitle() as $title) {
|
||||
$qb->andWhere($qb->expr()->iLike('c.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($title->getValue()) . '%', IQueryBuilder::PARAM_STR)));
|
||||
}
|
||||
|
||||
foreach ($query->getDescription() as $description) {
|
||||
$qb->andWhere($qb->expr()->iLike('c.description', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($description->getValue()) . '%', IQueryBuilder::PARAM_STR)));
|
||||
}
|
||||
|
||||
foreach ($query->getStack() as $stack) {
|
||||
$qb->andWhere($qb->expr()->iLike('s.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($stack->getValue()) . '%', IQueryBuilder::PARAM_STR)));
|
||||
}
|
||||
|
||||
if (count($query->getTag())) {
|
||||
foreach ($query->getTag() as $index => $tag) {
|
||||
$qb->innerJoin('c', 'deck_assigned_labels', 'al' . $index, $qb->expr()->eq('c.id', 'al' . $index . '.card_id'));
|
||||
$qb->innerJoin('al'. $index, 'deck_labels', 'l' . $index, $qb->expr()->eq('al' . $index . '.label_id', 'l' . $index . '.id'));
|
||||
$qb->andWhere($qb->expr()->iLike('l' . $index . '.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($tag->getValue()) . '%', IQueryBuilder::PARAM_STR)));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($query->getDuedate() as $duedate) {
|
||||
$date = $duedate->getValue();
|
||||
$supportedFilters = ['overdue', 'today', 'week', 'month', 'none'];
|
||||
if (in_array($date, $supportedFilters, true)) {
|
||||
$currentDate = new \DateTime();
|
||||
$rangeDate = new \DateTime();
|
||||
if ($date === 'overdue') {
|
||||
$qb->andWhere($qb->expr()->lt('c.duedate', $qb->createNamedParameter($currentDate, IQueryBuilder::PARAM_DATE)));
|
||||
} elseif ($date === 'today') {
|
||||
$rangeDate->add(new \DateInterval('P1D'));
|
||||
$qb->andWhere($qb->expr()->gte('c.duedate', $qb->createNamedParameter($currentDate, IQueryBuilder::PARAM_DATE)));
|
||||
$qb->andWhere($qb->expr()->lte('c.duedate', $qb->createNamedParameter($rangeDate, IQueryBuilder::PARAM_DATE)));
|
||||
} elseif ($date === 'week') {
|
||||
$rangeDate->add(new \DateInterval('P7D'));
|
||||
$qb->andWhere($qb->expr()->gte('c.duedate', $qb->createNamedParameter($currentDate, IQueryBuilder::PARAM_DATE)));
|
||||
$qb->andWhere($qb->expr()->lte('c.duedate', $qb->createNamedParameter($rangeDate, IQueryBuilder::PARAM_DATE)));
|
||||
} elseif ($date === 'month') {
|
||||
$rangeDate->add(new \DateInterval('P1M'));
|
||||
$qb->andWhere($qb->expr()->gte('c.duedate', $qb->createNamedParameter($currentDate, IQueryBuilder::PARAM_DATE)));
|
||||
$qb->andWhere($qb->expr()->lte('c.duedate', $qb->createNamedParameter($rangeDate, IQueryBuilder::PARAM_DATE)));
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->isNull('c.duedate'));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$date = new \DateTime($date);
|
||||
if ($duedate->getComparator() === SearchQuery::COMPARATOR_LESS) {
|
||||
$qb->andWhere($qb->expr()->lt('c.duedate', $qb->createNamedParameter($date, IQueryBuilder::PARAM_DATE)));
|
||||
} elseif ($duedate->getComparator() === SearchQuery::COMPARATOR_LESS_EQUAL) {
|
||||
// take the end of the day to include due dates at the same day (as datetime does't allow just setting the day)
|
||||
$date->setTime(23, 59, 59);
|
||||
$qb->andWhere($qb->expr()->lte('c.duedate', $qb->createNamedParameter($date, IQueryBuilder::PARAM_DATE)));
|
||||
} elseif ($duedate->getComparator() === SearchQuery::COMPARATOR_MORE) {
|
||||
// take the end of the day to exclude due dates at the same day (as datetime does't allow just setting the day)
|
||||
$date->setTime(23, 59, 59);
|
||||
$qb->andWhere($qb->expr()->gt('c.duedate', $qb->createNamedParameter($date, IQueryBuilder::PARAM_DATE)));
|
||||
} elseif ($duedate->getComparator() === SearchQuery::COMPARATOR_MORE_EQUAL) {
|
||||
$qb->andWhere($qb->expr()->gte('c.duedate', $qb->createNamedParameter($date, IQueryBuilder::PARAM_DATE)));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Invalid date, ignoring
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($query->getAssigned()) > 0) {
|
||||
foreach ($query->getAssigned() as $index => $assignment) {
|
||||
$qb->innerJoin('c', 'deck_assigned_users', 'au' . $index, $qb->expr()->eq('c.id', 'au' . $index . '.card_id'));
|
||||
$assignedQueryValue = $assignment->getValue();
|
||||
$searchUsers = $this->userManager->searchDisplayName($assignment->getValue());
|
||||
$users = array_filter($searchUsers, function (IUser $user) use ($assignedQueryValue) {
|
||||
return (mb_strtolower($user->getDisplayName()) === mb_strtolower($assignedQueryValue) || $user->getUID() === $assignedQueryValue);
|
||||
});
|
||||
$groups = $this->groupManager->search($assignment->getValue());
|
||||
foreach ($searchUsers as $user) {
|
||||
$groups = array_merge($groups, $this->groupManager->getUserIdGroups($user->getUID()));
|
||||
}
|
||||
|
||||
$assignmentSearches = [];
|
||||
$hasAssignedMatches = false;
|
||||
foreach ($users as $user) {
|
||||
$hasAssignedMatches = true;
|
||||
$assignmentSearches[] = $qb->expr()->andX(
|
||||
$qb->expr()->eq('au' . $index . '.participant', $qb->createNamedParameter($user->getUID(), IQueryBuilder::PARAM_STR)),
|
||||
$qb->expr()->eq('au' . $index . '.type', $qb->createNamedParameter(Assignment::TYPE_USER, IQueryBuilder::PARAM_INT))
|
||||
);
|
||||
}
|
||||
foreach ($groups as $group) {
|
||||
$hasAssignedMatches = true;
|
||||
$assignmentSearches[] = $qb->expr()->andX(
|
||||
$qb->expr()->eq('au' . $index . '.participant', $qb->createNamedParameter($group->getGID(), IQueryBuilder::PARAM_STR)),
|
||||
$qb->expr()->eq('au' . $index . '.type', $qb->createNamedParameter(Assignment::TYPE_GROUP, IQueryBuilder::PARAM_INT))
|
||||
);
|
||||
}
|
||||
if (!$hasAssignedMatches) {
|
||||
return [];
|
||||
}
|
||||
$qb->andWhere($qb->expr()->orX(...$assignmentSearches));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function searchRaw($boardIds, $term, $limit = null, $offset = null) {
|
||||
$qb = $this->queryCardsByBoards($boardIds)
|
||||
->select('s.board_id', 'board_id')
|
||||
|
||||
84
lib/Search/CardCommentProvider.php
Normal file
84
lib/Search/CardCommentProvider.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace OCA\Deck\Search;
|
||||
|
||||
use OCA\Deck\Service\SearchService;
|
||||
use OCP\IL10N;
|
||||
use OCP\IUser;
|
||||
use OCP\Search\IProvider;
|
||||
use OCP\Search\ISearchQuery;
|
||||
use OCP\Search\SearchResult;
|
||||
|
||||
class CardCommentProvider implements IProvider {
|
||||
|
||||
/** @var SearchService */
|
||||
private $searchService;
|
||||
/** @var IL10N */
|
||||
private $l10n;
|
||||
|
||||
public function __construct(
|
||||
SearchService $searchService,
|
||||
IL10N $l10n
|
||||
) {
|
||||
$this->searchService = $searchService;
|
||||
$this->l10n = $l10n;
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
return 'deck-comment';
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->l10n->t('Card comments');
|
||||
}
|
||||
|
||||
public function search(IUser $user, ISearchQuery $query): SearchResult {
|
||||
$cursor = $query->getCursor() !== null ? (int)$query->getCursor() : null;
|
||||
$results = $this->searchService->searchComments($query->getTerm(), $query->getLimit(), $cursor);
|
||||
if (count($results) < $query->getLimit()) {
|
||||
return SearchResult::complete(
|
||||
$this->l10n->t('Card comments'),
|
||||
$results,
|
||||
);
|
||||
}
|
||||
|
||||
return SearchResult::paginated(
|
||||
$this->l10n->t('Card comments'),
|
||||
$results,
|
||||
$results[count($results) - 1]->getCommentId()
|
||||
);
|
||||
}
|
||||
|
||||
public function getOrder(string $route, array $routeParameters): int {
|
||||
// Negative value to force showing deck providers on first position if the app is opened
|
||||
// This provider always has an order 1 higher than the default DeckProvider
|
||||
if ($route === 'deck.Page.index') {
|
||||
return -4;
|
||||
}
|
||||
return 11;
|
||||
}
|
||||
}
|
||||
51
lib/Search/CommentSearchResultEntry.php
Normal file
51
lib/Search/CommentSearchResultEntry.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace OCA\Deck\Search;
|
||||
|
||||
use OCA\Deck\Db\Card;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Search\SearchResultEntry;
|
||||
|
||||
class CommentSearchResultEntry extends SearchResultEntry {
|
||||
private $commentId;
|
||||
|
||||
public function __construct(string $commentId, string $commentMessage, string $commentAuthor, Card $card, IURLGenerator $urlGenerator, IL10N $l10n) {
|
||||
parent::__construct(
|
||||
'',
|
||||
// TRANSLATORS This is describing the author and card title related to a comment e.g. "Jane on MyTask"
|
||||
$l10n->t('%s on %s', [$commentAuthor, $card->getTitle()]),
|
||||
$commentMessage,
|
||||
$urlGenerator->linkToRouteAbsolute('deck.page.index') . '#/board/' . $card->getRelatedBoard()->getId() . '/card/' . $card->getId() . '/comments/' . $commentId, // $commentId
|
||||
'icon-comment');
|
||||
$this->commentId = $commentId;
|
||||
}
|
||||
|
||||
public function getCommentId(): string {
|
||||
return $this->commentId;
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,7 @@ namespace OCA\Deck\Search;
|
||||
|
||||
use OCA\Deck\Db\Board;
|
||||
use OCA\Deck\Db\Card;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
use OCA\Deck\Db\StackMapper;
|
||||
use OCA\Deck\Service\BoardService;
|
||||
use OCA\Deck\Service\SearchService;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUser;
|
||||
use OCP\Search\IProvider;
|
||||
@@ -40,31 +38,19 @@ use OCP\Search\SearchResult;
|
||||
class DeckProvider implements IProvider {
|
||||
|
||||
/**
|
||||
* @var BoardService
|
||||
* @var SearchService
|
||||
*/
|
||||
private $boardService;
|
||||
/**
|
||||
* @var CardMapper
|
||||
*/
|
||||
private $cardMapper;
|
||||
/**
|
||||
* @var StackMapper
|
||||
*/
|
||||
private $stackMapper;
|
||||
private $searchService;
|
||||
/**
|
||||
* @var IURLGenerator
|
||||
*/
|
||||
private $urlGenerator;
|
||||
|
||||
public function __construct(
|
||||
BoardService $boardService,
|
||||
StackMapper $stackMapper,
|
||||
CardMapper $cardMapper,
|
||||
SearchService $searchService,
|
||||
IURLGenerator $urlGenerator
|
||||
) {
|
||||
$this->boardService = $boardService;
|
||||
$this->stackMapper = $stackMapper;
|
||||
$this->cardMapper = $cardMapper;
|
||||
$this->searchService = $searchService;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
@@ -77,37 +63,34 @@ class DeckProvider implements IProvider {
|
||||
}
|
||||
|
||||
public function search(IUser $user, ISearchQuery $query): SearchResult {
|
||||
$boards = $this->boardService->getUserBoards();
|
||||
|
||||
$matchedBoards = array_filter($this->boardService->getUserBoards(), static function (Board $board) use ($query) {
|
||||
return mb_stripos($board->getTitle(), $query->getTerm()) > -1;
|
||||
});
|
||||
|
||||
$matchedCards = $this->cardMapper->search(array_map(static function (Board $board) {
|
||||
return $board->getId();
|
||||
}, $boards), $query->getTerm(), $query->getLimit(), $query->getCursor());
|
||||
|
||||
$self = $this;
|
||||
$cursor = $query->getCursor() !== null ? (int)$query->getCursor() : null;
|
||||
$boardResults = $this->searchService->searchBoards($query->getTerm(), $query->getLimit(), $cursor);
|
||||
$cardResults = $this->searchService->searchCards($query->getTerm(), $query->getLimit(), $cursor);
|
||||
$results = array_merge(
|
||||
array_map(function (Board $board) {
|
||||
return new BoardSearchResultEntry($board, $this->urlGenerator);
|
||||
}, $matchedBoards),
|
||||
|
||||
array_map(function (Card $card) use ($self) {
|
||||
$board = $self->boardService->find($self->cardMapper->findBoardId($card->getId()));
|
||||
$stack = $self->stackMapper->find($card->getStackId());
|
||||
return new CardSearchResultEntry($board, $stack, $card, $this->urlGenerator);
|
||||
}, $matchedCards)
|
||||
}, $boardResults),
|
||||
array_map(function (Card $card) {
|
||||
return new CardSearchResultEntry($card->getRelatedBoard(), $card->getRelatedStack(), $card, $this->urlGenerator);
|
||||
}, $cardResults)
|
||||
);
|
||||
|
||||
return SearchResult::complete(
|
||||
if (count($cardResults) < $query->getLimit()) {
|
||||
return SearchResult::complete(
|
||||
'Deck',
|
||||
$results,
|
||||
);
|
||||
}
|
||||
|
||||
return SearchResult::paginated(
|
||||
'Deck',
|
||||
$results
|
||||
$results,
|
||||
$cardResults[count($results) - 1]->getLastModified()
|
||||
);
|
||||
}
|
||||
|
||||
public function getOrder(string $route, array $routeParameters): int {
|
||||
if ($route === 'deck.page.index') {
|
||||
if ($route === 'deck.Page.index') {
|
||||
return -5;
|
||||
}
|
||||
return 10;
|
||||
|
||||
107
lib/Search/FilterStringParser.php
Normal file
107
lib/Search/FilterStringParser.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
/*
|
||||
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace OCA\Deck\Search;
|
||||
|
||||
use OCA\Deck\Search\Query\DateQueryParameter;
|
||||
use OCA\Deck\Search\Query\SearchQuery;
|
||||
use OCA\Deck\Search\Query\StringQueryParameter;
|
||||
use OCP\IL10N;
|
||||
|
||||
class FilterStringParser {
|
||||
|
||||
/**
|
||||
* @var IL10N
|
||||
*/
|
||||
private $l10n;
|
||||
|
||||
public function __construct(IL10N $l10n) {
|
||||
$this->l10n = $l10n;
|
||||
}
|
||||
|
||||
public function parse(?string $filter): SearchQuery {
|
||||
$query = new SearchQuery();
|
||||
if (empty($filter)) {
|
||||
return $query;
|
||||
}
|
||||
$tokens = preg_split('/\s(?=([^"]*"[^"]*")*[^"]*$)/', $filter);
|
||||
foreach ($tokens as $token) {
|
||||
if (!$this->parseFilterToken($query, $token)) {
|
||||
$token = ($token[0] === '"' && $token[mb_strlen($token) - 1] === '"') ? mb_substr($token, 1, -1): $token;
|
||||
$query->addTextToken($token);
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function parseFilterToken(SearchQuery $query, string $token): bool {
|
||||
if (strpos($token, ':') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
[$type, $param] = explode(':', $token, 2);
|
||||
$type = strtolower($type);
|
||||
|
||||
$qualifier = null;
|
||||
|
||||
switch ($type) {
|
||||
case 'date':
|
||||
$comparator = SearchQuery::COMPARATOR_EQUAL;
|
||||
$value = $param;
|
||||
if ($param[0] === '<' || $param[0] === '>') {
|
||||
$orEquals = $param[1] === '=';
|
||||
$value = $orEquals ? substr($param, 2) : substr($param, 1);
|
||||
$comparator = (
|
||||
($param[0] === '<' ? SearchQuery::COMPARATOR_LESS : 0) |
|
||||
($param[0] === '>' ? SearchQuery::COMPARATOR_MORE : 0) |
|
||||
($orEquals ? SearchQuery::COMPARATOR_EQUAL : 0)
|
||||
);
|
||||
}
|
||||
$value = ($value[0] === '"' && $value[mb_strlen($value) - 1] === '"') ? mb_substr($value, 1, -1): $value;
|
||||
|
||||
$query->addDuedate(new DateQueryParameter('date', $comparator, $value));
|
||||
return true;
|
||||
case 'title':
|
||||
$query->addTitle(new StringQueryParameter('title', SearchQuery::COMPARATOR_EQUAL, $param));
|
||||
return true;
|
||||
case 'description':
|
||||
$query->addDescription(new StringQueryParameter('description', SearchQuery::COMPARATOR_EQUAL, $param));
|
||||
return true;
|
||||
case 'list':
|
||||
$query->addStack(new StringQueryParameter('list', SearchQuery::COMPARATOR_EQUAL, $param));
|
||||
return true;
|
||||
case 'tag':
|
||||
$query->addTag(new StringQueryParameter('tag', SearchQuery::COMPARATOR_EQUAL, $param));
|
||||
return true;
|
||||
case 'assigned':
|
||||
$query->addAssigned(new StringQueryParameter('assigned', SearchQuery::COMPARATOR_EQUAL, $param));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
49
lib/Search/Query/AQueryParameter.php
Normal file
49
lib/Search/Query/AQueryParameter.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/*
|
||||
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace OCA\Deck\Search\Query;
|
||||
|
||||
class AQueryParameter {
|
||||
|
||||
/** @var string */
|
||||
protected $field;
|
||||
/** @var int */
|
||||
protected $comparator;
|
||||
/** @var mixed */
|
||||
protected $value;
|
||||
|
||||
public function getValue() {
|
||||
if (is_string($this->value)) {
|
||||
$param = ($this->value[0] === '"' && $this->value[mb_strlen($this->value) - 1] === '"') ? mb_substr($this->value, 1, -1): $this->value;
|
||||
return $param;
|
||||
}
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getComparator(): int {
|
||||
return $this->comparator;
|
||||
}
|
||||
}
|
||||
38
lib/Search/Query/DateQueryParameter.php
Normal file
38
lib/Search/Query/DateQueryParameter.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/*
|
||||
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace OCA\Deck\Search\Query;
|
||||
|
||||
class DateQueryParameter extends AQueryParameter {
|
||||
/** @var string|null */
|
||||
protected $value;
|
||||
|
||||
public function __construct(string $field, int $comparator, ?string $value) {
|
||||
$this->field = $field;
|
||||
$this->comparator = $comparator;
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
109
lib/Search/Query/SearchQuery.php
Normal file
109
lib/Search/Query/SearchQuery.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/*
|
||||
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace OCA\Deck\Search\Query;
|
||||
|
||||
class SearchQuery {
|
||||
public const COMPARATOR_EQUAL = 1;
|
||||
|
||||
public const COMPARATOR_LESS = 2;
|
||||
public const COMPARATOR_MORE = 4;
|
||||
|
||||
public const COMPARATOR_LESS_EQUAL = 3;
|
||||
public const COMPARATOR_MORE_EQUAL = 5;
|
||||
|
||||
/** @var string[] */
|
||||
private $textTokens = [];
|
||||
/** @var StringQueryParameter[] */
|
||||
private $title = [];
|
||||
/** @var StringQueryParameter[] */
|
||||
private $description = [];
|
||||
/** @var StringQueryParameter[] */
|
||||
private $stack = [];
|
||||
/** @var StringQueryParameter[] */
|
||||
private $tag = [];
|
||||
/** @var StringQueryParameter[] */
|
||||
private $assigned = [];
|
||||
/** @var DateQueryParameter[] */
|
||||
private $duedate = [];
|
||||
|
||||
|
||||
public function addTextToken(string $textToken): void {
|
||||
$this->textTokens[] = $textToken;
|
||||
}
|
||||
|
||||
public function getTextTokens(): array {
|
||||
return $this->textTokens;
|
||||
}
|
||||
|
||||
public function addTitle(StringQueryParameter $title): void {
|
||||
$this->title[] = $title;
|
||||
}
|
||||
|
||||
public function getTitle(): array {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function addDescription(StringQueryParameter $description): void {
|
||||
$this->description[] = $description;
|
||||
}
|
||||
|
||||
public function getDescription(): array {
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function addStack(StringQueryParameter $stack): void {
|
||||
$this->stack[] = $stack;
|
||||
}
|
||||
|
||||
public function getStack(): array {
|
||||
return $this->stack;
|
||||
}
|
||||
|
||||
public function addTag(StringQueryParameter $tag): void {
|
||||
$this->tag[] = $tag;
|
||||
}
|
||||
|
||||
public function getTag(): array {
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
public function addAssigned(StringQueryParameter $assigned): void {
|
||||
$this->assigned[] = $assigned;
|
||||
}
|
||||
|
||||
public function getAssigned(): array {
|
||||
return $this->assigned;
|
||||
}
|
||||
|
||||
public function addDuedate(DateQueryParameter $date): void {
|
||||
$this->duedate[] = $date;
|
||||
}
|
||||
|
||||
public function getDuedate(): array {
|
||||
return $this->duedate;
|
||||
}
|
||||
}
|
||||
39
lib/Search/Query/StringQueryParameter.php
Normal file
39
lib/Search/Query/StringQueryParameter.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/*
|
||||
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace OCA\Deck\Search\Query;
|
||||
|
||||
class StringQueryParameter extends AQueryParameter {
|
||||
|
||||
/** @var string */
|
||||
protected $value;
|
||||
|
||||
public function __construct(string $field, int $comparator, string $value) {
|
||||
$this->field = $field;
|
||||
$this->comparator = $comparator;
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
@@ -511,7 +511,6 @@ class BoardService {
|
||||
$acl->setPermissionShare($share);
|
||||
$acl->setPermissionManage($manage);
|
||||
|
||||
/* Notify users about the shared board */
|
||||
$this->notificationHelper->sendBoardShared($boardId, $acl);
|
||||
|
||||
$newAcl = $this->aclMapper->insert($acl);
|
||||
@@ -599,6 +598,9 @@ class BoardService {
|
||||
$this->assignedUsersMapper->delete($assignement);
|
||||
}
|
||||
}
|
||||
|
||||
$this->notificationHelper->sendBoardShared($acl->getBoardId(), $acl);
|
||||
|
||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $acl, ActivityManager::SUBJECT_BOARD_UNSHARE);
|
||||
$this->changeHelper->boardChanged($acl->getBoardId());
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ namespace OCA\Deck\Service;
|
||||
use OCA\Deck\Activity\ActivityManager;
|
||||
use OCA\Deck\Activity\ChangeSet;
|
||||
use OCA\Deck\Db\AssignmentMapper;
|
||||
use OCA\Deck\Db\Board;
|
||||
use OCA\Deck\Db\Card;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
use OCA\Deck\Db\Acl;
|
||||
@@ -108,6 +107,11 @@ class CardService {
|
||||
$lastRead = $this->commentsManager->getReadMark('deckCard', (string)$card->getId(), $user);
|
||||
$count = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId(), $lastRead);
|
||||
$card->setCommentsUnread($count);
|
||||
|
||||
$stack = $this->stackMapper->find($card->getStackId());
|
||||
$board = $this->boardService->find($stack->getBoardId());
|
||||
$card->setRelatedStack($stack);
|
||||
$card->setRelatedBoard($board);
|
||||
}
|
||||
|
||||
public function fetchDeleted($boardId) {
|
||||
@@ -119,22 +123,6 @@ class CardService {
|
||||
return $cards;
|
||||
}
|
||||
|
||||
public function search(string $term, int $limit = null, int $offset = null): array {
|
||||
$boards = $this->boardService->getUserBoards();
|
||||
$boardIds = array_map(static function (Board $board) {
|
||||
return $board->getId();
|
||||
}, $boards);
|
||||
return $this->cardMapper->search($boardIds, $term, $limit, $offset);
|
||||
}
|
||||
|
||||
public function searchRaw(string $term, int $limit = null, int $offset = null): array {
|
||||
$boards = $this->boardService->getUserBoards();
|
||||
$boardIds = array_map(static function (Board $board) {
|
||||
return $board->getId();
|
||||
}, $boards);
|
||||
return $this->cardMapper->searchRaw($boardIds, $term, $limit, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $cardId
|
||||
* @return \OCA\Deck\Db\RelationalEntity
|
||||
|
||||
102
lib/Service/SearchService.php
Normal file
102
lib/Service/SearchService.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
/*
|
||||
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace OCA\Deck\Service;
|
||||
|
||||
use OCA\Deck\Db\Board;
|
||||
use OCA\Deck\Db\Card;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
use OCA\Deck\Search\CommentSearchResultEntry;
|
||||
use OCA\Deck\Search\FilterStringParser;
|
||||
use OCP\Comments\ICommentsManager;
|
||||
|
||||
class SearchService {
|
||||
|
||||
/** @var BoardService */
|
||||
private $boardService;
|
||||
/** @var CardMapper */
|
||||
private $cardMapper;
|
||||
/** @var CardService */
|
||||
private $cardService;
|
||||
/** @var ICommentsManager */
|
||||
private $commentsManager;
|
||||
/** @var FilterStringParser */
|
||||
private $filterStringParser;
|
||||
|
||||
public function __construct(
|
||||
BoardService $boardService,
|
||||
CardMapper $cardMapper,
|
||||
CardService $cardService,
|
||||
ICommentsManager $commentsManager,
|
||||
FilterStringParser $filterStringParser
|
||||
) {
|
||||
$this->boardService = $boardService;
|
||||
$this->cardMapper = $cardMapper;
|
||||
$this->cardService = $cardService;
|
||||
$this->commentsManager = $commentsManager;
|
||||
$this->filterStringParser = $filterStringParser;
|
||||
}
|
||||
|
||||
public function searchCards(string $term, int $limit = null, ?int $cursor = null): array {
|
||||
$boards = $this->boardService->getUserBoards();
|
||||
$boardIds = array_map(static function (Board $board) {
|
||||
return $board->getId();
|
||||
}, $boards);
|
||||
$matchedCards = $this->cardMapper->search($boardIds, $this->filterStringParser->parse($term), $limit, $cursor);
|
||||
|
||||
$self = $this;
|
||||
return array_map(function (Card $card) use ($self) {
|
||||
$self->cardService->enrich($card);
|
||||
return $card;
|
||||
}, $matchedCards);
|
||||
}
|
||||
|
||||
public function searchBoards(string $term, ?int $limit, ?int $cursor): array {
|
||||
$boards = $this->boardService->getUserBoards();
|
||||
return array_filter($boards, static function (Board $board) use ($term) {
|
||||
return mb_stripos(mb_strtolower($board->getTitle()), mb_strtolower($term)) > -1;
|
||||
});
|
||||
}
|
||||
|
||||
public function searchComments(string $term, ?int $limit = null, ?int $cursor = null): array {
|
||||
$boards = $this->boardService->getUserBoards();
|
||||
$boardIds = array_map(static function (Board $board) {
|
||||
return $board->getId();
|
||||
}, $boards);
|
||||
$matchedComments = $this->cardMapper->searchComments($boardIds, $this->filterStringParser->parse($term), $limit, $cursor);
|
||||
|
||||
$self = $this;
|
||||
return array_filter(array_map(function ($cardRow) use ($self) {
|
||||
$comment = $this->commentsManager->get($cardRow['comment_id']);
|
||||
unset($cardRow['comment_id']);
|
||||
$card = Card::fromRow($cardRow);
|
||||
$self->cardService->enrich($card);
|
||||
$user = $this->userManager->get($comment->getActorId());
|
||||
$displayName = $user ? $user->getDisplayName() : '';
|
||||
return new CommentSearchResultEntry($comment->getId(), $comment->getMessage(), $displayName, $card, $this->urlGenerator, $this->l10n);
|
||||
}, $matchedComments));
|
||||
}
|
||||
}
|
||||
@@ -9,19 +9,9 @@ use PHPUnit\Framework\Assert;
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
trait RequestTrait {
|
||||
private $baseUrl;
|
||||
private $adminUser;
|
||||
private $regularUser;
|
||||
private $cookieJar;
|
||||
|
||||
private $response;
|
||||
|
||||
public function __construct($baseUrl, $admin = 'admin', $regular_user_password = '123456') {
|
||||
$this->baseUrl = $baseUrl;
|
||||
$this->adminUser = $admin === 'admin' ? ['admin', 'admin'] : $admin;
|
||||
$this->regularUser = $regular_user_password;
|
||||
}
|
||||
|
||||
/** @var ServerContext */
|
||||
private $serverContext;
|
||||
|
||||
@@ -105,12 +95,32 @@ trait RequestTrait {
|
||||
try {
|
||||
$this->response = $client->request(
|
||||
$method,
|
||||
$this->baseUrl . $url,
|
||||
$this->severContext->getBaseUrl() . $url,
|
||||
[
|
||||
'cookies' => $this->serverContext->getCookieJar(),
|
||||
'json' => $data,
|
||||
'headers' => [
|
||||
'requesttoken' => $this->serverContext->getReqestToken()
|
||||
'requesttoken' => $this->serverContext->getReqestToken(),
|
||||
]
|
||||
]
|
||||
);
|
||||
} catch (ClientException $e) {
|
||||
$this->response = $e->getResponse();
|
||||
}
|
||||
}
|
||||
|
||||
private function sendOCSRequest($method, $url, $data = []) {
|
||||
$client = new Client;
|
||||
try {
|
||||
$this->response = $client->request(
|
||||
$method,
|
||||
$this->severContext->getBaseUrl() . $url,
|
||||
[
|
||||
'cookies' => $this->serverContext->getCookieJar(),
|
||||
'json' => $data,
|
||||
'headers' => [
|
||||
'requesttoken' => $this->serverContext->getReqestToken(),
|
||||
'OCS-APIRequest' => true,
|
||||
]
|
||||
]
|
||||
);
|
||||
@@ -10,11 +10,6 @@
|
||||
<code>(int)$subjectParams['comment']</code>
|
||||
</InvalidScalarArgument>
|
||||
</file>
|
||||
<file src="lib/AppInfo/Application.php">
|
||||
<RedundantCondition occurrences="1">
|
||||
<code>method_exists($shareManager, 'registerShareProvider')</code>
|
||||
</RedundantCondition>
|
||||
</file>
|
||||
<file src="lib/Command/UserExport.php">
|
||||
<ImplementedReturnTypeMismatch occurrences="1">
|
||||
<code>void</code>
|
||||
@@ -121,12 +116,16 @@
|
||||
</UndefinedClass>
|
||||
</file>
|
||||
<file src="lib/Db/CardMapper.php">
|
||||
<InvalidArgument occurrences="1"/>
|
||||
<InvalidScalarArgument occurrences="1">
|
||||
<code>$entity->getId()</code>
|
||||
</InvalidScalarArgument>
|
||||
<ParamNameMismatch occurrences="1">
|
||||
<code>$cardId</code>
|
||||
</ParamNameMismatch>
|
||||
<UndefinedInterfaceMethod occurrences="1">
|
||||
<code>getUserIdGroups</code>
|
||||
</UndefinedInterfaceMethod>
|
||||
</file>
|
||||
<file src="lib/Db/ChangeHelper.php">
|
||||
<UndefinedThisPropertyAssignment occurrences="3">
|
||||
@@ -280,6 +279,13 @@
|
||||
<code>\OCA\Circles\Api\v1\Circles</code>
|
||||
</UndefinedClass>
|
||||
</file>
|
||||
<file src="lib/Service/SearchService.php">
|
||||
<UndefinedThisPropertyFetch occurrences="3">
|
||||
<code>$this->l10n</code>
|
||||
<code>$this->urlGenerator</code>
|
||||
<code>$this->userManager</code>
|
||||
</UndefinedThisPropertyFetch>
|
||||
</file>
|
||||
<file src="lib/Service/StackService.php">
|
||||
<UndefinedClass occurrences="1">
|
||||
<code>BadRquestException</code>
|
||||
|
||||
@@ -111,7 +111,7 @@ class UserExportTest extends \Test\TestCase {
|
||||
->method('find')
|
||||
->willReturn($cards[0]);
|
||||
$this->assignedUserMapper->expects($this->exactly(count($boards) * count($stacks) * count($cards)))
|
||||
->method('find')
|
||||
->method('findAll')
|
||||
->willReturn([]);
|
||||
$result = $this->invokePrivate($this->userExport, 'execute', [$input, $output]);
|
||||
}
|
||||
|
||||
@@ -132,9 +132,15 @@ class BoardMapperTest extends MapperTestUtility {
|
||||
|
||||
public function testFindAll() {
|
||||
$actual = $this->boardMapper->findAll();
|
||||
$this->assertEquals($this->boards[0]->getId(), $actual[0]->getId());
|
||||
$this->assertEquals($this->boards[1]->getId(), $actual[1]->getId());
|
||||
$this->assertEquals($this->boards[2]->getId(), $actual[2]->getId());
|
||||
$this->assertEquals(1, count(array_filter($actual, function ($card) {
|
||||
return $card->getId() === $this->boards[0]->getId();
|
||||
})));
|
||||
$this->assertEquals(1, count(array_filter($actual, function ($card) {
|
||||
return $card->getId() === $this->boards[1]->getId();
|
||||
})));
|
||||
$this->assertEquals(1, count(array_filter($actual, function ($card) {
|
||||
return $card->getId() === $this->boards[2]->getId();
|
||||
})));
|
||||
}
|
||||
|
||||
public function testFindAllToDelete() {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
namespace OCA\Deck\Notification;
|
||||
|
||||
use OCA\Deck\Db\Acl;
|
||||
use OCA\Deck\Db\AssignedUsersMapper;
|
||||
use OCA\Deck\Db\AssignmentMapper;
|
||||
use OCA\Deck\Db\Board;
|
||||
use OCA\Deck\Db\BoardMapper;
|
||||
use OCA\Deck\Db\Card;
|
||||
@@ -59,7 +59,7 @@ class NotificationHelperTest extends \Test\TestCase {
|
||||
protected $cardMapper;
|
||||
/** @var BoardMapper|MockObject */
|
||||
protected $boardMapper;
|
||||
/** @var AssignedUsersMapper|MockObject */
|
||||
/** @var AssignmentMapper|MockObject */
|
||||
protected $assignedUsersMapper;
|
||||
/** @var PermissionService|MockObject */
|
||||
protected $permissionService;
|
||||
@@ -78,7 +78,7 @@ class NotificationHelperTest extends \Test\TestCase {
|
||||
parent::setUp();
|
||||
$this->cardMapper = $this->createMock(CardMapper::class);
|
||||
$this->boardMapper = $this->createMock(BoardMapper::class);
|
||||
$this->assignedUsersMapper = $this->createMock(AssignedUsersMapper::class);
|
||||
$this->assignedUsersMapper = $this->createMock(AssignmentMapper::class);
|
||||
$this->permissionService = $this->createMock(PermissionService::class);
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->notificationManager = $this->createMock(IManager::class);
|
||||
|
||||
@@ -382,7 +382,7 @@ class BoardServiceTest extends TestCase {
|
||||
$assignment = new Assignment();
|
||||
$assignment->setParticipant('admin');
|
||||
$this->assignedUsersMapper->expects($this->once())
|
||||
->method('findByUserId')
|
||||
->method('findByParticipant')
|
||||
->with('admin')
|
||||
->willReturn([$assignment]);
|
||||
$this->assignedUsersMapper->expects($this->once())
|
||||
|
||||
@@ -25,9 +25,11 @@ namespace OCA\Deck\Service;
|
||||
|
||||
use OCA\Deck\Activity\ActivityManager;
|
||||
use OCA\Deck\Db\AssignmentMapper;
|
||||
use OCA\Deck\Db\Board;
|
||||
use OCA\Deck\Db\Card;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
use OCA\Deck\Db\ChangeHelper;
|
||||
use OCA\Deck\Db\Stack;
|
||||
use OCA\Deck\Db\StackMapper;
|
||||
use OCA\Deck\Db\BoardMapper;
|
||||
use OCA\Deck\Db\LabelMapper;
|
||||
@@ -126,6 +128,17 @@ class CardServiceTest extends TestCase {
|
||||
$this->userManager->expects($this->once())
|
||||
->method('get')
|
||||
->willReturn($user);
|
||||
$this->commentsManager->expects($this->once())
|
||||
->method('getNumberOfCommentsForObject')
|
||||
->willReturn(0);
|
||||
$boardMock = $this->createMock(Board::class);
|
||||
$stackMock = $this->createMock(Stack::class);
|
||||
$this->stackMapper->expects($this->any())
|
||||
->method('find')
|
||||
->willReturn($stackMock);
|
||||
$this->boardService->expects($this->any())
|
||||
->method('find')
|
||||
->willReturn($boardMock);
|
||||
$card = new Card();
|
||||
$card->setId(1337);
|
||||
$this->cardMapper->expects($this->any())
|
||||
@@ -133,12 +146,14 @@ class CardServiceTest extends TestCase {
|
||||
->with(123)
|
||||
->willReturn($card);
|
||||
$this->assignedUsersMapper->expects($this->any())
|
||||
->method('find')
|
||||
->method('findAll')
|
||||
->with(1337)
|
||||
->willReturn(['user1', 'user2']);
|
||||
$cardExpected = new Card();
|
||||
$cardExpected->setId(1337);
|
||||
$cardExpected->setAssignedUsers(['user1', 'user2']);
|
||||
$cardExpected->setRelatedBoard($boardMock);
|
||||
$cardExpected->setRelatedStack($stackMock);
|
||||
$this->assertEquals($cardExpected, $this->cardService->find(123));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user