Added startDate and endDate params to ListShortUrlsCommand

This commit is contained in:
Alejandro Celaya 2019-12-17 09:59:54 +01:00
parent 8142801f1f
commit 5616579131
6 changed files with 215 additions and 58 deletions

View file

@ -36,7 +36,7 @@ class GenerateKeyCommand extends Command
->addOption(
'expirationDate',
'e',
InputOption::VALUE_OPTIONAL,
InputOption::VALUE_REQUIRED,
'The date in which the API key should expire. Use any valid PHP format.'
);
}

View file

@ -5,24 +5,25 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\Stdlib\ArrayUtils;
use Throwable;
use function array_map;
use function Functional\map;
use function Functional\select_keys;
use function sprintf;
class GetVisitsCommand extends Command
class GetVisitsCommand extends AbstractWithDateRangeCommand
{
public const NAME = 'short-url:visits';
private const ALIASES = ['shortcode:visits', 'short-code:visits'];
@ -36,25 +37,23 @@ class GetVisitsCommand extends Command
parent::__construct();
}
protected function configure(): void
protected function doConfigure(): void
{
$this
->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription('Returns the detailed visits information for provided short code')
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get')
->addOption(
'startDate',
's',
InputOption::VALUE_OPTIONAL,
'Allows to filter visits, returning only those older than start date'
)
->addOption(
'endDate',
'e',
InputOption::VALUE_OPTIONAL,
'Allows to filter visits, returning only those newer than end date'
);
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get');
}
protected function getStartDateDesc(): string
{
return 'Allows to filter visits, returning only those older than start date';
}
protected function getEndDateDesc(): string
{
return 'Allows to filter visits, returning only those newer than end date';
}
protected function interact(InputInterface $input, OutputInterface $output): void
@ -74,24 +73,18 @@ class GetVisitsCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$shortCode = $input->getArgument('shortCode');
$startDate = $this->getDateOption($input, 'startDate');
$endDate = $this->getDateOption($input, 'endDate');
$startDate = $this->getDateOption($input, $output, 'startDate');
$endDate = $this->getDateOption($input, $output, 'endDate');
$paginator = $this->visitsTracker->info($shortCode, new VisitsParams(new DateRange($startDate, $endDate)));
$visits = ArrayUtils::iteratorToArray($paginator->getCurrentItems());
$rows = array_map(function (Visit $visit) {
$rows = map($paginator->getCurrentItems(), function (Visit $visit) {
$rowData = $visit->jsonSerialize();
$rowData['country'] = $visit->getVisitLocation()->getCountryName();
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country']);
}, $visits);
});
ShlinkTable::fromOutput($output)->render(['Referer', 'Date', 'User agent', 'Country'], $rows);
return ExitCodes::EXIT_SUCCESS;
}
private function getDateOption(InputInterface $input, $key)
{
$value = $input->getOption($key);
return ! empty($value) ? Chronos::parse($value) : $value;
}
}

View file

@ -4,14 +4,16 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@ -26,7 +28,7 @@ use function explode;
use function implode;
use function sprintf;
class ListShortUrlsCommand extends Command
class ListShortUrlsCommand extends AbstractWithDateRangeCommand
{
use PaginatorUtilsTrait;
@ -53,7 +55,7 @@ class ListShortUrlsCommand extends Command
$this->domainConfig = $domainConfig;
}
protected function configure(): void
protected function doConfigure(): void
{
$this
->setName(self::NAME)
@ -68,7 +70,7 @@ class ListShortUrlsCommand extends Command
)
->addOption(
'searchTerm',
's',
'st',
InputOption::VALUE_REQUIRED,
'A query used to filter results by searching for it on the longUrl and shortCode fields'
)
@ -87,6 +89,16 @@ class ListShortUrlsCommand extends Command
->addOption('showTags', null, InputOption::VALUE_NONE, 'Whether to display the tags or not');
}
protected function getStartDateDesc(): string
{
return 'Allows to filter short URLs, returning only those created after "startDate"';
}
protected function getEndDateDesc(): string
{
return 'Allows to filter short URLs, returning only those created before "endDate"';
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
@ -95,10 +107,23 @@ class ListShortUrlsCommand extends Command
$tags = $input->getOption('tags');
$tags = ! empty($tags) ? explode(',', $tags) : [];
$showTags = (bool) $input->getOption('showTags');
$startDate = $this->getDateOption($input, $output, 'startDate');
$endDate = $this->getDateOption($input, $output, 'endDate');
$transformer = new ShortUrlDataTransformer($this->domainConfig);
do {
$result = $this->renderPage($input, $output, $page, $searchTerm, $tags, $showTags, $transformer);
$result = $this->renderPage(
$input,
$output,
$page,
$searchTerm,
$tags,
$showTags,
$startDate,
$endDate,
$transformer
);
$page++;
$continue = $this->isLastPage($result)
@ -108,6 +133,7 @@ class ListShortUrlsCommand extends Command
$io->newLine();
$io->success('Short URLs properly listed');
return ExitCodes::EXIT_SUCCESS;
}
@ -118,9 +144,17 @@ class ListShortUrlsCommand extends Command
?string $searchTerm,
array $tags,
bool $showTags,
?Chronos $startDate,
?Chronos $endDate,
DataTransformerInterface $transformer
): Paginator {
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
$result = $this->shortUrlService->listShortUrls(
$page,
$searchTerm,
$tags,
$this->processOrderBy($input),
new DateRange($startDate, $endDate)
);
$headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count'];
if ($showTags) {
@ -143,6 +177,7 @@ class ListShortUrlsCommand extends Command
$result,
'Page %s of %s'
));
return $result;
}

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Util;
use Cake\Chronos\Chronos;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
use function sprintf;
abstract class AbstractWithDateRangeCommand extends Command
{
final protected function configure(): void
{
$this->doConfigure();
$this
->addOption('startDate', 's', InputOption::VALUE_REQUIRED, $this->getStartDateDesc())
->addOption('endDate', 'e', InputOption::VALUE_REQUIRED, $this->getEndDateDesc());
}
protected function getDateOption(InputInterface $input, OutputInterface $output, string $key): ?Chronos
{
$value = $input->getOption($key);
if (empty($value)) {
return null;
}
try {
return Chronos::parse($value);
} catch (Throwable $e) {
$output->writeln(sprintf(
'<comment>> Ignored provided "%s" since its value "%s" is not a valid date. <</comment>',
$key,
$value
));
if ($output->isVeryVerbose()) {
$this->getApplication()->renderThrowable($e, $output);
}
return null;
}
}
abstract protected function doConfigure(): void;
abstract protected function getStartDateDesc(): string;
abstract protected function getEndDateDesc(): string;
}

View file

@ -22,6 +22,8 @@ use Symfony\Component\Console\Tester\CommandTester;
use Zend\Paginator\Adapter\ArrayAdapter;
use Zend\Paginator\Paginator;
use function sprintf;
class GetVisitsCommandTest extends TestCase
{
/** @var CommandTester */
@ -39,7 +41,7 @@ class GetVisitsCommandTest extends TestCase
}
/** @test */
public function noDateFlagsTriesToListWithoutDateRange()
public function noDateFlagsTriesToListWithoutDateRange(): void
{
$shortCode = 'abc123';
$this->visitsTracker->info($shortCode, new VisitsParams(new DateRange(null, null)))->willReturn(
@ -50,7 +52,7 @@ class GetVisitsCommandTest extends TestCase
}
/** @test */
public function providingDateFlagsTheListGetsFiltered()
public function providingDateFlagsTheListGetsFiltered(): void
{
$shortCode = 'abc123';
$startDate = '2016-01-01';
@ -69,6 +71,27 @@ class GetVisitsCommandTest extends TestCase
]);
}
/** @test */
public function providingInvalidDatesPrintsWarning(): void
{
$shortCode = 'abc123';
$startDate = 'foo';
$info = $this->visitsTracker->info($shortCode, new VisitsParams(new DateRange()))
->willReturn(new Paginator(new ArrayAdapter([])));
$this->commandTester->execute([
'shortCode' => $shortCode,
'--startDate' => $startDate,
]);
$output = $this->commandTester->getDisplay();
$info->shouldHaveBeenCalledOnce();
$this->assertStringContainsString(
sprintf('Ignored provided "startDate" since its value "%s" is not a valid date', $startDate),
$output
);
}
/** @test */
public function outputIsProperlyGenerated(): void
{

View file

@ -4,10 +4,12 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Symfony\Component\Console\Application;
@ -15,6 +17,8 @@ use Symfony\Component\Console\Tester\CommandTester;
use Zend\Paginator\Adapter\ArrayAdapter;
use Zend\Paginator\Paginator;
use function explode;
class ListShortUrlsCommandTest extends TestCase
{
/** @var CommandTester */
@ -32,17 +36,7 @@ class ListShortUrlsCommandTest extends TestCase
}
/** @test */
public function noInputCallsListJustOnce()
{
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledOnce();
$this->commandTester->setInputs(['n']);
$this->commandTester->execute([]);
}
/** @test */
public function loadingMorePagesCallsListMoreTimes()
public function loadingMorePagesCallsListMoreTimes(): void
{
// The paginator will return more than one page
$data = [];
@ -64,7 +58,7 @@ class ListShortUrlsCommandTest extends TestCase
}
/** @test */
public function havingMorePagesButAnsweringNoCallsListJustOnce()
public function havingMorePagesButAnsweringNoCallsListJustOnce(): void
{
// The paginator will return more than one page
$data = [];
@ -72,8 +66,9 @@ class ListShortUrlsCommandTest extends TestCase
$data[] = new ShortUrl('url_' . $i);
}
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter($data)))
->shouldBeCalledOnce();
$this->shortUrlService->listShortUrls(1, null, [], null, new DateRange())
->willReturn(new Paginator(new ArrayAdapter($data)))
->shouldBeCalledOnce();
$this->commandTester->setInputs(['n']);
$this->commandTester->execute([]);
@ -89,25 +84,82 @@ class ListShortUrlsCommandTest extends TestCase
}
/** @test */
public function passingPageWillMakeListStartOnThatPage()
public function passingPageWillMakeListStartOnThatPage(): void
{
$page = 5;
$this->shortUrlService->listShortUrls($page, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledOnce();
$this->shortUrlService->listShortUrls($page, null, [], null, new DateRange())
->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledOnce();
$this->commandTester->setInputs(['y']);
$this->commandTester->execute(['--page' => $page]);
}
/** @test */
public function ifTagsFlagIsProvidedTagsColumnIsIncluded()
public function ifTagsFlagIsProvidedTagsColumnIsIncluded(): void
{
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledOnce();
$this->shortUrlService->listShortUrls(1, null, [], null, new DateRange())
->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledOnce();
$this->commandTester->setInputs(['y']);
$this->commandTester->execute(['--showTags' => true]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('Tags', $output);
}
/**
* @test
* @dataProvider provideArgs
*/
public function serviceIsInvokedWithProvidedArgs(
array $commandArgs,
?int $page,
?string $searchTerm,
array $tags,
?DateRange $dateRange
): void {
$listShortUrls = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, null, $dateRange)
->willReturn(new Paginator(new ArrayAdapter()));
$this->commandTester->setInputs(['n']);
$this->commandTester->execute($commandArgs);
$listShortUrls->shouldHaveBeenCalledOnce();
}
public function provideArgs(): iterable
{
yield [[], 1, null, [], new DateRange()];
yield [['--page' => $page = 3], $page, null, [], new DateRange()];
yield [['--searchTerm' => $searchTerm = 'search this'], 1, $searchTerm, [], new DateRange()];
yield [
['--page' => $page = 3, '--searchTerm' => $searchTerm = 'search this', '--tags' => $tags = 'foo,bar'],
$page,
$searchTerm,
explode(',', $tags),
new DateRange(),
];
yield [
['--startDate' => $startDate = '2019-01-01'],
1,
null,
[],
new DateRange(Chronos::parse($startDate)),
];
yield [
['--endDate' => $endDate = '2020-05-23'],
1,
null,
[],
new DateRange(null, Chronos::parse($endDate)),
];
yield [
['--startDate' => $startDate = '2019-01-01', '--endDate' => $endDate = '2020-05-23'],
1,
null,
[],
new DateRange(Chronos::parse($startDate), Chronos::parse($endDate)),
];
}
}