mirror of
https://github.com/shlinkio/shlink.git
synced 2025-03-14 04:00:57 +03:00
Added startDate and endDate params to ListShortUrlsCommand
This commit is contained in:
parent
8142801f1f
commit
5616579131
6 changed files with 215 additions and 58 deletions
|
@ -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.'
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
54
module/CLI/src/Command/Util/AbstractWithDateRangeCommand.php
Normal file
54
module/CLI/src/Command/Util/AbstractWithDateRangeCommand.php
Normal 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;
|
||||
}
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue