diff --git a/docs/swagger.json b/docs/swagger.json index d731f858..fea8a020 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -74,10 +74,22 @@ { "name": "searchTerm", "in": "query", - "description": "A query used to filter results by searching for it on the longUrl and shortCode fields. (From Shlink 1.3.0)", + "description": "A query used to filter results by searching for it on the longUrl and shortCode fields. (Since v1.3.0)", "required": false, "type": "string" }, + { + "name": "tags", + "in": "query", + "description": "A list of tags used to filter the resultset. Only short URLs tagged with at least one of the provided tags will be returned. (Since v1.3.0)", + "required": false, + "type": "array", + "schema": { + "items": { + "type": "string" + } + } + }, { "$ref": "#/parameters/Authorization" } diff --git a/module/CLI/lang/es.mo b/module/CLI/lang/es.mo index 87678715..06f0a3db 100644 Binary files a/module/CLI/lang/es.mo and b/module/CLI/lang/es.mo differ diff --git a/module/CLI/lang/es.po b/module/CLI/lang/es.po index b2ac0670..f2c806c7 100644 --- a/module/CLI/lang/es.po +++ b/module/CLI/lang/es.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: Shlink 1.0\n" -"POT-Creation-Date: 2016-08-21 18:16+0200\n" -"PO-Revision-Date: 2016-08-21 18:16+0200\n" +"POT-Creation-Date: 2016-10-22 13:14+0200\n" +"PO-Revision-Date: 2016-10-22 13:15+0200\n" "Last-Translator: Alejandro Celaya \n" "Language-Team: \n" "Language: es_ES\n" @@ -162,6 +162,16 @@ msgstr "Listar todas las URLs cortas" msgid "The first page to list (%s items per page)" msgstr "La primera página a listar (%s elementos por página)" +msgid "" +"A query used to filter results by searching for it on the longUrl and " +"shortCode fields" +msgstr "" +"Una consulta usada para filtrar el resultado buscándola en los campos " +"longUrl y shortCode" + +msgid "A comma-separated list of tags to filter results" +msgstr "Una lista de etiquetas separadas por coma para filtrar el resultado" + msgid "Whether to display the tags or not" msgstr "Si se desea mostrar las etiquetas o no" diff --git a/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php b/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php index b8a1310c..bfe2ae6f 100644 --- a/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php +++ b/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php @@ -67,6 +67,12 @@ class ListShortcodesCommand extends Command ->addOption( 'tags', 't', + InputOption::VALUE_OPTIONAL, + $this->translator->translate('A comma-separated list of tags to filter results') + ) + ->addOption( + 'showTags', + null, InputOption::VALUE_NONE, $this->translator->translate('Whether to display the tags or not') ); @@ -76,13 +82,15 @@ class ListShortcodesCommand extends Command { $page = intval($input->getOption('page')); $searchTerm = $input->getOption('searchTerm'); - $showTags = $input->getOption('tags'); + $tags = $input->getOption('tags'); + $tags = ! empty($tags) ? explode(',', $tags) : []; + $showTags = $input->getOption('showTags'); /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); do { - $result = $this->shortUrlService->listShortUrls($page, $searchTerm); + $result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags); $page++; $table = new Table($output); diff --git a/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php b/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php index 8ab0d232..306cb87b 100644 --- a/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php +++ b/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php @@ -46,8 +46,8 @@ class ListShortcodesCommandTest extends TestCase public function noInputCallsListJustOnce() { $this->questionHelper->setInputStream($this->getInputStream('\n')); - $this->shortUrlService->listShortUrls(1, null)->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->shortUrlService->listShortUrls(1, null, [])->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $this->commandTester->execute(['command' => 'shortcode:list']); } @@ -103,8 +103,8 @@ class ListShortcodesCommandTest extends TestCase { $page = 5; $this->questionHelper->setInputStream($this->getInputStream('\n')); - $this->shortUrlService->listShortUrls($page, null)->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->shortUrlService->listShortUrls($page, null, [])->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $this->commandTester->execute([ 'command' => 'shortcode:list', @@ -118,12 +118,12 @@ class ListShortcodesCommandTest extends TestCase public function ifTagsFlagIsProvidedTagsColumnIsIncluded() { $this->questionHelper->setInputStream($this->getInputStream('\n')); - $this->shortUrlService->listShortUrls(1, null)->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->shortUrlService->listShortUrls(1, null, [])->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $this->commandTester->execute([ 'command' => 'shortcode:list', - '--tags' => true, + '--showTags' => true, ]); $output = $this->commandTester->getDisplay(); $this->assertTrue(strpos($output, 'Tags') > 0); diff --git a/module/Common/src/Paginator/Adapter/PaginableRepositoryAdapter.php b/module/Common/src/Paginator/Adapter/PaginableRepositoryAdapter.php index 017f5e89..995c7263 100644 --- a/module/Common/src/Paginator/Adapter/PaginableRepositoryAdapter.php +++ b/module/Common/src/Paginator/Adapter/PaginableRepositoryAdapter.php @@ -20,12 +20,21 @@ class PaginableRepositoryAdapter implements AdapterInterface * @var null|array|string */ private $orderBy; + /** + * @var array + */ + private $tags; - public function __construct(PaginableRepositoryInterface $paginableRepository, $searchQuery = null, $orderBy = null) - { + public function __construct( + PaginableRepositoryInterface $paginableRepository, + $searchTerm = null, + array $tags = [], + $orderBy = null + ) { $this->paginableRepository = $paginableRepository; - $this->searchTerm = trim(strip_tags($searchQuery)); + $this->searchTerm = trim(strip_tags($searchTerm)); $this->orderBy = $orderBy; + $this->tags = $tags; } /** @@ -37,7 +46,13 @@ class PaginableRepositoryAdapter implements AdapterInterface */ public function getItems($offset, $itemCountPerPage) { - return $this->paginableRepository->findList($itemCountPerPage, $offset, $this->searchTerm, $this->orderBy); + return $this->paginableRepository->findList( + $itemCountPerPage, + $offset, + $this->searchTerm, + $this->tags, + $this->orderBy + ); } /** @@ -51,6 +66,6 @@ class PaginableRepositoryAdapter implements AdapterInterface */ public function count() { - return $this->paginableRepository->countList($this->searchTerm); + return $this->paginableRepository->countList($this->searchTerm, $this->tags); } } diff --git a/module/Common/src/Repository/PaginableRepositoryInterface.php b/module/Common/src/Repository/PaginableRepositoryInterface.php index b9a75aea..7481d186 100644 --- a/module/Common/src/Repository/PaginableRepositoryInterface.php +++ b/module/Common/src/Repository/PaginableRepositoryInterface.php @@ -9,16 +9,18 @@ interface PaginableRepositoryInterface * @param int|null $limit * @param int|null $offset * @param string|null $searchTerm + * @param array $tags * @param string|array|null $orderBy * @return array */ - public function findList($limit = null, $offset = null, $searchTerm = null, $orderBy = null); + public function findList($limit = null, $offset = null, $searchTerm = null, array $tags = [], $orderBy = null); /** * Counts the number of elements in a list using provided filtering data * * @param null $searchTerm + * @param array $tags * @return int */ - public function countList($searchTerm = null); + public function countList($searchTerm = null, array $tags = []); } diff --git a/module/Common/test/Paginator/PaginableRepositoryAdapterTest.php b/module/Common/test/Paginator/PaginableRepositoryAdapterTest.php index 1135682f..196cf0c9 100644 --- a/module/Common/test/Paginator/PaginableRepositoryAdapterTest.php +++ b/module/Common/test/Paginator/PaginableRepositoryAdapterTest.php @@ -20,7 +20,7 @@ class PaginableRepositoryAdapterTest extends TestCase public function setUp() { $this->repo = $this->prophesize(PaginableRepositoryInterface::class); - $this->adapter = new PaginableRepositoryAdapter($this->repo->reveal(), 'search', 'order'); + $this->adapter = new PaginableRepositoryAdapter($this->repo->reveal(), 'search', ['foo', 'bar'], 'order'); } /** @@ -28,7 +28,7 @@ class PaginableRepositoryAdapterTest extends TestCase */ public function getItemsFallbacksToFindList() { - $this->repo->findList(10, 5, 'search', 'order')->shouldBeCalledTimes(1); + $this->repo->findList(10, 5, 'search', ['foo', 'bar'], 'order')->shouldBeCalledTimes(1); $this->adapter->getItems(5, 10); } @@ -37,7 +37,7 @@ class PaginableRepositoryAdapterTest extends TestCase */ public function countFallbacksToCountList() { - $this->repo->countList('search')->shouldBeCalledTimes(1); + $this->repo->countList('search', ['foo', 'bar'])->shouldBeCalledTimes(1); $this->adapter->count(); } } diff --git a/module/Core/src/Repository/ShortUrlRepository.php b/module/Core/src/Repository/ShortUrlRepository.php index 9becb216..a287fe0b 100644 --- a/module/Core/src/Repository/ShortUrlRepository.php +++ b/module/Core/src/Repository/ShortUrlRepository.php @@ -11,12 +11,13 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI * @param int|null $limit * @param int|null $offset * @param string|null $searchTerm + * @param array $tags * @param string|array|null $orderBy - * @return ShortUrl[] + * @return \Shlinkio\Shlink\Core\Entity\ShortUrl[] */ - public function findList($limit = null, $offset = null, $searchTerm = null, $orderBy = null) + public function findList($limit = null, $offset = null, $searchTerm = null, array $tags = [], $orderBy = null) { - $qb = $this->createListQueryBuilder($searchTerm); + $qb = $this->createListQueryBuilder($searchTerm, $tags); $qb->select('s'); if (isset($limit)) { @@ -43,11 +44,12 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI * Counts the number of elements in a list using provided filtering data * * @param null|string $searchTerm + * @param array $tags * @return int */ - public function countList($searchTerm = null) + public function countList($searchTerm = null, array $tags = []) { - $qb = $this->createListQueryBuilder($searchTerm); + $qb = $this->createListQueryBuilder($searchTerm, $tags); $qb->select('COUNT(s)'); return (int) $qb->getQuery()->getSingleScalarResult(); @@ -55,12 +57,14 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI /** * @param null|string $searchTerm + * @param array $tags * @return QueryBuilder */ - protected function createListQueryBuilder($searchTerm = null) + protected function createListQueryBuilder($searchTerm = null, array $tags = []) { $qb = $this->getEntityManager()->createQueryBuilder(); $qb->from(ShortUrl::class, 's'); + $qb->where('1=1'); // Apply search term to every searchable field if not empty if (! empty($searchTerm)) { @@ -70,11 +74,17 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI ]; // Unpack and apply search conditions - $qb->where($qb->expr()->orX(...$conditions)); + $qb->andWhere($qb->expr()->orX(...$conditions)); $searchTerm = '%' . $searchTerm . '%'; $qb->setParameter('searchPattern', $searchTerm); } + // Filter by tags if provided + if (! empty($tags)) { + $qb->join('s.tags', 't') + ->andWhere($qb->expr()->in('t.name', $tags)); + } + return $qb; } } diff --git a/module/Core/src/Service/ShortUrlService.php b/module/Core/src/Service/ShortUrlService.php index e3c2e451..faf778b1 100644 --- a/module/Core/src/Service/ShortUrlService.php +++ b/module/Core/src/Service/ShortUrlService.php @@ -33,13 +33,14 @@ class ShortUrlService implements ShortUrlServiceInterface /** * @param int $page * @param string $searchQuery + * @param array $tags * @return ShortUrl[]|Paginator */ - public function listShortUrls($page = 1, $searchQuery = null) + public function listShortUrls($page = 1, $searchQuery = null, array $tags = []) { /** @var ShortUrlRepository $repo */ $repo = $this->em->getRepository(ShortUrl::class); - $paginator = new Paginator(new PaginableRepositoryAdapter($repo, $searchQuery)); + $paginator = new Paginator(new PaginableRepositoryAdapter($repo, $searchQuery, $tags)); $paginator->setItemCountPerPage(PaginableRepositoryAdapter::ITEMS_PER_PAGE) ->setCurrentPageNumber($page); diff --git a/module/Core/src/Service/ShortUrlServiceInterface.php b/module/Core/src/Service/ShortUrlServiceInterface.php index 7fbc1b4e..cdf9a45f 100644 --- a/module/Core/src/Service/ShortUrlServiceInterface.php +++ b/module/Core/src/Service/ShortUrlServiceInterface.php @@ -10,9 +10,10 @@ interface ShortUrlServiceInterface /** * @param int $page * @param string $searchQuery + * @param array $tags * @return ShortUrl[]|Paginator */ - public function listShortUrls($page = 1, $searchQuery = null); + public function listShortUrls($page = 1, $searchQuery = null, array $tags = []); /** * @param string $shortCode diff --git a/module/Rest/src/Action/ListShortcodesAction.php b/module/Rest/src/Action/ListShortcodesAction.php index d6f6782d..dd08c071 100644 --- a/module/Rest/src/Action/ListShortcodesAction.php +++ b/module/Rest/src/Action/ListShortcodesAction.php @@ -74,6 +74,7 @@ class ListShortcodesAction extends AbstractRestAction return [ isset($query['page']) ? $query['page'] : 1, isset($query['searchTerm']) ? $query['searchTerm'] : null, + isset($query['tags']) ? $query['tags'] : [], ]; } } diff --git a/module/Rest/test/Action/ListShortcodesActionTest.php b/module/Rest/test/Action/ListShortcodesActionTest.php index 9ff2e54a..df15bb0c 100644 --- a/module/Rest/test/Action/ListShortcodesActionTest.php +++ b/module/Rest/test/Action/ListShortcodesActionTest.php @@ -34,8 +34,8 @@ class ListShortcodesActionTest extends TestCase public function properListReturnsSuccessResponse() { $page = 3; - $this->service->listShortUrls($page, null)->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->service->listShortUrls($page, null, [])->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $response = $this->action->__invoke( ServerRequestFactory::fromGlobals()->withQueryParams([ @@ -52,8 +52,8 @@ class ListShortcodesActionTest extends TestCase public function anExceptionsReturnsErrorResponse() { $page = 3; - $this->service->listShortUrls($page, null)->willThrow(\Exception::class) - ->shouldBeCalledTimes(1); + $this->service->listShortUrls($page, null, [])->willThrow(\Exception::class) + ->shouldBeCalledTimes(1); $response = $this->action->__invoke( ServerRequestFactory::fromGlobals()->withQueryParams([