create exports via command (#3605)
This commit is contained in:
324
src/Command/ExportCreateCommand.php
Normal file
324
src/Command/ExportCreateCommand.php
Normal file
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Export\ServiceExport;
|
||||
use App\Mail\KimaiMailer;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ExportQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\Utils\Translator;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
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 Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ExportCreateCommand extends Command
|
||||
{
|
||||
private $serviceExport;
|
||||
private $customerRepository;
|
||||
private $projectRepository;
|
||||
private $teamRepository;
|
||||
private $userRepository;
|
||||
private $translator;
|
||||
private $mailer;
|
||||
|
||||
public function __construct(
|
||||
ServiceExport $serviceExport,
|
||||
CustomerRepository $customerRepository,
|
||||
ProjectRepository $projectRepository,
|
||||
TeamRepository $teamRepository,
|
||||
UserRepository $userRepository,
|
||||
Translator $translator,
|
||||
KimaiMailer $mailer
|
||||
) {
|
||||
$this->serviceExport = $serviceExport;
|
||||
$this->customerRepository = $customerRepository;
|
||||
$this->projectRepository = $projectRepository;
|
||||
$this->teamRepository = $teamRepository;
|
||||
$this->userRepository = $userRepository;
|
||||
$this->translator = $translator;
|
||||
$this->mailer = $mailer;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:export:create')
|
||||
->setDescription('Create exports')
|
||||
->setHelp('Create exports by several different filters and sent them via email.')
|
||||
->addOption('start', null, InputOption::VALUE_OPTIONAL, 'Start date (format: 2020-01-01, default: start of the month)', null)
|
||||
->addOption('end', null, InputOption::VALUE_OPTIONAL, 'End date (format: 2020-01-31, default: end of the month)', null)
|
||||
->addOption('timezone', null, InputOption::VALUE_OPTIONAL, 'Timezone for start and end date query (fallback: server timezone)', null)
|
||||
->addOption('locale', null, InputOption::VALUE_REQUIRED, 'The locale to use', 'en')
|
||||
->addOption('customer', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Customer IDs to filter', null)
|
||||
->addOption('project', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Project IDs to filter', null)
|
||||
->addOption('team', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Team IDs to filter', null)
|
||||
->addOption('user', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'User IDs to filter', null)
|
||||
->addOption('set-exported', null, InputOption::VALUE_NONE, 'Whether the included items should be marked as exported (default: false)')
|
||||
->addOption('template', null, InputOption::VALUE_REQUIRED, 'Export template', null)
|
||||
->addOption('exported', null, InputOption::VALUE_OPTIONAL, 'Exported filter for export entries. By default only "not exported" items are fetched (possible values: exported, all)', null)
|
||||
->addOption('directory', null, InputOption::VALUE_OPTIONAL, 'Absolute path for the rendered export documents (uses system tmp dir by default)', null)
|
||||
->addOption('email', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Email address(es) for the recipients (email will be sent out with attached file, generated exports will be removed afterwards)', null)
|
||||
->addOption('subject', null, InputOption::VALUE_OPTIONAL, 'Email subject (needs to be set if "email" is configured)', null)
|
||||
->addOption('body', null, InputOption::VALUE_OPTIONAL, 'Body of the email (needs to be set if "email" is configured)', null)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$exportedFilter = TimesheetQuery::STATE_NOT_EXPORTED;
|
||||
switch ($input->getOption('exported')) {
|
||||
case null:
|
||||
break;
|
||||
|
||||
case 'all':
|
||||
$exportedFilter = TimesheetQuery::STATE_ALL;
|
||||
break;
|
||||
|
||||
case 'exported':
|
||||
$exportedFilter = TimesheetQuery::STATE_EXPORTED;
|
||||
break;
|
||||
|
||||
default:
|
||||
$io->error('Unknown "exported" filter given');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$locale = $input->getOption('locale');
|
||||
\Locale::setDefault($locale);
|
||||
$this->translator->setLocale($locale);
|
||||
|
||||
$timezone = $input->getOption('timezone');
|
||||
if ($timezone === null) {
|
||||
$timezone = date_default_timezone_get();
|
||||
}
|
||||
$timezone = new \DateTimeZone($timezone);
|
||||
$dateFactory = new DateTimeFactory($timezone);
|
||||
|
||||
$customerIDs = $input->getOption('customer');
|
||||
$customers = [];
|
||||
if (\count($customerIDs) > 0) {
|
||||
$customers = $this->customerRepository->findByIds($customerIDs);
|
||||
}
|
||||
|
||||
$projectIDs = $input->getOption('project');
|
||||
$projects = [];
|
||||
if (\count($projectIDs) > 0) {
|
||||
$projects = $this->projectRepository->findByIds($projectIDs);
|
||||
}
|
||||
|
||||
$teamIDs = $input->getOption('team');
|
||||
$teams = [];
|
||||
if (\count($teamIDs) > 0) {
|
||||
$teams = $this->teamRepository->findByIds($teamIDs);
|
||||
}
|
||||
|
||||
$userIDs = $input->getOption('user');
|
||||
$users = [];
|
||||
if (\count($userIDs) > 0) {
|
||||
$users = $this->userRepository->findByIds($userIDs);
|
||||
}
|
||||
|
||||
$template = $input->getOption('template');
|
||||
if ($template === null) {
|
||||
$io->error('You must pass the "template" option');
|
||||
|
||||
return 1;
|
||||
}
|
||||
$renderer = $this->serviceExport->getRendererById($template);
|
||||
if ($renderer === null) {
|
||||
$io->error('Unknown export "template", available are:');
|
||||
$rows = [];
|
||||
foreach ($this->serviceExport->getRenderer() as $renderer) {
|
||||
$rows[] = [$renderer->getId()];
|
||||
}
|
||||
$io->table(['ID'], $rows);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$start = $input->getOption('start');
|
||||
if (!empty($start)) {
|
||||
try {
|
||||
$start = $dateFactory->createDateTime($start);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Invalid start date given');
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (!$start instanceof \DateTime) {
|
||||
$start = $dateFactory->getStartOfMonth();
|
||||
}
|
||||
$start->setTime(0, 0, 0);
|
||||
|
||||
$end = $input->getOption('end');
|
||||
if (!empty($end)) {
|
||||
try {
|
||||
$end = $dateFactory->createDateTime($end);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Invalid end date given');
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($end)) {
|
||||
$end = $dateFactory->getEndOfMonth($start);
|
||||
}
|
||||
|
||||
if (!$end instanceof \DateTime) {
|
||||
$end = $dateFactory->getEndOfMonth();
|
||||
}
|
||||
|
||||
$end->setTime(23, 59, 59);
|
||||
|
||||
$directory = rtrim(sys_get_temp_dir(), '/') . '/';
|
||||
if ($input->getOption('directory') !== null) {
|
||||
$directory = rtrim($input->getOption('directory'), '/') . '/';
|
||||
}
|
||||
|
||||
if (!is_dir($directory) || !is_writable($directory)) {
|
||||
$io->error('Invalid "directory" given: ' . $directory);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$subject = 'Export data available';
|
||||
$body = 'Your exported data is available, please find it attached to this email.';
|
||||
|
||||
$emails = [];
|
||||
$tmp = $input->getOption('email');
|
||||
if (\count($tmp) > 0) {
|
||||
foreach ($tmp as $email) {
|
||||
$result = filter_var($email, FILTER_VALIDATE_EMAIL);
|
||||
if ($result === false) {
|
||||
$io->error('Invalid "email" given: ' . $email);
|
||||
|
||||
return 1;
|
||||
}
|
||||
$emails[] = $email;
|
||||
}
|
||||
}
|
||||
|
||||
if ($input->getOption('subject') !== null) {
|
||||
$subject = trim($input->getOption('subject'));
|
||||
}
|
||||
|
||||
if ($input->getOption('body') !== null) {
|
||||
$body = trim($input->getOption('body'));
|
||||
}
|
||||
|
||||
$markAsExported = false;
|
||||
if ($input->getOption('set-exported')) {
|
||||
$markAsExported = true;
|
||||
}
|
||||
|
||||
// =============== VALIDATION END ===============
|
||||
|
||||
$query = new ExportQuery();
|
||||
$query->setBegin($start);
|
||||
$query->setEnd($end);
|
||||
$query->setExported($exportedFilter);
|
||||
$query->setCustomers($customers);
|
||||
$query->setProjects($projects);
|
||||
$query->setTeams($teams);
|
||||
foreach ($users as $user) {
|
||||
$query->addUser($user);
|
||||
}
|
||||
//$query->setRenderer($template);
|
||||
//$query->setMarkAsExported($markAsExported);
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$entries = $this->serviceExport->getExportItems($query);
|
||||
if (\count($entries) === 0) {
|
||||
$io->success('No entries found, skipping');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$response = $renderer->render($entries, $query);
|
||||
$file = $this->savePreview($response, $directory);
|
||||
|
||||
if ($markAsExported) {
|
||||
$this->serviceExport->setExported($entries);
|
||||
}
|
||||
|
||||
if (\count($emails) > 0) {
|
||||
foreach ($emails as $to) {
|
||||
$mail = new TemplatedEmail();
|
||||
$mail->addTo($to);
|
||||
$mail->subject($subject);
|
||||
$mail->htmlTemplate('emails/default.html.twig');
|
||||
$mail->context([
|
||||
'subject' => $subject,
|
||||
'body' => $body,
|
||||
]);
|
||||
$mail->attachFromPath($file);
|
||||
$this->mailer->send($mail);
|
||||
|
||||
$io->success('Send email with report to: ' . $to);
|
||||
}
|
||||
|
||||
unlink($file);
|
||||
} else {
|
||||
$io->success('Saved export to: ' . $file);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function savePreview(Response $response, string $directory): string
|
||||
{
|
||||
$filename = uniqid('invoice_');
|
||||
$directory = rtrim($directory, '/') . '/';
|
||||
|
||||
if ($response->headers->has('Content-Disposition')) {
|
||||
$disposition = $response->headers->get('Content-Disposition');
|
||||
$parts = explode(';', $disposition);
|
||||
foreach ($parts as $part) {
|
||||
if (stripos($part, 'filename=') === false) {
|
||||
continue;
|
||||
}
|
||||
$filename = explode('filename=', $part);
|
||||
if (\count($filename) > 1) {
|
||||
$filename = $filename[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($response instanceof BinaryFileResponse) {
|
||||
$file = $response->getFile();
|
||||
$file->move($directory, $filename);
|
||||
} else {
|
||||
(new Filesystem())->dumpFile($directory . $filename, $response->getContent());
|
||||
}
|
||||
|
||||
return $directory . $filename;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
@@ -71,7 +71,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
*/
|
||||
protected $dispatcher;
|
||||
/**
|
||||
* @var AuthorizationCheckerInterface
|
||||
* @var Security
|
||||
*/
|
||||
protected $voter;
|
||||
/**
|
||||
@@ -111,16 +111,21 @@ abstract class AbstractSpreadsheetRenderer
|
||||
'order_number' => [],
|
||||
];
|
||||
|
||||
public function __construct(TranslatorInterface $translator, LocaleFormatExtensions $dateExtension, EventDispatcherInterface $dispatcher, AuthorizationCheckerInterface $voter)
|
||||
public function __construct(TranslatorInterface $translator, LocaleFormatExtensions $dateExtension, EventDispatcherInterface $dispatcher, Security $security)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
$this->dateExtension = $dateExtension;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->voter = $voter;
|
||||
$this->voter = $security;
|
||||
}
|
||||
|
||||
protected function isRenderRate(TimesheetQuery $query): bool
|
||||
{
|
||||
if ($this->voter->getUser() === null) {
|
||||
// for command line export
|
||||
return true;
|
||||
}
|
||||
|
||||
if (null !== $query->getUser()) {
|
||||
return $this->voter->isGranted('view_rate_own_timesheet');
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
|
||||
final class KimaiMailer implements MailerInterface
|
||||
/**
|
||||
* @final
|
||||
*/
|
||||
class KimaiMailer implements MailerInterface
|
||||
{
|
||||
/**
|
||||
* @var MailerInterface
|
||||
|
||||
@@ -57,6 +57,26 @@ class CustomerRepository extends EntityRepository
|
||||
return $customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $customerIDs
|
||||
* @return Customer[]
|
||||
*/
|
||||
public function findByIds(array $customerIDs): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('c');
|
||||
$qb
|
||||
->where($qb->expr()->in('c.id', ':id'))
|
||||
->setParameter('id', $customerIDs)
|
||||
;
|
||||
|
||||
$customers = $qb->getQuery()->getResult();
|
||||
|
||||
$loader = new CustomerLoader($qb->getEntityManager(), true);
|
||||
$loader->loadResults($customers);
|
||||
|
||||
return $customers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @throws ORMException
|
||||
|
||||
@@ -54,6 +54,26 @@ class TeamRepository extends EntityRepository
|
||||
return $team;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $teamIds
|
||||
* @return Team[]
|
||||
*/
|
||||
public function findByIds(array $teamIds): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('t');
|
||||
$qb
|
||||
->where($qb->expr()->in('t.id', ':id'))
|
||||
->setParameter('id', $teamIds)
|
||||
;
|
||||
|
||||
$teams = $qb->getQuery()->getResult();
|
||||
|
||||
$loader = new TeamLoader($qb->getEntityManager());
|
||||
$loader->loadResults($teams);
|
||||
|
||||
return $teams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Team $team
|
||||
* @throws ORMException
|
||||
|
||||
@@ -69,6 +69,26 @@ class UserRepository extends EntityRepository implements UserLoaderInterface
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $userIds
|
||||
* @return User[]
|
||||
*/
|
||||
public function findByIds(array $userIds): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('u');
|
||||
$qb
|
||||
->where($qb->expr()->in('u.id', ':id'))
|
||||
->setParameter('id', $userIds)
|
||||
;
|
||||
|
||||
$users = $qb->getQuery()->getResult();
|
||||
|
||||
$loader = new UserLoader($qb->getEntityManager(), true);
|
||||
$loader->loadResults($users);
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwritten to fetch preferences when using the Profile controller actions.
|
||||
* Depends on the query, some magic mechanisms like the ParamConverter will use this method to fetch the user.
|
||||
|
||||
13
templates/emails/default.html.twig
Normal file
13
templates/emails/default.html.twig
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends 'emails/layout.html.twig' %}
|
||||
|
||||
{% block title %}
|
||||
{{ subject|trans({}, 'email') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<p>{{ body|trans({}, 'email') }}<p>
|
||||
|
||||
<spacer size="16"></spacer>
|
||||
|
||||
<p><small>{{ 'automated_email_dont_answer'|trans({}, 'email') }}</small></p>
|
||||
{% endblock %}
|
||||
253
tests/Command/ExportCreateCommandTest.php
Normal file
253
tests/Command/ExportCreateCommandTest.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Command;
|
||||
|
||||
use App\Command\ExportCreateCommand;
|
||||
use App\DataFixtures\UserFixtures;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Export\ServiceExport;
|
||||
use App\Mail\KimaiMailer;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Tests\DataFixtures\CustomerFixtures;
|
||||
use App\Tests\DataFixtures\ProjectFixtures;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
use App\Tests\KernelTestTrait;
|
||||
use App\Utils\Translator;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
/**
|
||||
* @covers \App\Command\ExportCreateCommand
|
||||
* @group integration
|
||||
*/
|
||||
class ExportCreateCommandTest extends KernelTestCase
|
||||
{
|
||||
use KernelTestTrait;
|
||||
|
||||
/**
|
||||
* @var Application
|
||||
*/
|
||||
protected $application;
|
||||
|
||||
private function clearExportFiles()
|
||||
{
|
||||
$path = __DIR__ . '/../_data/export/';
|
||||
|
||||
if (is_dir($path)) {
|
||||
$files = glob($path . '*');
|
||||
foreach ($files as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
$this->clearExportFiles();
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->clearExportFiles();
|
||||
$this->application = $this->createApplication();
|
||||
}
|
||||
|
||||
private function createApplication($mailer = null): Application
|
||||
{
|
||||
$kernel = self::bootKernel();
|
||||
$application = new Application($kernel);
|
||||
$container = self::$container;
|
||||
|
||||
$application->add(new ExportCreateCommand(
|
||||
$container->get(ServiceExport::class),
|
||||
$container->get(CustomerRepository::class),
|
||||
$container->get(ProjectRepository::class),
|
||||
$container->get(TeamRepository::class),
|
||||
$container->get(UserRepository::class),
|
||||
$container->get(Translator::class),
|
||||
$mailer ?? $container->get(KimaiMailer::class),
|
||||
));
|
||||
|
||||
return $application;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allowed option: start
|
||||
* Allowed option: end
|
||||
* Allowed option: timezone
|
||||
* Allowed option: locale
|
||||
* Allowed option: customer
|
||||
* Allowed option: project
|
||||
* Allowed option: team
|
||||
* Allowed option: user
|
||||
* Allowed option: set-exported
|
||||
* Allowed option: template
|
||||
* Allowed option: exported
|
||||
* Allowed option: directory
|
||||
* Allowed option: email
|
||||
* Allowed option: subject
|
||||
* Allowed option: body
|
||||
*
|
||||
* @param array $options
|
||||
* @return CommandTester
|
||||
*/
|
||||
protected function createExport(array $options = [])
|
||||
{
|
||||
$command = $this->application->find('kimai:export:create');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->execute(array_merge($options, [
|
||||
'command' => $command->getName(),
|
||||
]));
|
||||
|
||||
return $commandTester;
|
||||
}
|
||||
|
||||
protected function assertCommandErrors(array $options = [], string $errorMessage = '')
|
||||
{
|
||||
$commandTester = $this->createExport($options);
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('[ERROR] ' . $errorMessage, $output);
|
||||
}
|
||||
|
||||
protected function assertCommandResult(array $options = [], string $message = '')
|
||||
{
|
||||
$commandTester = $this->createExport($options);
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('[OK] ' . $message, $output);
|
||||
}
|
||||
|
||||
public function testCreateWithUnknownExportFilter()
|
||||
{
|
||||
$this->assertCommandErrors(['--exported' => 'foo'], 'Unknown "exported" filter given');
|
||||
}
|
||||
|
||||
public function testCreateWithUnknownTemplate()
|
||||
{
|
||||
$this->assertCommandErrors(['--template' => 'foo'], 'Unknown export "template", available are:');
|
||||
}
|
||||
|
||||
public function testCreateWithMissingTemplate()
|
||||
{
|
||||
$this->assertCommandErrors([], 'You must pass the "template" option');
|
||||
}
|
||||
|
||||
public function testCreateWithInvalidStart()
|
||||
{
|
||||
$this->assertCommandErrors(['--template' => 'csv', '--start' => '202ß-ä1-01'], 'Invalid start date given');
|
||||
}
|
||||
|
||||
public function testCreateWithInvalidEnd()
|
||||
{
|
||||
$this->assertCommandErrors(['--template' => 'csv', '--end' => '202ß-ä1-01'], 'Invalid end date given');
|
||||
}
|
||||
|
||||
public function testCreateWithInvalidDirectory()
|
||||
{
|
||||
$this->assertCommandErrors(['--template' => 'csv', '--directory' => '/tzuikmnbgtz/'], 'Invalid "directory" given: /tzuikmnbgtz/');
|
||||
}
|
||||
|
||||
public function testCreateWithInvalidEmail()
|
||||
{
|
||||
$this->assertCommandErrors(['--template' => 'csv', '--email' => ['tzuikmnbgtz']], 'Invalid "email" given: tzuikmnbgtz');
|
||||
}
|
||||
|
||||
public function testCreateWithInvalidEmails()
|
||||
{
|
||||
$this->assertCommandErrors(['--template' => 'csv', '--email' => ['foo@example.com', 'foo@1']], 'Invalid "email" given: foo@1');
|
||||
}
|
||||
|
||||
public function testCreateWithMissingEntries()
|
||||
{
|
||||
$options = ['--set-exported' => null, '--customer' => [1], '--template' => 'csv', '--start' => '2020-01-01', '--end' => '2020-03-01'];
|
||||
$commandTester = $this->createExport($options);
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('[OK] No entries found, skipping', $output);
|
||||
}
|
||||
|
||||
protected function prepareFixtures(\DateTime $start)
|
||||
{
|
||||
$fixture = new CustomerFixtures();
|
||||
$fixture->setAmount(1);
|
||||
$customer = $this->importFixture($fixture)[0];
|
||||
|
||||
$fixture = new ProjectFixtures();
|
||||
$fixture->setCustomers([$customer]);
|
||||
$fixture->setAmount(1);
|
||||
$project = $this->importFixture($fixture);
|
||||
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setUser($this->getUserByName(UserFixtures::USERNAME_SUPER_ADMIN));
|
||||
$fixture->setAmount(20);
|
||||
$fixture->setStartDate($start);
|
||||
$fixture->setProjects($project);
|
||||
$this->importFixture($fixture);
|
||||
|
||||
return [$customer, $project];
|
||||
}
|
||||
|
||||
public function testCreateExportByCustomer()
|
||||
{
|
||||
$start = new \DateTime('-2 months');
|
||||
$end = new \DateTime();
|
||||
|
||||
$data = $this->prepareFixtures($start);
|
||||
|
||||
$commandTester = $this->createExport(['--template' => 'csv', '--customer' => [$data[0]->getId()], '--start' => $start->format('Y-m-d'), '--end' => $end->format('Y-m-d')]);
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('Saved export to: ', $output);
|
||||
}
|
||||
|
||||
public function testCreateExportByProject()
|
||||
{
|
||||
$start = new \DateTime('-2 months');
|
||||
$end = new \DateTime();
|
||||
|
||||
$data = $this->prepareFixtures($start);
|
||||
|
||||
$commandTester = $this->createExport(['--template' => 'csv', '--project' => [$data[1][0]->getId()], '--start' => $start->format('Y-m-d'), '--end' => $end->format('Y-m-d')]);
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('Saved export to: ', $output);
|
||||
}
|
||||
|
||||
public function testCreateExportWithEmail()
|
||||
{
|
||||
$start = new \DateTime('-2 months');
|
||||
$end = new \DateTime();
|
||||
|
||||
$this->prepareFixtures($start);
|
||||
$options = ['--template' => 'csv', '--email' => ['foo@example.com', 'foo2@example.com'], '--start' => $start->format('Y-m-d'), '--end' => $end->format('Y-m-d')];
|
||||
|
||||
$mailer = $this->createMock(KimaiMailer::class);
|
||||
$mailer->expects($this->exactly(2))->method('send');
|
||||
|
||||
$application = $this->createApplication($mailer);
|
||||
$command = $application->find('kimai:export:create');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->execute(array_merge($options, [
|
||||
'command' => $command->getName(),
|
||||
]));
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('Send email with report to: foo@example.com', $output);
|
||||
$this->assertStringContainsString('Send email with report to: foo2@example.com', $output);
|
||||
}
|
||||
}
|
||||
@@ -192,8 +192,6 @@ class InvoiceCreateCommandTest extends KernelTestCase
|
||||
|
||||
protected function prepareFixtures(\DateTime $start)
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
|
||||
$fixture = new CustomerFixtures();
|
||||
$fixture->setAmount(1);
|
||||
$fixture->setCallback(function (Customer $customer) {
|
||||
|
||||
@@ -33,7 +33,6 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
@@ -55,6 +54,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
|
||||
$security = $this->createMock(Security::class);
|
||||
$security->expects($this->any())->method('getUser')->willReturn(new User());
|
||||
$security->expects($this->any())->method('isGranted')->willReturn(true);
|
||||
|
||||
$translator = $this->createMock(TranslatorInterface::class);
|
||||
$dateExtension = new LocaleFormatExtensions(new LanguageFormattings($languages), $security);
|
||||
@@ -62,10 +62,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
$dispatcher = new EventDispatcher();
|
||||
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber());
|
||||
|
||||
$authMock = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
|
||||
$authMock->method('isGranted')->willReturn(true);
|
||||
|
||||
return new $classname($translator, $dateExtension, $dispatcher, $authMock);
|
||||
return new $classname($translator, $dateExtension, $dispatcher, $security);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,7 +32,6 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
@@ -54,6 +53,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
|
||||
$security = $this->createMock(Security::class);
|
||||
$security->expects($this->any())->method('getUser')->willReturn(new User());
|
||||
$security->expects($this->any())->method('isGranted')->willReturn(true);
|
||||
|
||||
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
|
||||
$dateExtension = new LocaleFormatExtensions(new LanguageFormattings($languages), $security);
|
||||
@@ -61,10 +61,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
$dispatcher = new EventDispatcher();
|
||||
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber());
|
||||
|
||||
$authMock = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
|
||||
$authMock->method('isGranted')->willReturn(true);
|
||||
|
||||
return new $classname($translator, $dateExtension, $dispatcher, $authMock);
|
||||
return new $classname($translator, $dateExtension, $dispatcher, $security);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user